Merge branch 'MDL-64851-master' of https://github.com/s-cenni/MDL-64851
authorAdrian Greeve <abgreeve@gmail.com>
Fri, 3 May 2019 02:43:28 +0000 (10:43 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Fri, 3 May 2019 02:43:28 +0000 (10:43 +0800)
746 files changed:
admin/cli/install.php
admin/renderer.php
admin/settings/analytics.php
admin/settings/development.php
admin/settings/users.php
admin/tool/analytics/amd/build/model.min.js
admin/tool/analytics/amd/src/model.js
admin/tool/analytics/classes/clihelper.php
admin/tool/analytics/classes/output/form/edit_model.php
admin/tool/analytics/classes/output/form/import_model.php
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/classes/output/restorable_models.php [new file with mode: 0644]
admin/tool/analytics/classes/task/predict_models.php
admin/tool/analytics/classes/task/train_models.php
admin/tool/analytics/cli/guess_course_start_and_end.php
admin/tool/analytics/createmodel.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/restoredefault.php [new file with mode: 0644]
admin/tool/analytics/templates/evaluation_options.mustache [new file with mode: 0644]
admin/tool/analytics/templates/models_list.mustache
admin/tool/analytics/templates/restorable_models.mustache [new file with mode: 0644]
admin/tool/analytics/tests/behat/restoredefault.feature [new file with mode: 0644]
admin/tool/analytics/version.php
admin/tool/behat/renderer.php
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/external/data_request_exporter.php
admin/tool/dataprivacy/classes/output/data_registry_page.php
admin/tool/dataprivacy/classes/output/data_requests_table.php
admin/tool/dataprivacy/createdatarequest.php
admin/tool/dataprivacy/createdatarequest_form.php
admin/tool/dataprivacy/db/access.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/resubmitrequest.php
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/datadelete.feature
admin/tool/dataprivacy/version.php
admin/tool/generator/classes/make_testplan_form.php
admin/tool/generator/classes/testplan_backend.php
admin/tool/generator/upgrade.txt [new file with mode: 0644]
admin/tool/log/backup/moodle2/restore_tool_log_logstore_subplugin.class.php
admin/tool/log/classes/helper/buffered_writer.php
admin/tool/log/classes/helper/reader.php
admin/tool/log/classes/local/privacy/helper.php
admin/tool/log/store/database/backup/moodle2/restore_logstore_database_subplugin.class.php
admin/tool/log/store/database/classes/log/store.php
admin/tool/log/store/database/db/upgrade.php
admin/tool/log/store/database/lang/en/logstore_database.php
admin/tool/log/store/database/settings.php
admin/tool/log/store/database/tests/store_test.php
admin/tool/log/store/database/version.php
admin/tool/log/store/standard/backup/moodle2/restore_logstore_standard_subplugin.class.php
admin/tool/log/store/standard/classes/log/store.php
admin/tool/log/store/standard/db/upgrade.php
admin/tool/log/store/standard/lang/en/logstore_standard.php
admin/tool/log/store/standard/settings.php
admin/tool/log/store/standard/tests/fixtures/event.php
admin/tool/log/store/standard/tests/store_test.php
admin/tool/log/store/standard/version.php
admin/tool/log/upgrade.txt
admin/tool/mobile/classes/api.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/monitor/classes/privacy/provider.php
admin/tool/uploaduser/locallib.php
admin/tool/usertours/lang/en/tool_usertours.php
admin/tool/xmldb/tests/behat/mandatory_persistent_fields.feature [new file with mode: 0644]
admin/user/user_bulk_message.php
analytics/classes/analysis.php
analytics/classes/local/time_splitting/base.php
analytics/classes/local/time_splitting/periodic.php
analytics/classes/local/time_splitting/upcoming_periodic.php
analytics/classes/manager.php
analytics/classes/model.php
analytics/lib.php [new file with mode: 0644]
analytics/tests/analysis_test.php [new file with mode: 0644]
analytics/tests/fixtures/db_analytics_php/no_teaching.php
analytics/tests/fixtures/test_timesplitting_upcoming_seconds.php [new file with mode: 0644]
analytics/tests/manager_test.php
analytics/tests/model_test.php
analytics/tests/prediction_test.php
analytics/tests/stats_test.php
analytics/upgrade.txt
auth/email/tests/behat/signup.feature
auth/nologin/auth.php
auth/oauth2/classes/auth.php
auth/oauth2/lang/en/auth_oauth2.php
auth/oauth2/tests/auth_test.php [new file with mode: 0644]
auth/upgrade.txt
backup/controller/backup_controller.class.php
backup/controller/restore_controller.class.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_controller_dbops.class.php
backup/util/helper/async_helper.class.php
backup/util/helper/restore_log_rule.class.php
backup/util/helper/tests/backup_encode_content_test.php
backup/util/helper/tests/restore_log_rule_test.php
backup/util/settings/setting_dependency.class.php
backup/util/settings/tests/settings_test.php
backup/util/ui/classes/privacy/provider.php
backup/util/ui/tests/behat/duplicate_activities.feature
badges/classes/privacy/provider.php
badges/criteria/award_criteria_courseset.php
badges/tests/badgeslib_test.php
badges/tests/privacy_test.php
blocks/course_list/block_course_list.php
blocks/html/lib.php
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/src/view.js
blocks/myoverview/block_myoverview.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lib.php
blocks/myoverview/settings.php [new file with mode: 0644]
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/templates/view-list.mustache
blocks/myoverview/templates/view-summary.mustache
blocks/navigation/lang/en/block_navigation.php
blog/classes/external/post_exporter.php
blog/tests/external_test.php
blog/upgrade.txt
cache/classes/loaders.php
cache/stores/mongodb/addinstanceform.php
calendar/amd/build/modal_event_form.min.js
calendar/amd/build/view_manager.min.js
calendar/amd/src/modal_event_form.js
calendar/amd/src/view_manager.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/event_exporter.php
calendar/classes/external/event_exporter_base.php
calendar/classes/privacy/provider.php
calendar/externallib.php
calendar/lib.php
calendar/templates/calendar_day.mustache
calendar/templates/day_detailed.mustache
calendar/templates/event_details.mustache [new file with mode: 0644]
calendar/templates/event_item.mustache
calendar/templates/event_list.mustache
calendar/templates/event_summary_body.mustache
calendar/templates/month_detailed.mustache
calendar/templates/month_mini.mustache
calendar/templates/upcoming_mini.mustache
calendar/tests/behat/calendar.feature
calendar/tests/externallib_test.php
calendar/tests/privacy_test.php
calendar/view.php
cohort/classes/privacy/provider.php
competency/classes/course_module_competency.php
competency/lib.php
competency/tests/course_module_competency_test.php [new file with mode: 0644]
competency/tests/lib_test.php
completion/classes/privacy/provider.php
completion/criteria/completion_criteria.php
completion/cron.php [deleted file]
completion/tests/behat/behat_completion.php
completion/tests/behat/completion_course_page_checkboxes.feature [new file with mode: 0644]
completion/upgrade.txt
course/classes/analytics/target/course_competencies.php [moved from lib/classes/analytics/target/course_competencies.php with 92% similarity]
course/classes/analytics/target/course_completion.php [moved from lib/classes/analytics/target/course_completion.php with 90% similarity]
course/classes/analytics/target/course_dropout.php [moved from lib/classes/analytics/target/course_dropout.php with 92% similarity]
course/classes/analytics/target/course_enrolments.php [moved from lib/classes/analytics/target/course_enrolments.php with 90% similarity]
course/classes/analytics/target/course_gradetopass.php [moved from lib/classes/analytics/target/course_gradetopass.php with 94% similarity]
course/classes/analytics/target/no_teaching.php [moved from lib/classes/analytics/target/no_teaching.php with 95% similarity]
course/classes/category.php
course/classes/list_element.php
course/classes/management/helper.php
course/classes/management_renderer.php
course/classes/search/course.php
course/classes/search/customfield.php
course/completion_form.php
course/externallib.php
course/index.php
course/info.php
course/lib.php
course/management.php
course/publish/metadata.php
course/renderer.php
course/request.php
course/search.php
course/tests/behat/app_course_completion.feature [new file with mode: 0644]
course/tests/behat/behat_course.php
course/tests/behat/course_browsing.feature [new file with mode: 0644]
course/tests/behat/customfields_locked.feature
course/tests/behat/customfields_visibility.feature
course/tests/category_test.php
course/tests/search_test.php
course/upgrade.txt
course/view.php
customfield/field/checkbox/lang/en/customfield_checkbox.php
customfield/field/date/lang/en/customfield_date.php
customfield/field/select/lang/en/customfield_select.php
customfield/field/text/classes/field_controller.php
customfield/field/text/lang/en/customfield_text.php
customfield/field/text/tests/behat/field.feature
customfield/field/textarea/lang/en/customfield_textarea.php
customfield/tests/behat/edit_fields_settings.feature
customfield/tests/behat/required_field.feature
customfield/tests/behat/unique_field.feature
enrol/classes/privacy/provider.php
enrol/externallib.php
enrol/guest/classes/external.php
enrol/index.php
enrol/renderer.php
enrol/self/externallib.php
enrol/tests/externallib_test.php
enrol/upgrade.txt
filter/algebra/tests/filter_test.php
filter/mathjaxloader/filter.php
grade/grading/classes/privacy/gradingform_legacy_polyfill.php
grade/grading/tests/privacy_legacy_polyfill_test.php
grade/import/csv/classes/load_data.php
grade/import/csv/tests/load_data_test.php
grade/import/direct/lang/en/gradeimport_direct.php
group/classes/privacy/provider.php
install/lang/ar/admin.php
install/lang/de_wp/langconfig.php [new file with mode: 0644]
install/lang/en_wp/langconfig.php [new file with mode: 0644]
install/lang/es_wp/langconfig.php [new file with mode: 0644]
install/lang/fi/moodle.php
install/lang/it_wp/langconfig.php [new file with mode: 0644]
install/lang/no_wp/langconfig.php [new file with mode: 0644]
install/lang/pt/admin.php
install/lang/pt/install.php
lang/en/access.php
lang/en/admin.php
lang/en/analytics.php
lang/en/backup.php
lang/en/badges.php
lang/en/cache.php
lang/en/course.php
lang/en/customfield.php
lang/en/deprecated.txt
lang/en/error.php
lang/en/message.php
lang/en/moodle.php
lang/en/question.php
lang/en/role.php
lang/en/user.php
lib/accesslib.php
lib/amd/build/event.min.js
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-course-selector.min.js
lib/amd/src/event.js
lib/amd/src/form-autocomplete.js
lib/amd/src/form-course-selector.js
lib/authlib.php
lib/badgeslib.php
lib/behat/classes/behat_command.php
lib/behat/classes/partial_named_selector.php
lib/behat/classes/util.php
lib/classes/analytics/time_splitting/upcoming_3_days.php [new file with mode: 0644]
lib/classes/analytics/time_splitting/upcoming_fortnight.php [new file with mode: 0644]
lib/classes/component.php
lib/classes/event/question_base.php [new file with mode: 0644]
lib/classes/event/question_category_base.php [new file with mode: 0644]
lib/classes/event/question_category_created.php
lib/classes/event/question_category_deleted.php [new file with mode: 0644]
lib/classes/event/question_category_moved.php [new file with mode: 0644]
lib/classes/event/question_category_updated.php [new file with mode: 0644]
lib/classes/event/question_category_viewed.php [new file with mode: 0644]
lib/classes/event/question_created.php [new file with mode: 0644]
lib/classes/event/question_deleted.php [new file with mode: 0644]
lib/classes/event/question_moved.php [new file with mode: 0644]
lib/classes/event/question_updated.php [new file with mode: 0644]
lib/classes/event/question_viewed.php [new file with mode: 0644]
lib/classes/event/questions_exported.php [new file with mode: 0644]
lib/classes/event/questions_imported.php [new file with mode: 0644]
lib/classes/external/exporter.php
lib/classes/lock/db_record_lock_factory.php
lib/classes/lock/file_lock_factory.php
lib/classes/lock/postgres_lock_factory.php
lib/classes/message/manager.php
lib/classes/message/message.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/string_manager_standard.php
lib/classes/task/completion_daily_task.php
lib/classes/task/completion_regular_task.php
lib/classes/task/send_failed_login_notifications_task.php
lib/completionlib.php
lib/datalib.php
lib/db/access.php
lib/db/analytics.php
lib/db/caches.php
lib/db/install.xml
lib/db/messages.php
lib/db/renamedclasses.php
lib/db/services.php
lib/db/upgrade.php
lib/ddl/database_manager.php
lib/deprecatedlib.php
lib/editor/atto/plugins/emoticon/lib.php
lib/editor/atto/plugins/title/yui/build/moodle-atto_title-button/moodle-atto_title-button-debug.js
lib/editor/atto/plugins/title/yui/build/moodle-atto_title-button/moodle-atto_title-button-min.js
lib/editor/atto/plugins/title/yui/build/moodle-atto_title-button/moodle-atto_title-button.js
lib/editor/atto/plugins/title/yui/src/button/js/button.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/autosave-io.js
lib/editor/tinymce/module.js
lib/editor/tinymce/plugins/moodleemoticon/dialog.php
lib/editor/tinymce/plugins/moodleemoticon/lib.php
lib/editor/tinymce/tiny_mce/3.5.11/tiny_mce_src.js
lib/filebrowser/file_info_context_coursecat.php
lib/filelib.php
lib/form/autocomplete.php
lib/form/course.php
lib/form/filemanager.php
lib/form/filepicker.js
lib/form/filepicker.php
lib/form/float.php [new file with mode: 0644]
lib/form/group.php
lib/form/listing.php
lib/form/select.php
lib/form/submit.php
lib/form/tags.php
lib/form/templatable_form_element.php
lib/form/templates/element-advcheckbox-inline.mustache
lib/form/templates/element-advcheckbox.mustache
lib/form/templates/element-autocomplete-inline.mustache
lib/form/templates/element-autocomplete.mustache
lib/form/templates/element-button-inline.mustache
lib/form/templates/element-button.mustache
lib/form/templates/element-checkbox-inline.mustache
lib/form/templates/element-checkbox.mustache
lib/form/templates/element-password.mustache
lib/form/templates/element-passwordunmask.mustache
lib/form/templates/element-radio-inline.mustache
lib/form/templates/element-radio.mustache
lib/form/templates/element-select-inline.mustache
lib/form/templates/element-select.mustache
lib/form/templates/element-selectgroups-inline.mustache
lib/form/templates/element-selectgroups.mustache
lib/form/templates/element-selectwithlink.mustache
lib/form/templates/element-submit-inline.mustache
lib/form/templates/element-submit.mustache
lib/form/templates/element-tags-inline.mustache
lib/form/templates/element-tags.mustache
lib/form/templates/element-template-inline.mustache
lib/form/templates/element-template.mustache
lib/form/templates/element-text-inline.mustache
lib/form/templates/element-text.mustache
lib/form/templates/element-textarea.mustache
lib/form/templates/element-url.mustache
lib/form/tests/autocomplete_test.php
lib/form/tests/float_test.php [new file with mode: 0644]
lib/formslib.php
lib/messagelib.php
lib/mlbackend/php/classes/processor.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/pear/HTML/QuickForm/element.php
lib/portfoliolib.php
lib/questionlib.php
lib/setup.php
lib/setuplib.php
lib/templates/permissionmanager_panelcontent.mustache
lib/testing/classes/util.php
lib/tests/behat/app_behat_runtime.js
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/component_test.php
lib/tests/exporter_test.php
lib/tests/filelib_test.php
lib/tests/lock_test.php
lib/tests/messagelib_test.php
lib/tests/moodlelib_test.php
lib/tests/outputcomponents_test.php
lib/tests/string_manager_standard_test.php
lib/tests/targets_test.php
lib/tests/time_splittings_test.php
lib/upgrade.txt
lib/weblib.php
lib/xhprof/readme_moodle.txt
lib/xhprof/xhprof_moodle.php
lib/yui/build/moodle-core-event/moodle-core-event-debug.js
lib/yui/build/moodle-core-event/moodle-core-event-min.js
lib/yui/build/moodle-core-event/moodle-core-event.js
lib/yui/src/event/js/event.js
login/lib.php
login/tests/lib_test.php
media/player/vimeo/wsplayer.php
message/amd/build/message_drawer.min.js
message/amd/build/message_drawer_router.min.js
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation_constants.min.js
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_conversation_renderer.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_repository.min.js
message/amd/src/message_drawer.js
message/amd/src/message_drawer_router.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_conversation_constants.js
message/amd/src/message_drawer_view_conversation_patcher.js
message/amd/src/message_drawer_view_conversation_renderer.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_repository.js
message/classes/api.php
message/classes/helper.php
message/classes/privacy/provider.php
message/classes/task/migrate_message_data.php
message/externallib.php
message/index.php
message/lib.php
message/output/airnotifier/tests/externallib_test.php
message/output/email/classes/event_observers.php [new file with mode: 0644]
message/output/email/classes/output/email/renderer.php [new file with mode: 0644]
message/output/email/classes/output/email/renderer_textemail.php [new file with mode: 0644]
message/output/email/classes/output/email_digest.php [new file with mode: 0644]
message/output/email/classes/output/renderer.php [new file with mode: 0644]
message/output/email/classes/privacy/provider.php
message/output/email/classes/task/send_email_task.php [new file with mode: 0644]
message/output/email/db/events.php [new file with mode: 0644]
message/output/email/db/install.xml [new file with mode: 0644]
message/output/email/db/tasks.php [new file with mode: 0644]
message/output/email/db/upgrade.php
message/output/email/lang/en/message_email.php
message/output/email/message_output_email.php
message/output/email/templates/email_digest_html.mustache [new file with mode: 0644]
message/output/email/templates/email_digest_text.mustache [new file with mode: 0644]
message/output/email/tests/event_observers_test.php [new file with mode: 0644]
message/output/email/tests/send_email_task_test.php [new file with mode: 0644]
message/output/email/version.php
message/output/popup/classes/api.php
message/output/popup/externallib.php
message/output/popup/tests/base.php
message/output/popup/tests/externallib_test.php
message/templates/message_drawer_contacts_list.mustache
message/templates/message_drawer_conversations_list.mustache
message/templates/message_drawer_icon_back.mustache
message/templates/message_drawer_messages_list.mustache
message/templates/message_drawer_non_contacts_list.mustache
message/templates/message_drawer_view_contact_body_content.mustache
message/templates/message_drawer_view_contacts_header.mustache
message/templates/message_drawer_view_conversation_body.mustache
message/templates/message_drawer_view_conversation_body_message.mustache
message/templates/message_drawer_view_conversation_header_content_type_private.mustache
message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache
message/templates/message_drawer_view_conversation_header_content_type_public.mustache
message/templates/message_drawer_view_conversation_header_content_type_self.mustache [new file with mode: 0644]
message/templates/message_drawer_view_conversation_header_edit_mode.mustache
message/templates/message_drawer_view_conversation_header_placeholder.mustache
message/templates/message_drawer_view_group_info_participants_list.mustache
message/templates/message_drawer_view_overview_body.mustache
message/templates/message_drawer_view_overview_header.mustache
message/templates/message_drawer_view_search_body.mustache
message/templates/message_drawer_view_search_header.mustache
message/templates/message_drawer_view_settings_header.mustache
message/templates/message_index.mustache
message/tests/api_test.php
message/tests/behat/behat_message.php
message/tests/behat/favourite_conversations.feature [new file with mode: 0644]
message/tests/behat/group_conversation.feature
message/tests/behat/message_delete_conversation.feature [new file with mode: 0644]
message/tests/behat/message_drawer_manage_contacts.feature
message/tests/behat/message_manage_preferences.feature [new file with mode: 0644]
message/tests/behat/unread_messages.feature [new file with mode: 0644]
message/tests/externallib_test.php
message/tests/helper_test.php
message/tests/messagelib_test.php
message/tests/privacy_provider_test.php
message/upgrade.txt
mnet/service/enrol/classes/privacy/provider.php
mod/assign/amd/build/grading_panel.min.js
mod/assign/amd/src/grading_panel.js
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/tests/locallib_test.php
mod/book/lib.php
mod/book/tests/lib_test.php
mod/book/tool/print/classes/output/print_book_page.php
mod/book/tool/print/classes/output/renderer.php
mod/book/upgrade.txt
mod/chat/lang/en/chat.php
mod/chat/tests/lib_test.php
mod/choice/lang/en/choice.php
mod/choice/mod_form.php
mod/data/classes/external/record_exporter.php
mod/data/lang/en/data.php
mod/data/mod_form.php
mod/data/tests/externallib_test.php
mod/data/upgrade.txt
mod/feedback/classes/completion.php
mod/feedback/classes/structure.php
mod/feedback/item/feedback_item_form_class.php
mod/feedback/item/multichoice/multichoice_form.php
mod/feedback/item/multichoicerated/multichoicerated_form.php
mod/feedback/lib.php
mod/feedback/tests/behat/question_types.feature
mod/feedback/tests/behat/question_types_non_anon.feature
mod/feedback/tests/external_test.php
mod/feedback/tests/lib_test.php
mod/feedback/tests/privacy_test.php
mod/forum/amd/build/discussion.min.js [new file with mode: 0644]
mod/forum/amd/build/discussion_list.min.js
mod/forum/amd/build/favourite_toggle.min.js [new file with mode: 0644]
mod/forum/amd/build/inpage_reply.min.js [new file with mode: 0644]
mod/forum/amd/build/lock_toggle.min.js [new file with mode: 0644]
mod/forum/amd/build/pin_toggle.min.js [new file with mode: 0644]
mod/forum/amd/build/posts_list.min.js [new file with mode: 0644]
mod/forum/amd/build/repository.min.js
mod/forum/amd/build/selectors.min.js
mod/forum/amd/src/discussion.js [new file with mode: 0644]
mod/forum/amd/src/discussion_list.js
mod/forum/amd/src/favourite_toggle.js [new file with mode: 0644]
mod/forum/amd/src/inpage_reply.js [new file with mode: 0644]
mod/forum/amd/src/lock_toggle.js [new file with mode: 0644]
mod/forum/amd/src/pin_toggle.js [new file with mode: 0644]
mod/forum/amd/src/posts_list.js [new file with mode: 0644]
mod/forum/amd/src/repository.js
mod/forum/amd/src/selectors.js
mod/forum/backup/moodle2/backup_forum_stepslib.php
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/classes/local/builders/exported_discussion.php [new file with mode: 0644]
mod/forum/classes/local/builders/exported_discussion_summaries.php
mod/forum/classes/local/builders/exported_posts.php
mod/forum/classes/local/container.php
mod/forum/classes/local/data_mappers/legacy/discussion.php
mod/forum/classes/local/data_mappers/legacy/forum.php
mod/forum/classes/local/entities/discussion.php
mod/forum/classes/local/entities/forum.php
mod/forum/classes/local/exporters/author.php
mod/forum/classes/local/exporters/discussion.php
mod/forum/classes/local/exporters/discussion_summaries.php
mod/forum/classes/local/exporters/discussion_summary.php
mod/forum/classes/local/exporters/forum.php
mod/forum/classes/local/exporters/post.php
mod/forum/classes/local/exporters/posts.php
mod/forum/classes/local/factories/builder.php
mod/forum/classes/local/factories/entity.php
mod/forum/classes/local/factories/exporter.php
mod/forum/classes/local/factories/legacy_data_mapper.php
mod/forum/classes/local/factories/renderer.php
mod/forum/classes/local/factories/url.php
mod/forum/classes/local/factories/vault.php
mod/forum/classes/local/managers/capability.php
mod/forum/classes/local/renderers/discussion.php
mod/forum/classes/local/renderers/discussion_list.php
mod/forum/classes/local/vaults/author.php
mod/forum/classes/local/vaults/db_table_vault.php
mod/forum/classes/local/vaults/discussion.php
mod/forum/classes/local/vaults/discussion_list.php
mod/forum/classes/local/vaults/forum.php
mod/forum/classes/local/vaults/post.php
mod/forum/classes/local/vaults/post_read_receipt_collection.php
mod/forum/classes/post_form.php
mod/forum/classes/privacy/provider.php
mod/forum/classes/subscriptions.php
mod/forum/classes/task/send_user_notifications.php
mod/forum/db/access.php
mod/forum/db/install.xml
mod/forum/db/messages.php
mod/forum/db/services.php
mod/forum/db/upgrade.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/locallib.php
mod/forum/mod_form.php
mod/forum/pix/t/star.png [new file with mode: 0644]
mod/forum/pix/t/star.svg [new file with mode: 0644]
mod/forum/post.php
mod/forum/styles.css
mod/forum/templates/discussion_favourite_toggle.mustache [new file with mode: 0644]
mod/forum/templates/discussion_list.mustache
mod/forum/templates/discussion_lock_toggle.mustache [moved from admin/tool/analytics/templates/evaluation_mode_selection.mustache with 50% similarity]
mod/forum/templates/discussion_pin_toggle.mustache [new file with mode: 0644]
mod/forum/templates/discussion_subscription_toggle.mustache
mod/forum/templates/forum_action_menu.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion.mustache
mod/forum/templates/forum_discussion_favourite_toggle.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion_post.mustache
mod/forum/templates/forum_discussion_threaded_post.mustache
mod/forum/templates/forum_discussion_threaded_posts.mustache
mod/forum/templates/inpage_reply.mustache [new file with mode: 0644]
mod/forum/tests/behat/add_forum_inline.feature [new file with mode: 0644]
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/tests/behat/discussion_display.feature
mod/forum/tests/behat/discussion_lock.feature [new file with mode: 0644]
mod/forum/tests/behat/edit_tags.feature
mod/forum/tests/behat/favourite_discussion.feature [new file with mode: 0644]
mod/forum/tests/behat/forum_subscriptions_default.feature
mod/forum/tests/behat/inpage_reply.feature [new file with mode: 0644]
mod/forum/tests/behat/posts_ordering_blog.feature
mod/forum/tests/behat/posts_ordering_general.feature
mod/forum/tests/behat/separate_group_discussions.feature
mod/forum/tests/behat/separate_group_single_group_discussions.feature
mod/forum/tests/behat/visible_group_discussions.feature
mod/forum/tests/entities_discussion_summary_test.php
mod/forum/tests/entities_discussion_test.php
mod/forum/tests/entities_forum_test.php
mod/forum/tests/exporters_author_test.php
mod/forum/tests/exporters_discussion_test.php
mod/forum/tests/exporters_forum_test.php
mod/forum/tests/exporters_post_test.php
mod/forum/tests/externallib_test.php
mod/forum/tests/generator/lib.php
mod/forum/tests/lib_test.php
mod/forum/tests/locallib_test.php [new file with mode: 0644]
mod/forum/tests/mail_test.php
mod/forum/tests/subscriptions_test.php
mod/forum/tests/vaults_author_test.php
mod/forum/tests/vaults_discussion_list_test.php
mod/forum/tests/vaults_post_test.php
mod/forum/upgrade.txt
mod/forum/version.php
mod/forum/view.php
mod/glossary/classes/external.php
mod/glossary/edit_form.php
mod/glossary/mod_form.php
mod/glossary/tests/external_test.php
mod/glossary/upgrade.txt
mod/lesson/backup/moodle2/backup_lesson_stepslib.php
mod/lesson/backup/moodle2/restore_lesson_stepslib.php
mod/lesson/classes/privacy/provider.php
mod/lesson/continue.php
mod/lesson/db/messages.php
mod/lesson/editpage.php
mod/lesson/essay.php
mod/lesson/index.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/mod_form.php
mod/lesson/pagetypes/essay.php
mod/lesson/pagetypes/numerical.php
mod/lesson/pagetypes/shortanswer.php
mod/lesson/report.php
mod/lesson/tests/behat/all_other_answers_catch.feature [new file with mode: 0644]
mod/lesson/tests/behat/wrong_answer_continue.feature [new file with mode: 0644]
mod/lesson/upgrade.txt
mod/lti/mod_form.php
mod/lti/templates/tool_proxy_card.mustache
mod/page/mod_form.php
mod/quiz/accessrule/accessrulebase.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/db/messages.php
mod/quiz/lang/en/quiz.php
mod/quiz/locallib.php
mod/quiz/mod_form.php
mod/quiz/tests/external_test.php
mod/resource/mod_form.php
mod/scorm/mod_form.php
mod/scorm/report/interactions/classes/report.php
mod/scorm/report/objectives/classes/report.php
mod/survey/tests/behat/survey_completion.feature
mod/url/mod_form.php
mod/wiki/classes/external.php
mod/wiki/tests/externallib_test.php
mod/wiki/upgrade.txt
mod/workshop/mod_form.php
pix/e/styleparagraph.png [new file with mode: 0644]
pix/e/styleparagraph.svg [new file with mode: 0644]
pix/t/emptystar.png [new file with mode: 0644]
pix/t/emptystar.svg [new file with mode: 0644]
privacy/tests/provider_test.php
question/behaviour/interactivecountback/behaviour.php
question/category.php
question/category_class.php
question/edit.php
question/engine/questionattemptstep.php
question/engine/tests/helpers.php
question/engine/upgrade/tests/helper.php
question/export.php
question/format.php
question/import.php
question/preview.php
question/previewlib.php
question/question.php
question/tests/events_test.php
question/type/calculated/datasetitems_form.php
question/type/calculated/questiontype.php
question/type/calculatedsimple/edit_calculatedsimple_form.php
question/type/calculatedsimple/questiontype.php
question/type/calculatedsimple/tests/questiontype_test.php
question/type/edit_question_form.php
question/type/multichoice/amd/build/clearchoice.min.js [new file with mode: 0644]
question/type/multichoice/amd/src/clearchoice.js [new file with mode: 0644]
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/question.php
question/type/multichoice/renderer.php
question/type/multichoice/tests/behat/preview.feature
question/type/multichoice/tests/question_multi_test.php
question/type/multichoice/tests/question_single_test.php
question/type/multichoice/tests/walkthrough_test.php
question/type/numerical/edit_numerical_form.php
question/type/numerical/questiontype.php
question/type/numerical/tests/behat/add.feature [new file with mode: 0644]
question/type/numerical/tests/behat/backup_and_restore.feature [new file with mode: 0644]
question/type/numerical/tests/behat/edit.feature [new file with mode: 0644]
question/type/numerical/tests/behat/export.feature [new file with mode: 0644]
question/type/numerical/tests/behat/import.feature [new file with mode: 0644]
question/type/numerical/tests/behat/preview.feature [new file with mode: 0644]
question/type/numerical/tests/fixtures/testquestion.moodle.xml [new file with mode: 0644]
question/type/questiontypebase.php
report/stats/classes/privacy/provider.php
repository/dropbox/lang/en/repository_dropbox.php
repository/filesystem/lang/en/repository_filesystem.php
repository/webdav/lib.php
search/classes/manager.php
tag/classes/external.php
tag/classes/external/tag_area_exporter.php [new file with mode: 0644]
tag/classes/external/tag_collection_exporter.php [new file with mode: 0644]
tag/classes/external/tag_item_exporter.php [new file with mode: 0644]
tag/classes/external/util.php [new file with mode: 0644]
tag/tests/external_test.php
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/pending.min.js [new file with mode: 0644]
theme/boost/amd/src/loader.js
theme/boost/amd/src/pending.js [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_position.scss
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/message.scss
theme/boost/scss/moodle/question.scss
theme/boost/style/moodle.css
theme/boost/templates/core_form/element-float-inline.mustache [new file with mode: 0644]
theme/boost/templates/core_form/element-float.mustache [new file with mode: 0644]
theme/boost/tests/behat/group_conversation.feature [deleted file]
theme/classic/lang/en/theme_classic.php
theme/classic/pix/screenshot.png
theme/classic/style/moodle.css
theme/classic/templates/core/footer.mustache [deleted file]
user/classes/analytics/target/upcoming_activities_due.php
user/lib.php
user/tests/behat/delete_users.feature
user/tests/behat/view_full_profile.feature
user/tests/userlib_test.php
version.php
webservice/lib.php
webservice/tests/lib_test.php

index 1e5837f..e9203ec 100644 (file)
@@ -710,7 +710,7 @@ if ($interactive) {
     cli_separator();
     cli_heading(get_string('cliadminemail', 'install'));
     $prompt = get_string('clitypevaluedefault', 'admin', $options['adminemail']);
-    $options['adminemail'] = cli_input($prompt);
+    $options['adminemail'] = cli_input($prompt, $options['adminemail']);
 }
 
 // Validate that the address provided was an e-mail address.
index 3bc74ec..922617a 100644 (file)
@@ -1062,7 +1062,7 @@ class core_admin_renderer extends plugin_renderer_base {
 
                 if ($isstandard = $plugin->is_standard()) {
                     $row->attributes['class'] .= ' standard';
-                    $sourcelabel = html_writer::span(get_string('sourcestd', 'core_plugin'), 'sourcetext label');
+                    $sourcelabel = html_writer::span(get_string('sourcestd', 'core_plugin'), 'sourcetext badge badge-secondary');
                 } else {
                     $row->attributes['class'] .= ' extension';
                     $sourcelabel = html_writer::span(get_string('sourceext', 'core_plugin'), 'sourcetext badge badge-info');
@@ -1074,7 +1074,7 @@ class core_admin_renderer extends plugin_renderer_base {
 
                 $statuscode = $plugin->get_status();
                 $row->attributes['class'] .= ' status-' . $statuscode;
-                $statusclass = 'statustext label ';
+                $statusclass = 'statustext badge ';
                 switch ($statuscode) {
                     case core_plugin_manager::PLUGIN_STATUS_NEW:
                         $statusclass .= $dependenciesok ? 'badge-success' : 'badge-warning';
@@ -2025,7 +2025,7 @@ class core_admin_renderer extends plugin_renderer_base {
                     $messagetype = 'ok';
                     $statusclass = 'badge-success';
                 }
-                $status = html_writer::span($status, 'label ' . $statusclass);
+                $status = html_writer::span($status, 'badge ' . $statusclass);
                 // Here we'll store all the feedback found
                 $feedbacktext = '';
                 // Append the feedback if there is some
index 5f43a52..5270745 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 if ($hassiteconfig) {
+
+    $settings = new admin_settingpage('analyticssite', new lang_string('analyticssiteinfo', 'analytics'));
+    $ADMIN->add('analytics', $settings);
+
+    if ($ADMIN->fulltree) {
+        $modeinstructions = [
+            'facetoface' => get_string('modeinstructionfacetoface', 'analytics'),
+            'blendedhybrid' => get_string('modeinstructionblendedhybrid', 'analytics'),
+            'fullyonline' => get_string('modeinstructionfullyonline', 'analytics'),
+        ];
+        $settings->add(new admin_setting_configmultiselect('analytics/modeinstruction', get_string('modeinstruction', 'analytics'),
+            '', [], $modeinstructions));
+
+        $settings->add(new admin_setting_configtext_with_maxlength('analytics/percentonline',
+            get_string('percentonline', 'analytics'),
+            get_string('percentonline_help', 'analytics'), '', PARAM_INT, 3, 3));
+
+        $typeinstitutions = [
+            'typeinstitutionacademic' => get_string('typeinstitutionacademic', 'analytics'),
+            'typeinstitutiontraining' => get_string('typeinstitutiontraining', 'analytics'),
+            'typeinstitutionngo' => get_string('typeinstitutionngo', 'analytics'),
+        ];
+        $settings->add(new admin_setting_configmultiselect('analytics/typeinstitution', get_string('typeinstitution', 'analytics'),
+            '', [], $typeinstitutions));
+
+        $levelinstitutions = [
+            'levelinstitutionisced0' => get_string('levelinstitutionisced0', 'analytics'),
+            'levelinstitutionisced1' => get_string('levelinstitutionisced1', 'analytics'),
+            'levelinstitutionisced2' => get_string('levelinstitutionisced2', 'analytics'),
+            'levelinstitutionisced3' => get_string('levelinstitutionisced3', 'analytics'),
+            'levelinstitutionisced4' => get_string('levelinstitutionisced4', 'analytics'),
+            'levelinstitutionisced5' => get_string('levelinstitutionisced5', 'analytics'),
+            'levelinstitutionisced6' => get_string('levelinstitutionisced6', 'analytics'),
+            'levelinstitutionisced7' => get_string('levelinstitutionisced7', 'analytics'),
+            'levelinstitutionisced8' => get_string('levelinstitutionisced8', 'analytics'),
+        ];
+        $settings->add(new admin_setting_configmultiselect('analytics/levelinstitution',
+            get_string('levelinstitution', 'analytics'), '', [], $levelinstitutions));
+    }
+
     $settings = new admin_settingpage('analyticssettings', new lang_string('analyticssettings', 'analytics'));
     $ADMIN->add('analytics', $settings);
 
     if ($ADMIN->fulltree) {
+
+
         // Select the site prediction's processor.
         $predictionprocessors = \core_analytics\manager::get_all_prediction_processors();
         $predictors = array();
@@ -75,7 +117,7 @@ if ($hassiteconfig) {
             $defaultreader, $options));
 
         // Enable/disable time splitting methods.
-        $alltimesplittings = \core_analytics\manager::get_all_time_splittings();
+        $alltimesplittings = \core_analytics\manager::get_time_splitting_methods_for_evaluation(true);
 
         $timesplittingoptions = array();
         $timesplittingdefaults = array('\core\analytics\time_splitting\quarters_accum',
index 61d82c3..58493dd 100644 (file)
@@ -59,6 +59,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configcheckbox('profilingallowme', new lang_string('profilingallowme', 'admin'), new lang_string('profilingallowme_help', 'admin'), false));
     // Allow PROFILEALL/PROFILEALLSTOP GPC.
     $temp->add(new admin_setting_configcheckbox('profilingallowall', new lang_string('profilingallowall', 'admin'), new lang_string('profilingallowall_help', 'admin'), false));
+    $temp->add(new admin_setting_configtext('profilingslow', new lang_string('profilingslow', 'admin'),
+        new lang_string('profilingslow_help', 'admin'), 0, PARAM_FLOAT));
     // TODO: Allow to skip PHP functions (XHPROF_FLAGS_NO_BUILTINS)
     // TODO: Allow to skip call_user functions (ignored_functions array)
     // Specify the life time (in minutes) of profiling runs.
index 3adbf0b..901a38a 100644 (file)
@@ -174,12 +174,11 @@ if ($hassiteconfig
         // Options include fields from the user table that might be helpful to
         // distinguish when adding or listing users ('I want to add the John
         // Smith from Science faculty').
-        // Username is not included as an option because in some sites, it might
-        // be a security problem to reveal usernames even to trusted staff.
         // Custom user profile fields are not currently supported.
         $temp->add(new admin_setting_configmulticheckbox('showuseridentity',
                 new lang_string('showuseridentity', 'admin'),
                 new lang_string('showuseridentity_desc', 'admin'), array('email' => 1), array(
+                    'username'    => new lang_string('username'),
                     'idnumber'    => new lang_string('idnumber'),
                     'email'       => new lang_string('email'),
                     'phone1'      => new lang_string('phone1'),
@@ -255,4 +254,4 @@ if ($hassiteconfig) {
         new lang_string('sitepolicyguest_help', 'core_admin'), (isset($CFG->sitepolicy) ? $CFG->sitepolicy : ''), PARAM_RAW));
 
     $ADMIN->add('privacy', $temp);
-}
\ No newline at end of file
+}
index 9e958db..fd1374c 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 8d35e86..7a33345 100644 (file)
@@ -101,50 +101,53 @@ define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_facto
         },
 
         /**
-         * Displays a select-evaluation-mode choice.
+         * Displays evaluation mode and time-splitting method choices.
          *
          * @param  {String}  actionId
          * @param  {Boolean} trainedOnlyExternally
          */
-        selectEvaluationMode: function(actionId, trainedOnlyExternally) {
+        selectEvaluationOptions: function(actionId, trainedOnlyExternally, timeSplittingMethods) {
             $('[data-action-id="' + actionId + '"]').on('click', function(ev) {
                 ev.preventDefault();
 
                 var a = $(ev.currentTarget);
 
-                if (!trainedOnlyExternally) {
-                    // We can not evaluate trained models if the model was trained using data from this site.
-                    // Default to evaluate the model configuration if that is the case.
-                    window.location.href = a.attr('href');
-                    return;
-                }
-
                 var stringsPromise = Str.get_strings([
                     {
                         key: 'evaluatemodel',
                         component: 'tool_analytics'
                     }, {
-                        key: 'evaluationmode',
+                        key: 'evaluate',
                         component: 'tool_analytics'
                     }
                 ]);
                 var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});
-                var bodyPromise = Templates.render('tool_analytics/evaluation_mode_selection', {});
+                var bodyPromise = Templates.render('tool_analytics/evaluation_options', {
+                    trainedexternally: trainedOnlyExternally,
+                    timesplittingmethods: timeSplittingMethods
+                });
 
                 $.when(stringsPromise, modalPromise).then(function(strings, modal) {
 
 
                     modal.getRoot().on(ModalEvents.hidden, modal.destroy.bind(modal));
 
-                    modal.setTitle(strings[1]);
-                    modal.setSaveButtonText(strings[0]);
+                    modal.setTitle(strings[0]);
+                    modal.setSaveButtonText(strings[1]);
                     modal.setBody(bodyPromise);
 
                     modal.getRoot().on(ModalEvents.save, function() {
+
+                        // Evaluation mode.
                         var evaluationMode = $("input[name='evaluationmode']:checked").val();
                         if (evaluationMode == 'trainedmodel') {
                             a.attr('href', a.attr('href') + '&mode=trainedmodel');
                         }
+
+                        // Selected time-splitting id.
+                        var timeSplittingMethod = $("#id-evaluation-timesplitting").val();
+                        a.attr('href', a.attr('href') + '&timesplitting=' + timeSplittingMethod);
+
                         window.location.href = a.attr('href');
                         return;
                     });
index c46e6fc..8736580 100644 (file)
@@ -48,7 +48,7 @@ class clihelper {
         foreach ($models as $model) {
             $modelid = $model->get_id();
             $isenabled = $model->is_enabled() ? get_string('enabled', 'tool_analytics') : get_string('disabled', 'tool_analytics');
-            $name = $model->get_target()->get_name();
+            $name = $model->get_name();
             echo str_pad($modelid, 15, ' ') . ' ' . str_pad($name, 50, ' ') . ' ' . str_pad($isenabled, 15, ' ') . "\n";
         }
     }
index 31d0f57..b529d50 100644 (file)
@@ -45,13 +45,14 @@ class edit_model extends \moodleform {
 
         $mform = $this->_form;
 
-        if ($this->_customdata['trainedmodel']) {
+        if ($this->_customdata['trainedmodel'] && $this->_customdata['staticmodel'] === false) {
             $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'));
 
+        // Target.
         if (!empty($this->_customdata['targets'])) {
             $targets = array('' => '');
             foreach ($this->_customdata['targets'] as $classname => $target) {
@@ -64,42 +65,48 @@ class edit_model extends \moodleform {
             $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);
-            $indicators[$optionname] = $indicator->get_name();
+        // Indicators.
+        if (!$this->_customdata['staticmodel']) {
+            $indicators = array();
+            foreach ($this->_customdata['indicators'] as $classname => $indicator) {
+                $optionname = \tool_analytics\output\helper::class_to_option($classname);
+                $indicators[$optionname] = $indicator->get_name();
+            }
+            $options = array(
+                'multiple' => true
+            );
+            $mform->addElement('autocomplete', 'indicators', get_string('indicators', 'tool_analytics'), $indicators, $options);
+            $mform->setType('indicators', PARAM_ALPHANUMEXT);
+            $mform->addHelpButton('indicators', 'indicators', 'tool_analytics');
         }
-        $options = array(
-            'multiple' => true
-        );
-        $mform->addElement('autocomplete', 'indicators', get_string('indicators', 'tool_analytics'), $indicators, $options);
-        $mform->setType('indicators', PARAM_ALPHANUMEXT);
-        $mform->addHelpButton('indicators', 'indicators', 'tool_analytics');
 
+        // Time-splitting methods.
         $timesplittings = array('' => '');
         foreach ($this->_customdata['timesplittings'] as $classname => $timesplitting) {
             $optionname = \tool_analytics\output\helper::class_to_option($classname);
             $timesplittings[$optionname] = $timesplitting->get_name();
         }
-
         $mform->addElement('select', 'timesplitting', get_string('timesplittingmethod', 'analytics'), $timesplittings);
         $mform->addHelpButton('timesplitting', 'timesplittingmethod', 'analytics');
 
-        $defaultprocessor = \core_analytics\manager::get_predictions_processor_name(
-            \core_analytics\manager::get_predictions_processor()
-        );
-        $predictionprocessors = ['' => get_string('defaultpredictoroption', 'analytics', $defaultprocessor)];
-        foreach ($this->_customdata['predictionprocessors'] as $classname => $predictionsprocessor) {
-            if ($predictionsprocessor->is_ready() !== true) {
-                continue;
+        // Predictions processor.
+        if (!$this->_customdata['staticmodel']) {
+            $defaultprocessor = \core_analytics\manager::get_predictions_processor_name(
+                \core_analytics\manager::get_predictions_processor()
+            );
+            $predictionprocessors = ['' => get_string('defaultpredictoroption', 'analytics', $defaultprocessor)];
+            foreach ($this->_customdata['predictionprocessors'] as $classname => $predictionsprocessor) {
+                if ($predictionsprocessor->is_ready() !== true) {
+                    continue;
+                }
+                $optionname = \tool_analytics\output\helper::class_to_option($classname);
+                $predictionprocessors[$optionname] = \core_analytics\manager::get_predictions_processor_name($predictionsprocessor);
             }
-            $optionname = \tool_analytics\output\helper::class_to_option($classname);
-            $predictionprocessors[$optionname] = \core_analytics\manager::get_predictions_processor_name($predictionsprocessor);
-        }
 
-        $mform->addElement('select', 'predictionsprocessor', get_string('predictionsprocessor', 'analytics'),
-            $predictionprocessors);
-        $mform->addHelpButton('predictionsprocessor', 'predictionsprocessor', 'analytics');
+            $mform->addElement('select', 'predictionsprocessor', get_string('predictionsprocessor', 'analytics'),
+                $predictionprocessors);
+            $mform->addHelpButton('predictionsprocessor', 'predictionsprocessor', 'analytics');
+        }
 
         if (!empty($this->_customdata['id'])) {
             $mform->addElement('hidden', 'id', $this->_customdata['id']);
@@ -130,13 +137,15 @@ class edit_model extends \moodleform {
             }
         }
 
-        if (empty($data['indicators'])) {
-            $errors['indicators'] = get_string('errornoindicators', 'analytics');
-        } else {
-            foreach ($data['indicators'] as $indicator) {
-                $realindicatorname = \tool_analytics\output\helper::option_to_class($indicator);
-                if (\core_analytics\manager::is_valid($realindicatorname, '\core_analytics\local\indicator\base') === false) {
-                    $errors['indicators'] = get_string('errorinvalidindicator', 'analytics', $realindicatorname);
+        if (!$this->_customdata['staticmodel']) {
+            if (empty($data['indicators'])) {
+                $errors['indicators'] = get_string('errornoindicators', 'analytics');
+            } else {
+                foreach ($data['indicators'] as $indicator) {
+                    $realindicatorname = \tool_analytics\output\helper::option_to_class($indicator);
+                    if (\core_analytics\manager::is_valid($realindicatorname, '\core_analytics\local\indicator\base') === false) {
+                        $errors['indicators'] = get_string('errorinvalidindicator', 'analytics', $realindicatorname);
+                    }
                 }
             }
         }
index 1b4c375..7130246 100644 (file)
@@ -26,6 +26,8 @@ namespace tool_analytics\output\form;
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once($CFG->libdir.'/formslib.php');
+
 /**
  * Model upload form.
  *
index 861d679..1db9981 100644 (file)
@@ -45,7 +45,7 @@ class helper {
         // Form field is PARAM_ALPHANUMEXT and we are sending fully qualified class names
         // as option names, but replacing the backslash for a string that is really unlikely
         // to ever be part of a class name.
-        return str_replace('\\', '2015102400ouuu', $class);
+        return str_replace('\\', '__', $class);
     }
 
     /**
@@ -56,7 +56,7 @@ class helper {
      */
     public static function option_to_class($option) {
         // Really unlikely but yeah, I'm a bad booyyy.
-        return str_replace('2015102400ouuu', '\\', $option);
+        return str_replace('__', '\\', $option);
     }
 
     /**
@@ -86,7 +86,7 @@ class helper {
         if ($analyticmodels = $PAGE->settingsnav->find('analyticmodels', \navigation_node::TYPE_SETTING)) {
             $PAGE->navbar->add($analyticmodels->get_content(), $analyticmodels->action());
         }
-        $PAGE->navbar->add($title, $url);
+        $PAGE->navbar->add($title);
 
         $PAGE->set_pagelayout('report');
         $PAGE->set_title($title);
index dbdaa00..fd205f1 100644 (file)
@@ -125,7 +125,7 @@ class invalid_analysables implements \renderable, \templatable {
 
         // Prepare the context object.
         $data = new \stdClass();
-        $data->modelname = $this->model->get_target()->get_name();
+        $data->modelname = $this->model->get_name();
 
         if ($this->page > 0) {
             $prev = clone $PAGE->url;
index 5152b6d..c6831d0 100644 (file)
@@ -62,8 +62,33 @@ 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');
+
+        $newmodelmenu = new \action_menu();
+        $newmodelmenu->set_menu_trigger(get_string('newmodel', 'tool_analytics'), 'btn btn-default');
+        $newmodelmenu->set_alignment(\action_menu::TL, \action_menu::BL);
+
+        $newmodelmenu->add(new \action_menu_link(
+            new \moodle_url('/admin/tool/analytics/createmodel.php'),
+            new \pix_icon('i/edit', ''),
+            get_string('createmodel', 'tool_analytics'),
+            false
+        ));
+
+        $newmodelmenu->add(new \action_menu_link(
+            new \moodle_url('/admin/tool/analytics/importmodel.php'),
+            new \pix_icon('i/import', ''),
+            get_string('importmodel', 'tool_analytics'),
+            false
+        ));
+
+        $newmodelmenu->add(new \action_menu_link(
+            new \moodle_url('/admin/tool/analytics/restoredefault.php'),
+            new \pix_icon('i/reload', ''),
+            get_string('restoredefault', 'tool_analytics'),
+            false
+        ));
+
+        $data->newmodelmenu = $newmodelmenu->export_for_template($output);
 
         $onlycli = get_config('analytics', 'onlycli');
         if ($onlycli === false) {
@@ -71,9 +96,20 @@ class models_list implements \renderable, \templatable {
             $onlycli = 1;
         }
 
+        // Evaluation options.
+        $timesplittingmethods = [
+            ['id' => 'all', 'text' => get_string('alltimesplittingmethods', 'tool_analytics')],
+        ];
+        foreach (\core_analytics\manager::get_time_splitting_methods_for_evaluation(true) as $timesplitting) {
+            $timesplittingmethods[] = [
+                'id' => \tool_analytics\output\helper::class_to_option($timesplitting->get_id()),
+                'text' => $timesplitting->get_name()->out(),
+            ];
+        }
+
         $data->models = array();
         foreach ($this->models as $model) {
-            $modeldata = $model->export();
+            $modeldata = $model->export($output);
 
             // Check if there is a help icon for the target to show.
             $identifier = $modeldata->target->get_identifier();
@@ -109,6 +145,8 @@ class models_list implements \renderable, \templatable {
                 $modeldata->indicators = $indicators;
             }
 
+            $modeldata->indicatorsnum = count($modeldata->indicators);
+
             // Check if there is a help icon for the time splitting method.
             if (!empty($modeldata->timesplitting)) {
                 $identifier = $modeldata->timesplitting->get_identifier();
@@ -192,7 +230,16 @@ class models_list implements \renderable, \templatable {
                 $trainedonlyexternally = !$model->trained_locally() && $model->is_trained();
 
                 $actionid = 'evaluate-' . $model->get_id();
-                $PAGE->requires->js_call_amd('tool_analytics/model', 'selectEvaluationMode', [$actionid, $trainedonlyexternally]);
+
+                $modeltimesplittingmethods = $timesplittingmethods;
+                // Include the current time-splitting method as the default selection method the model already have one.
+                if ($model->get_model_obj()->timesplitting) {
+                    $currenttimesplitting = ['id' => 'current', 'text' => get_string('currenttimesplitting', 'tool_analytics')];
+                    array_unshift($modeltimesplittingmethods, $currenttimesplitting);
+                }
+
+                $evaluateparams = [$actionid, $trainedonlyexternally, $modeltimesplittingmethods];
+                $PAGE->requires->js_call_amd('tool_analytics/model', 'selectEvaluationOptions', $evaluateparams);
                 $urlparams['action'] = 'evaluate';
                 $url = new \moodle_url('model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
@@ -210,12 +257,10 @@ class models_list implements \renderable, \templatable {
             }
 
             // Edit model.
-            if (!$model->is_static()) {
-                $urlparams['action'] = 'edit';
-                $url = new \moodle_url('model.php', $urlparams);
-                $icon = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit'));
-                $actionsmenu->add($icon);
-            }
+            $urlparams['action'] = 'edit';
+            $url = new \moodle_url('model.php', $urlparams);
+            $icon = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit'));
+            $actionsmenu->add($icon);
 
             // Enable / disable.
             if ($model->is_enabled() || !empty($modeldata->timesplitting)) {
@@ -280,6 +325,7 @@ class models_list implements \renderable, \templatable {
                 $actionsmenu->add($icon);
             }
 
+            // Delete model.
             $actionid = 'delete-' . $model->get_id();
             $PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'delete']);
             $urlparams['action'] = 'delete';
diff --git a/admin/tool/analytics/classes/output/restorable_models.php b/admin/tool/analytics/classes/output/restorable_models.php
new file mode 100644 (file)
index 0000000..a4e3456
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+// This file is part of Moodle - https://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/>.
+
+/**
+ * Provides {@link \tool_analytics\output\restorable_models} class.
+ *
+ * @package     tool_analytics
+ * @category    output
+ * @copyright   2019 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Represents the list of default models that can be eventually restored.
+ *
+ * @copyright 2019 David Mudrák <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restorable_models implements \renderable, \templatable {
+
+    /** @var array */
+    protected $models;
+
+    /**
+     * Instantiate an object of this class.
+     *
+     * @param array $models List of models as returned by {@link \core_analytics\manager::load_default_models_for_all_components()}
+     */
+    public function __construct(array $models) {
+
+        $this->models = $models;
+    }
+
+    /**
+     * Export the list of models to be rendered.
+     *
+     * @param renderer_base $output
+     * @return string
+     */
+    public function export_for_template(\renderer_base $output) {
+
+        $components = [];
+
+        foreach ($this->models as $componentname => $modelslist) {
+            $component = [
+                'name' => $this->component_name($componentname),
+                'component' => $componentname,
+                'models' => [],
+            ];
+
+            foreach ($modelslist as $definition) {
+                list($target, $indicators) = \core_analytics\manager::get_declared_target_and_indicators_instances($definition);
+
+                if (\core_analytics\model::exists($target, $indicators)) {
+                    continue;
+                }
+
+                $targetnamelangstring = $target->get_name();
+
+                $model = [
+                    'defid' => \core_analytics\manager::model_declaration_identifier($definition),
+                    'targetname' => $targetnamelangstring,
+                    'targetclass' => $definition['target'],
+                    'indicatorsnum' => count($definition['indicators']),
+                    'indicators' => [],
+                ];
+
+                if (get_string_manager()->string_exists($targetnamelangstring->get_identifier().'_help',
+                        $targetnamelangstring->get_component())) {
+                    $helpicon = new \help_icon($targetnamelangstring->get_identifier(), $targetnamelangstring->get_component());
+                    $model['targethelp'] = $helpicon->export_for_template($output);
+                }
+
+                foreach ($indicators as $indicator) {
+                    $indicatornamelangstring = $indicator->get_name();
+                    $indicatordata = [
+                        'name' => $indicatornamelangstring,
+                        'classname' => $indicator->get_id(),
+                    ];
+
+                    if (get_string_manager()->string_exists($indicatornamelangstring->get_identifier().'_help',
+                            $indicatornamelangstring->get_component())) {
+                        $helpicon = new \help_icon($indicatornamelangstring->get_identifier(),
+                            $indicatornamelangstring->get_component());
+                        $indicatordata['indicatorhelp'] = $helpicon->export_for_template($output);
+                    }
+
+                    $model['indicators'][] = $indicatordata;
+                }
+
+                $component['models'][] = $model;
+            }
+
+            if (!empty($component['models'])) {
+                $components[] = $component;
+            }
+        }
+
+        $result = [
+            'hasdata' => !empty($components),
+            'components' => array_values($components),
+            'submiturl' => new \moodle_url('/admin/tool/analytics/restoredefault.php'),
+            'backurl' => new \moodle_url('/admin/tool/analytics/index.php'),
+            'sesskey' => sesskey(),
+        ];
+
+        return $result;
+    }
+
+    /**
+     * Return a human readable name for the given frankenstyle component.
+     *
+     * @param string $component Frankenstyle component such as 'core', 'core_analytics' or 'mod_workshop'
+     * @return string Human readable name of the component
+     */
+    protected function component_name(string $component): string {
+
+        if ($component === 'core' || strpos($component, 'core_')) {
+            return get_string('componentcore', 'tool_analytics');
+
+        } else {
+            return get_string('pluginname', $component);
+        }
+    }
+}
index 91f3c85..6ea0d0d 100644 (file)
@@ -65,7 +65,7 @@ class predict_models extends \core\task\scheduled_task {
             \tool_analytics\output\helper::reset_page();
 
             if ($result) {
-                echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_target()->get_name()));
+                echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_name()));
                 $renderer = $PAGE->get_renderer('tool_analytics');
                 echo $renderer->render_get_predictions_results(false, array(), $result, $model->get_analyser()->get_logs());
             }
index 3c0c3c9..67c0a3a 100644 (file)
@@ -76,7 +76,7 @@ class train_models extends \core\task\scheduled_task {
             \tool_analytics\output\helper::reset_page();
 
             if ($result) {
-                echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_target()->get_name()));
+                echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_name()));
 
                 $renderer = $PAGE->get_renderer('tool_analytics');
                 echo $renderer->render_get_predictions_results($result, $model->get_analyser()->get_logs());
index e0c8974..f8a8aa5 100644 (file)
@@ -204,9 +204,9 @@ function tool_analytics_calculate_course_dates($course, $options) {
 
                 $updateit = false;
                 if ($course->enddate < $course->startdate) {
-                    $notification .= PHP_EOL . '  ' . get_string('errorendbeforestart', 'analytics', userdate($course->enddate));
+                    $notification .= PHP_EOL . '  ' . get_string('errorendbeforestart', 'course', userdate($course->enddate));
                 } else if ($course->startdate + (YEARSECS + (WEEKSECS * 4)) > $course->enddate) {
-                    $notification .= PHP_EOL . '  ' . get_string('coursetoolong', 'analytics');
+                    $notification .= PHP_EOL . '  ' . get_string('coursetoolong', 'course');
                 } else {
                     $notification .= PHP_EOL . '  ' . get_string('enddate') . ': ' . userdate($course->enddate);
                     $updateit = true;
index bf1701c..e5b9b92 100644 (file)
@@ -40,9 +40,10 @@ $targets = array_filter(\core_analytics\manager::get_all_targets(), function($ta
 
 $customdata = array(
     'trainedmodel' => false,
+    'staticmodel' => false,
     'targets' => $targets,
     'indicators' => \core_analytics\manager::get_all_indicators(),
-    'timesplittings' => \core_analytics\manager::get_enabled_time_splitting_methods(),
+    'timesplittings' => \core_analytics\manager::get_all_time_splittings(),
     'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors(),
 );
 $mform = new \tool_analytics\output\form\edit_model(null, $customdata);
index b3a8ba4..9e7f30d 100644 (file)
@@ -24,6 +24,7 @@
 
 $string['accuracy'] = 'Accuracy';
 $string['allpredictions'] = 'All predictions';
+$string['alltimesplittingmethods'] = 'All time-splitting methods';
 $string['analysingsitedata'] = 'Analysing the site';
 $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.';
@@ -35,9 +36,14 @@ $string['clearmodelpredictions'] = 'Are you sure you want to clear all "{$a}" pr
 $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['component'] = 'Component';
+$string['componentcore'] = 'Core';
+$string['componentselect'] = 'Select all models provided by the component \'{$a}\'';
+$string['componentselectnone'] = 'Unselect all';
 $string['createmodel'] = 'Create model';
+$string['currenttimesplitting'] = 'Current time-splitting method';
 $string['delete'] = 'Delete';
-$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes can not be reverted.';
+$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes cannot 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.';
@@ -46,7 +52,6 @@ $string['errorcantenablenotimesplitting'] = 'You need to select a time-splitting
 $string['errornoenabledandtrainedmodels'] = 'There are no enabled and trained models to predict.';
 $string['errornoenabledmodels'] = 'There are no enabled models to train.';
 $string['errornoexport'] = 'Only trained models can be exported';
-$string['errornostaticedit'] = 'Models based on assumptions cannot be edited.';
 $string['errornostaticevaluated'] = 'Models based on assumptions cannot be evaluated. They are always 100% correct according to how they were defined.';
 $string['errornostaticlog'] = 'Models based on assumptions cannot be evaluated because there is no performance log.';
 $string['erroronlycli'] = 'Execution only allowed via command line';
@@ -80,6 +85,7 @@ $string['importmodel'] = 'Import model';
 $string['indicators'] = 'Indicators';
 $string['indicators_help'] = 'The indicators are what you think will lead to an accurate prediction of the target.';
 $string['indicators_link'] = 'Indicators';
+$string['indicatorsnum'] = 'Number of indicators: {$a}';
 $string['info'] = 'Info';
 $string['ignoreversionmismatches'] = 'Ignore version mismatches';
 $string['ignoreversionmismatchescheckbox'] = 'Ignore the differences between this site version and the original site version.';
@@ -92,11 +98,13 @@ $string['invalidindicatorsremoved'] = 'A new model has been added. Indicators th
 $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['missingmoodleversion'] = 'Imported file doesn\'t define a version number';
 $string['modelid'] = 'Model ID';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
+$string['modelname'] = 'Model name';
 $string['modelresults'] = '{$a} results';
 $string['modeltimesplitting'] = 'Time splitting';
+$string['newmodel'] = 'New model';
 $string['nextpage'] = 'Next page';
 $string['nodatatoevaluate'] = 'There is no data to evaluate the model';
 $string['nodatatopredict'] = 'No new elements to get predictions for';
@@ -109,8 +117,15 @@ $string['predictmodels'] = 'Predict models';
 $string['predictorresultsin'] = 'Predictor logged information in {$a} directory';
 $string['predictionprocessfinished'] = 'Prediction process finished';
 $string['previouspage'] = 'Previous page';
+$string['restoredefault'] = 'Restore default models';
+$string['restoredefaultempty'] = 'Please select models to be restored.';
+$string['restoredefaultinfo'] = 'These default models are missing or have changed since being installed. You can restore selected default models.';
+$string['restoredefaultnone'] = 'All default models provided by core and installed plugins have been created. No new models were found; there is nothing to restore.';
+$string['restoredefaultsome'] = 'Succesfully re-created {$a->count} new model(s).';
+$string['restoredefaultsubmit'] = 'Restore selected';
 $string['samestartdate'] = 'Current start date is good';
 $string['sameenddate'] = 'Current end date is good';
+$string['selecttimesplittingforevaluation'] = 'Select the time-splitting method you want to use to evaluate the model configuration.';
 $string['target'] = 'Target';
 $string['target_help'] = 'The target is what the model will predict.';
 $string['target_link'] = 'Targets';
@@ -120,7 +135,7 @@ $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['versionnotsame'] = 'Imported file was from a different version ({$a->importedversion}) than the current one ({$a->version})';
 $string['viewlog'] = 'Evaluation 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 ab2fe61..c6cc950 100644 (file)
@@ -40,7 +40,7 @@ $url = new \moodle_url('/admin/tool/analytics/model.php', $params);
 switch ($action) {
 
     case 'edit':
-        $title = get_string('editmodel', 'tool_analytics', $model->get_target()->get_name());
+        $title = get_string('editmodel', 'tool_analytics', $model->get_name());
         break;
     case 'evaluate':
         $title = get_string('evaluatemodel', 'tool_analytics');
@@ -110,14 +110,10 @@ switch ($action) {
     case 'edit':
         confirm_sesskey();
 
-        if ($model->is_static()) {
-            echo $OUTPUT->header();
-            throw new moodle_exception('errornostaticedit', 'tool_analytics');
-        }
-
         $customdata = array(
             'id' => $model->get_id(),
             'trainedmodel' => $model->is_trained(),
+            'staticmodel' => $model->is_static(),
             'indicators' => $model->get_potential_indicators(),
             'timesplittings' => \core_analytics\manager::get_all_time_splittings(),
             'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors()
@@ -129,14 +125,22 @@ switch ($action) {
 
         } else if ($data = $mform->get_data()) {
 
-            // Converting option names to class names.
-            $indicators = array();
-            foreach ($data->indicators as $indicator) {
-                $indicatorclass = \tool_analytics\output\helper::option_to_class($indicator);
-                $indicators[] = \core_analytics\manager::get_indicator($indicatorclass);
-            }
             $timesplitting = \tool_analytics\output\helper::option_to_class($data->timesplitting);
-            $predictionsprocessor = \tool_analytics\output\helper::option_to_class($data->predictionsprocessor);
+
+            if (!$model->is_static()) {
+                // Converting option names to class names.
+                $indicators = array();
+                foreach ($data->indicators as $indicator) {
+                    $indicatorclass = \tool_analytics\output\helper::option_to_class($indicator);
+                    $indicators[] = \core_analytics\manager::get_indicator($indicatorclass);
+                }
+                $predictionsprocessor = \tool_analytics\output\helper::option_to_class($data->predictionsprocessor);
+            } else {
+                // These fields can not be modified.
+                $indicators = false;
+                $predictionsprocessor = false;
+            }
+
             $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor);
             redirect($returnurl);
         }
@@ -172,7 +176,19 @@ switch ($action) {
         $mode = optional_param('mode', false, PARAM_ALPHANUM);
         if ($mode == 'trainedmodel') {
             $options['mode'] = 'trainedmodel';
+        } else {
+
+            // All is the default in core_analytics\model::evaluate() as well.
+            $timesplitting = optional_param('timesplitting', 'all', PARAM_ALPHANUMEXT);
+            if ($timesplitting === 'current') {
+                $options['timesplitting'] = \core_analytics\manager::get_time_splitting($model->get_model_obj()->timesplitting);
+            } else if ($timesplitting !== 'all') {
+                $options['timesplitting'] = \core_analytics\manager::get_time_splitting(
+                    \tool_analytics\output\helper::option_to_class($timesplitting)
+                );
+            }
         }
+
         $results = $model->evaluate($options);
 
         // We reset the theme and the output as some indicators may be using external functions
diff --git a/admin/tool/analytics/restoredefault.php b/admin/tool/analytics/restoredefault.php
new file mode 100644 (file)
index 0000000..28b1b02
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+// This file is part of Moodle - https://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/>.
+
+/**
+ * Check and create missing default prediction models.
+ *
+ * @package     tool_analytics
+ * @copyright   2019 David Mudrák <david@moodle.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();
+
+$confirmed = optional_param('confirmed', false, PARAM_BOOL);
+$restoreids = optional_param_array('restoreid', [], PARAM_ALPHANUM);
+
+$returnurl = new \moodle_url('/admin/tool/analytics/index.php');
+$myurl = new \moodle_url('/admin/tool/analytics/restoredefault.php');
+
+\tool_analytics\output\helper::set_navbar(get_string('restoredefault', 'tool_analytics'), $myurl);
+
+if (data_submitted()) {
+    require_sesskey();
+
+    if (empty($restoreids)) {
+        $message = get_string('restoredefaultempty', 'tool_analytics');
+        $type = \core\output\notification::NOTIFY_WARNING;
+        redirect($myurl, $message, null, $type);
+    }
+
+    $numcreated = 0;
+
+    foreach (\core_analytics\manager::load_default_models_for_all_components() as $componentname => $modelslist) {
+        foreach ($modelslist as $definition) {
+            if (!in_array(\core_analytics\manager::model_declaration_identifier($definition), $restoreids)) {
+                // This model has not been selected by the user.
+                continue;
+            }
+
+            list($target, $indicators) = \core_analytics\manager::get_declared_target_and_indicators_instances($definition);
+
+            if (\core_analytics\model::exists($target, $indicators)) {
+                // This model exists (normally this should not happen as we do not show such models in the UI to select).
+                continue;
+            }
+
+            \core_analytics\manager::create_declared_model($definition);
+            $numcreated++;
+        }
+    }
+
+    $message = get_string('restoredefaultsome', 'tool_analytics', ['count' => $numcreated]);
+    $type = \core\output\notification::NOTIFY_SUCCESS;
+
+    redirect($returnurl, $message, null, $type);
+}
+
+$models = \core_analytics\manager::load_default_models_for_all_components();
+$ui = new \tool_analytics\output\restorable_models($models);
+
+echo $OUTPUT->header();
+echo $PAGE->get_renderer('tool_analytics')->render($ui);
+echo $OUTPUT->footer();
diff --git a/admin/tool/analytics/templates/evaluation_options.mustache b/admin/tool/analytics/templates/evaluation_options.mustache
new file mode 100644 (file)
index 0000000..9d77c13
--- /dev/null
@@ -0,0 +1,82 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_analytics/evaluation_options
+
+    Evaluation selector.
+
+    The purpose of this template is to render the evaluation mode options.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+        "trainedexternally": "1",
+        "timesplittingmethods": [
+            {
+                "id": "ou",
+                "name": "Quarters"
+            }, {
+                "id": "yeah",
+                "name": "Tenths"
+            }
+        ]
+    }
+}}
+
+{{#trainedexternally}}
+    <div class="box mb-4">{{#str}} evaluationmodeinfo, tool_analytics {{/str}}</div>
+
+    <div class="custom-control custom-radio">
+        <input class="custom-control-input" type="radio" name="evaluationmode" id="id-mode-trainedmodel" value="trainedmodel" checked>
+        <label class="custom-control-label" for="id-mode-trainedmodel">{{#str}} evaluationmodetrainedmodel, tool_analytics {{/str}}</label>
+    </div>
+
+    <div class="custom-control custom-radio">
+        <input class="custom-control-input" type="radio" name="evaluationmode" id="id-mode-configuration" value="configuration">
+        <label class="custom-control-label" for="id-mode-configuration">{{#str}} evaluationmodeconfiguration, tool_analytics {{/str}}</label>
+    </div>
+{{/trainedexternally}}
+
+{{! Hidden by default if #trainedexternally as the default option is trainedmodel in this case.}}
+<div id="id-evaluation-timesplitting-container" class="m-t-1 {{#trainedexternally}}hidden{{/trainedexternally}}">
+    {{#str}} selecttimesplittingforevaluation, tool_analytics {{/str}}
+    <div>
+        <select id="id-evaluation-timesplitting" name="timesplitting" class="custom-select m-t-1">
+            {{#timesplittingmethods}}
+                <option value="{{id}}">{{text}}</option>
+            {{/timesplittingmethods}}
+        </select>
+    </div>
+</div>
+
+
+{{#js}}
+    require(['jquery'], function($) {
+        $("input[name='evaluationmode']:radio").change(function() {
+            if ($(this).val() == 'configuration') {
+                $('#id-evaluation-timesplitting-container').show();
+            } else {
+                $('#id-evaluation-timesplitting-container').hide();
+            }
+        });
+    });
+{{/js}}
\ No newline at end of file
index b49c9e2..39d8e6b 100644 (file)
     Template for models list.
 
     Classes required for JS:
-    * none
+    * The list od models wrapped within a id="predictionmodelslist" element.
 
     Data attributes required for JS:
-    * none
+    * [data-widget="toggle"] indicates the clickable element for expanding/collapsing
+      the list of indicators used by the given model.
 
     Context variables required for this template:
-    * none
+    * models: array - list of models to display
+        - id: int - model unique identifier
+        - name: object - data for the inplace editable element template
+        - target: string - name of the target associated with the model
+        - targetclass: string - fully qualified name of the target class
+        - targethelp: object - data for the help tooltip template
+        - enabled: bool - is the model enabled
+        - indicatorsnum: int - number of indicators
+        - indicators: array - list of indicators used by the model
+            + name: string - name of the indicator
+            + help: object - data for the help tooltip template
+        - insights: object - data for the single select template
+        - noinsights: string - text to display instead of insights
+    * warnings: array - list of data for notification warning template
+    * infos: array - list of data for notification info template
+    * createmodelurl: string - URL to create a new model
+    * importmodelurl: string - URL to import a model
 
     Example context (json):
     {
         "models": [
             {
+                "id": 11,
+                "name": {
+                    "component": "local_analyticsdemo",
+                    "itemtype": "modelname",
+                    "itemid": 42,
+                    "displayvalue": "Prevent devs at risk",
+                    "value": ""
+                },
                 "target": "Prevent devs at risk",
-                "targethelp": [
-                    {
-                        "title": "Help with something",
-                        "url": "http://example.org/help",
-                        "linktext": "",
-                        "icon":{
-                            "extraclasses": "iconhelp",
-                            "attributes": [
-                                {"name": "src", "value": "../../../pix/help.svg"},
-                                {"name": "alt", "value": "Help icon"}
-                            ]
-                        }
+                "targetclass": "\\local_analyticsdemo\\analytics\\target\\dev_risk",
+                "targethelp": {
+                    "title": "Help with Prevent devs at risk",
+                    "text": "This target blah blah ...",
+                    "url": "http://example.org/help",
+                    "linktext": "",
+                    "icon": {
+                        "extraclasses": "iconhelp",
+                        "attributes": [
+                            {"name": "src", "value": "../../../pix/help.svg"},
+                            {"name": "alt", "value": "Help icon"}
+                        ]
                     }
-                ],
+                },
                 "enabled": 1,
-                "indicators": [{
-                    "name": "Indicator 1",
-                    "help": [{
-                            "title": "Help with something",
+                "indicatorsnum": 2,
+                "indicators": [
+                    {
+                        "name": "Indicator 1",
+                        "help": {
+                            "text": "This indicator blah blah ...",
+                            "title": "Help with Indicator 1",
                             "url": "http://example.org/help",
                             "linktext": "",
-                            "icon":{
+                            "icon": {
                                 "extraclasses": "iconhelp",
                                 "attributes": [
                                     {"name": "src", "value": "../../../pix/help.svg"},
                                     {"name": "alt", "value": "Help icon"}
                                 ]
                             }
-                        }]
+                        }
                     },
                     {
-                    "name": "Indicator 2",
-                    "help": [{
-                            "title": "Help with something",
+                        "name": "Indicator 2",
+                        "help": {
+                            "text": "This indicator blah blah ...",
+                            "title": "Help with Indicator 2",
                             "url": "http://example.org/help",
                             "linktext": "",
-                            "icon":{
+                            "icon": {
                                 "extraclasses": "iconhelp",
                                 "attributes": [
                                     {"name": "src", "value": "../../../pix/help.svg"},
                                     {"name": "alt", "value": "Help icon"}
                                 ]
                             }
-                        }]
-                    }],
-                "timesplitting": "Quarters",
-                "timesplittinghelp": [
-                    {
-                        "title": "Help with something",
-                        "url": "http://example.org/help",
-                        "linktext": "",
-                        "icon":{
-                            "extraclasses": "iconhelp",
-                            "attributes": [
-                                {"name": "src", "value": "../../../pix/help.svg"},
-                                {"name": "alt", "value": "Help icon"}
-                            ]
                         }
                     }
                 ],
+                "timesplitting": "Quarters",
+                "timesplittinghelp": {
+                    "text": "This time splitting methof blah blah ...",
+                    "title": "Help with Quarters",
+                    "url": "http://example.org/help",
+                    "linktext": "",
+                    "icon": {
+                        "extraclasses": "iconhelp",
+                        "attributes": [
+                            {"name": "src", "value": "../../../pix/help.svg"},
+                            {"name": "alt", "value": "Help icon"}
+                        ]
+                    }
+                },
                 "noinsights": "No insights available yet"
             }
         ],
-        "warnings": {
-            "message": "Hey, this is a warning"
-        }
+        "warnings": [
+            {
+                "message": "Be ware, this is just an example!"
+            }
+        ],
+        "createmodelurl": "#",
+        "importmodelurl": "#"
     }
 }}
 
 
 <div class="box">
     <div class="top-nav d-flex">
-        <a href="{{createmodelurl}}" class="btn btn-secondary mr-2">{{#str}}createmodel, tool_analytics{{/str}}</a>
-        <a href="{{importmodelurl}}" class="btn btn-secondary">{{#str}}importmodel, tool_analytics{{/str}}</a>
+        {{#newmodelmenu}}
+        {{>core/action_menu}}
+        {{/newmodelmenu}}
     </div>
-    <table class="generaltable fullwidth">
+    <table id="predictionmodelslist" class="generaltable fullwidth">
         <caption>{{#str}}analyticmodels, tool_analytics{{/str}}</caption>
         <thead>
             <tr>
-                <th scope="col">{{#str}}target, tool_analytics{{/str}}</th>
+                <th scope="col">{{#str}}modelname, tool_analytics{{/str}}</th>
                 <th scope="col">{{#str}}enabled, tool_analytics{{/str}}</th>
                 <th scope="col">{{#str}}indicators, tool_analytics{{/str}}</th>
                 <th scope="col">{{#str}}modeltimesplitting, tool_analytics{{/str}}</th>
         {{#models}}
             <tr>
                 <td>
-                    <span class="target-name">{{target}}</span>
-                    {{#targethelp}}
-                        {{>core/help_icon}}
-                    {{/targethelp}}
+                    {{#name}}
+                        <span class="model-name">{{>core/inplace_editable}}</span>
+                    {{/name}}
+                    <div>
+                        <small class="target-class">{{targetclass}}</small>
+                        {{#targethelp}}
+                            {{>core/help_icon}}
+                        {{/targethelp}}
+                    </div>
                 </td>
                 <td>
                     {{#enabled}}
                     {{/enabled}}
                 </td>
                 <td>
-                    <ul>
+                    <a data-widget="toggle"
+                           title="{{#str}} clicktohideshow {{/str}}"
+                           aria-expanded="false"
+                           aria-controls="indicators-{{id}}"
+                           role="button"
+                           href="">
+                        {{#str}} indicatorsnum, tool_analytics, {{indicatorsnum}} {{/str}}
+                    </a>
+                    <ul class="hidden" id="indicators-{{id}}">
                     {{#indicators}}
                         <li>
                             {{name}}
         </tbody>
     </table>
 </div>
+{{#js}}
+require(['jquery'], function($) {
+
+    // Toggle the visibility of the indicators list.
+    $('#predictionmodelslist').on('click', '[data-widget="toggle"]', function(e) {
+        e.preventDefault();
+        var toggle = $(e.currentTarget);
+        var listid = toggle.attr('aria-controls');
+
+        $(document.getElementById(listid)).toggle();
+
+        if (toggle.attr('aria-expanded') == 'false') {
+            toggle.attr('aria-expanded', 'true');
+        } else {
+            toggle.attr('aria-expanded', 'false');
+        }
+    });
+});
+{{/js}}
diff --git a/admin/tool/analytics/templates/restorable_models.mustache b/admin/tool/analytics/templates/restorable_models.mustache
new file mode 100644 (file)
index 0000000..da85f7b
--- /dev/null
@@ -0,0 +1,225 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_analytics/restorable_models
+
+    Displays the list of missing prediction models that can be restored.
+
+    Classes required for JS:
+    * The list should be wrapped within a id="restorablemodelslist" element.
+
+    Data attributes required for JS:
+    * [data-widget="toggle"] indicates the clickable element for expanding/collapsing
+      the list of indicators used by the given model.
+    * [data-select] indicates a clickable element used for selecting multiple checkboxes.
+    * [data-component] should be set for checkboxes that select the particular model.
+
+    Context variables required for this template:
+    * hasdata: boolean - do we have data to display
+    * submiturl: string - URL where the form should be submitted
+    * backurl: string - URL where the user should be sent without making any changes
+    * sesskey: string
+    * components: array - list of components to display
+        - name: string - human readable name of the component
+        - component: string - frankenstyle name of the component
+        - models: array - list of restorable models provided by the component
+            + defid: string - model definition identifier
+            + targetname: string - human readable name of the target
+            + targetclass: string - fully qualified classname of the target
+            + indicatorsnum: int - number of indicators
+            + indicators: array - list of indicators
+                ~ name: string - human readable name of the indicator
+                ~ classname: string - fully qualified classname of the indicator
+
+    Example context (json):
+    {
+        "hasdata": true,
+        "submiturl": "https://example.com/moodle/admin/tool/analytics/restoredefault.php",
+        "backurl": "https://example.com/moodle/admin/tool/analytics/index.php",
+        "sesskey": "abcdefg123456",
+        "components": [
+            {
+                "name": "Core",
+                "component": "core",
+                "models": [
+                    {
+                        "defid": "id24680aceg",
+                        "targetname": "No teaching",
+                        "targetclass": "\\core\\analytics\\target\\no_teaching",
+                        "indicatorsnum": 2,
+                        "indicators": [
+                            {
+                                "name": "There are no teachers",
+                                "classname": "\\core\\analytics\\indicator\\no_teacher"
+                            },
+                            {
+                                "name": "There are no students",
+                                "classname": "\\core\\analytics\\indicator\\no_students"
+                            }
+                        ]
+                    },
+                    {
+                        "defid": "id13579bdfi",
+                        "targetname": "Students at risk of dropping out",
+                        "targetclass": "\\core\\analytics\\target\\course_dropout",
+                        "indicatorsnum": 1,
+                        "indicators": [
+                            {
+                                "name": "Read actions amount",
+                                "classname": "\\core\\analytics\\indicator\\read_actions"
+                            }
+                        ]
+                    }
+                ]
+            },
+            {
+                "name": "Custom analytics plugin",
+                "component": "tool_customanalytics",
+                "models": [
+                    {
+                        "defid": "id566dsgffg655",
+                        "targetname": "Cheater",
+                        "targetclass": "\\tool_customanalytics\\analytics\\target\\cheater",
+                        "indicatorsnum": 1,
+                        "indicators": [
+                            {
+                                "name": "Copy-pasted submissions",
+                                "classname": "\\tool_customanalytics\\analytics\\indicator\\copy_paster_submissions"
+                            }
+                        ]
+                    }
+                ]
+            }
+        ]
+    }
+}}
+<div class="box">
+    {{^hasdata}}
+    <p>{{#str}} restoredefaultnone, tool_analytics {{/str}}</p>
+    <div><a href="{{backurl}}" class="btn btn-secondary">{{#str}} back {{/str}}</a></div>
+    {{/hasdata}}
+
+    {{#hasdata}}
+    <p>{{#str}} restoredefaultinfo, tool_analytics {{/str}}</p>
+    <form method="post" action="{{submiturl}}">
+        <table id="restorablemodelslist" class="generaltable fullwidth">
+            <colgroup>
+                <col width="10%">
+                <col width="45%">
+                <col width="45%">
+            </colgroup>
+            <thead>
+                <tr>
+                    <th scope="col"><a href="" data-select="*">{{#str}} selectall {{/str}}</a></th>
+                    <th scope="col">{{#str}} target, tool_analytics {{/str}}</th>
+                    <th scope="col">{{#str}} indicators, tool_analytics {{/str}}</th>
+                </tr>
+            </thead>
+            <tbody>
+            {{#components}}
+                <tr>
+                    <th scope="rowgroup" colspan="3">
+                        <span class="component-name">
+                            <a href=""
+                                    title="{{#str}} componentselect, tool_analytics, {{name}} {{/str}}"
+                                    data-select="{{component}}">
+                                {{name}}
+                            </a>
+                        </span>
+                        <div><small class="component-frankenstyle">{{component}}</small></div>
+                    </th>
+                </tr>
+                {{#models}}
+                <tr>
+                    <td>
+                        <input data-component="{{component}}" type="checkbox" name="restoreid[]" value="{{defid}}">
+                    </td>
+                    <td>
+                        <span class="target-name">{{targetname}}</span>
+                        {{#targethelp}}
+                            {{>core/help_icon}}
+                        {{/targethelp}}
+                        <div><small class="target-class">{{targetclass}}</small></div>
+                    </td>
+                    <td>
+                        <a data-widget="toggle"
+                                title="{{#str}} clicktohideshow {{/str}}"
+                                aria-expanded="false"
+                                aria-controls="indicators-{{defid}}"
+                                role="button"
+                                href="">
+                            {{#str}} indicatorsnum, tool_analytics, {{indicatorsnum}} {{/str}}
+                        </a>
+                        <ul class="hidden listunstyled" id="indicators-{{defid}}">
+                        {{#indicators}}
+                            <li>
+                                {{name}}
+                                {{#indicatorhelp}}
+                                    {{>core/help_icon}}
+                                {{/indicatorhelp}}
+                                <div><small>{{classname}}</small></div>
+                            </li>
+                        {{/indicators}}
+                        </ul>
+                    </td>
+                </tr>
+                {{/models}}
+            {{/components}}
+            </tbody>
+        </table>
+        <div>
+            <input class="btn btn-primary" type="submit" value="{{#str}} restoredefaultsubmit, tool_analytics {{/str}}">
+            <input class="btn btn-secondary" type="reset" value="{{#str}} componentselectnone, tool_analytics {{/str}}">
+            <a href="{{backurl}}" class="btn btn-secondary">{{#str}} back {{/str}}</a>
+            <input type="hidden" name="sesskey" value="{{sesskey}}">
+        </div>
+    </form>
+    {{/hasdata}}
+</div>
+
+{{#js}}
+require(['jquery'], function($) {
+
+    // Toggle the visibility of the indicators list.
+    $('#restorablemodelslist').on('click', '[data-widget="toggle"]', function(e) {
+        e.preventDefault();
+        var toggle = $(e.currentTarget);
+        var listid = toggle.attr('aria-controls');
+
+        $(document.getElementById(listid)).toggle();
+
+        if (toggle.attr('aria-expanded') == 'false') {
+            toggle.attr('aria-expanded', 'true');
+        } else {
+            toggle.attr('aria-expanded', 'false');
+        }
+    });
+
+    // Selecting all / all in component checkboxes.
+    $('#restorablemodelslist').on('click', '[data-select]', function(e) {
+        e.preventDefault();
+        var handler = $(e.currentTarget);
+        var component = handler.attr('data-select');
+
+        if (component == '*') {
+            $('input[data-component]').prop('checked', true);
+        } else {
+            $('input[data-component="' + component + '"]').prop('checked', true);
+        }
+    });
+});
+{{/js}}
diff --git a/admin/tool/analytics/tests/behat/restoredefault.feature b/admin/tool/analytics/tests/behat/restoredefault.feature
new file mode 100644 (file)
index 0000000..f819e5f
--- /dev/null
@@ -0,0 +1,103 @@
+@tool @tool_analytics
+Feature: Restoring default models
+  In order to get prediction models into their initial state
+  As a manager
+  I need to be able to restore deleted default models
+
+  Background:
+    Given the following "users" exist:
+      | username       | firstname     | lastname | email              |
+      | manager        | Max           | Manager  | man@example.com    |
+    And the following "role assigns" exist:
+      | user           | role          | contextlevel  | reference             |
+      | manager        | manager               | System        |                                                       |
+
+  Scenario: Restore a single deleted default model
+    Given I log in as "manager"
+    And I navigate to "Analytics > Analytics models" in site administration
+    # Delete 'No teaching' model.
+    And I click on "Delete" "link" in the "No teaching" "table_row"
+    And I should see "Analytics models"
+    And I should not see "No teaching"
+    # Delete 'Students at risk of dropping out' model.
+    And I click on "Delete" "link" in the "Students at risk of dropping out" "table_row"
+    And I should see "Analytics models"
+    And I should not see "Students at risk of dropping out"
+    # Go to the page for restoring deleted models.
+    When I click on "Restore default models" "link"
+    And I should see "No teaching"
+    And I should see "Students at risk of dropping out"
+    # Select and restore the 'No teaching' model.
+    And I set the field with xpath "//tr[contains(normalize-space(.), 'No teaching')]//input[@type='checkbox']" to "1"
+    And I click on "Restore selected" "button"
+    Then I should see "Succesfully re-created 1 new model(s)."
+    And I should see "Analytics models"
+    And I should see "No teaching"
+    And I should not see "Students at risk of dropping out"
+
+  Scenario: Restore multiple deleted default models at once
+    Given I log in as "manager"
+    And I navigate to "Analytics > Analytics models" in site administration
+    # Delete 'No teaching' model.
+    And I click on "Delete" "link" in the "No teaching" "table_row"
+    And I should see "Analytics models"
+    And I should not see "No teaching"
+    # Delete 'Students at risk of dropping out' model.
+    And I click on "Delete" "link" in the "Students at risk of dropping out" "table_row"
+    And I should see "Analytics models"
+    And I should not see "Students at risk of dropping out"
+    # Go to the page for restoring deleted models.
+    When I click on "Restore default models" "link"
+    And I should see "No teaching"
+    And I should see "Students at risk of dropping out"
+    # Select and restore both models.
+    And I set the field with xpath "//tr[contains(normalize-space(.), 'No teaching')]//input[@type='checkbox']" to "1"
+    And I set the field with xpath "//tr[contains(normalize-space(.), 'Students at risk of dropping out')]//input[@type='checkbox']" to "1"
+    And I click on "Restore selected" "button"
+    Then I should see "Succesfully re-created 2 new model(s)."
+    And I should see "Analytics models"
+    And I should see "No teaching"
+    And I should see "Students at risk of dropping out"
+
+  Scenario: Going to the restore page while no models can be restored
+    Given I log in as "manager"
+    And I navigate to "Analytics > Analytics models" in site administration
+    And I should see "Analytics models"
+    And I should see "No teaching"
+    When I click on "Restore default models" "link"
+    Then I should see "All default models provided by core and installed plugins have been created. No new models were found; there is nothing to restore."
+    And I click on "Back" "link"
+    And I should see "Analytics models"
+
+  @javascript
+  Scenario: User can select and restore all missing models
+    Given I log in as "manager"
+    And I navigate to "Analytics > Analytics models" in site administration
+    # Delete 'No teaching' model.
+    And I click on "Actions" "link" in the "No teaching" "table_row"
+    And I click on "Delete" "link" in the "No teaching" "table_row"
+    And I click on "Delete" "button" in the "Delete" "dialogue"
+    And I should see "Analytics models"
+    And I should not see "No teaching"
+    # Delete 'Students at risk of dropping out' model.
+    And I click on "Actions" "link" in the "Students at risk of dropping out" "table_row"
+    And I click on "Delete" "link" in the "Students at risk of dropping out" "table_row"
+    And I click on "Delete" "button" in the "Delete" "dialogue"
+    And I should see "Analytics models"
+    And I should not see "No teaching"
+    And I should not see "Students at risk of dropping out"
+    # Go to the page for restoring deleted models.
+    And I click on "New model" "link"
+    And I click on "Restore default models" "link"
+    And I should see "No teaching"
+    And I should see "Students at risk of dropping out"
+    # Attempt to submit the form without selecting any model.
+    And I click on "Restore selected" "button"
+    And I should see "Please select models to be restored."
+    # Select all models.
+    When I click on "Select all" "link"
+    And I click on "Restore selected" "button"
+    Then I should see "Succesfully re-created 2 new model(s)."
+    And I should see "Analytics models"
+    And I should see "No teaching"
+    And I should see "Students at risk of dropping out"
index c3d9813..fae12cb 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300; // The current plugin version (Date: YYYYMMDDXX).
-$plugin->requires  = 2018112800; // Requires this Moodle version.
+$plugin->version   = 2019032800; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2019032200; // Requires this Moodle version.
 $plugin->component = 'tool_analytics'; // Full name of the plugin (used for diagnostics).
index bf2b7a1..7de834a 100644 (file)
@@ -162,11 +162,11 @@ class tool_behat_renderer extends plugin_renderer_base {
         $html .= $this->output->heading($title);
 
         // Info.
-        $installurl = behat_command::DOCS_URL . '#Installation';
+        $installurl = behat_command::DOCS_URL;
         $installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
-        $writetestsurl = behat_command::DOCS_URL . '#Writing_features';
+        $writetestsurl = 'https://docs.moodle.org/dev/Writing acceptance tests';
         $writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
-        $writestepsurl = behat_command::DOCS_URL . '#Adding_steps_definitions';
+        $writestepsurl = 'https://docs.moodle.org/dev/Writing_new_acceptance_test_step_definitions';
         $writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
         $infos = array(
             get_string('installinfo', 'tool_behat', $installlink),
index 2c18ac7..80cb7ee 100644 (file)
@@ -614,6 +614,12 @@ class api {
             throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
         }
 
+        // Check if current user has permission to approve delete data request.
+        if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
+            throw new required_capability_exception(context_system::instance(),
+                'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
+        }
+
         // Update the status and the DPO.
         $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
 
@@ -653,6 +659,12 @@ class api {
             throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
         }
 
+        // Check if current user has permission to reject delete data request.
+        if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
+            throw new required_capability_exception(context_system::instance(),
+                'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
+        }
+
         // Update the status and the DPO.
         return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
     }
@@ -752,6 +764,48 @@ class api {
         return true;
     }
 
+    /**
+     * Check if user has permisson to create data deletion request for themselves.
+     *
+     * @param int|null $userid ID of the user.
+     * @return bool
+     * @throws coding_exception
+     */
+    public static function can_create_data_deletion_request_for_self(int $userid = null): bool {
+        global $USER;
+        $userid = $userid ?: $USER->id;
+        return has_capability('tool/dataprivacy:requestdelete', \context_user::instance($userid), $userid);
+    }
+
+    /**
+     * Check if user has permission to create data deletion request for another user.
+     *
+     * @param int|null $userid ID of the user.
+     * @return bool
+     * @throws coding_exception
+     * @throws dml_exception
+     */
+    public static function can_create_data_deletion_request_for_other(int $userid = null): bool {
+        global $USER;
+        $userid = $userid ?: $USER->id;
+        return has_capability('tool/dataprivacy:requestdeleteforotheruser', context_system::instance(), $userid);
+    }
+
+    /**
+     * Check if parent can create data deletion request for their children.
+     *
+     * @param int $userid ID of a user being requested.
+     * @param int|null $requesterid ID of a user making request.
+     * @return bool
+     * @throws coding_exception
+     */
+    public static function can_create_data_deletion_request_for_children(int $userid, int $requesterid = null): bool {
+        global $USER;
+        $requesterid = $requesterid ?: $USER->id;
+        return has_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', \context_user::instance($userid),
+            $requesterid);
+    }
+
     /**
      * Checks whether a user can download a data request.
      *
index a309c8e..3c5ce09 100644 (file)
@@ -170,6 +170,10 @@ class data_request_exporter extends persistent_exporter {
                 $values['canreview'] = true;
                 // Whether the DPO can approve or deny the request.
                 $values['approvedeny'] = in_array($requesttype, [api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_TYPE_DELETE]);
+                // If the request's type is delete, check if user have permission to approve/deny it.
+                if ($requesttype == api::DATAREQUEST_TYPE_DELETE) {
+                    $values['approvedeny'] = api::can_create_data_deletion_request_for_other();
+                }
                 break;
             case api::DATAREQUEST_STATUS_APPROVED:
                 $values['statuslabelclass'] = 'badge-info';
index 7249adf..2f12f58 100644 (file)
@@ -234,10 +234,6 @@ class data_registry_page implements renderable, templatable {
 
             $coursecontext = \context_course::instance($course->id);
 
-            if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
-                continue;
-            }
-
             $coursenode = [
                 'text' => shorten_text(format_string($course->shortname, true, ['context' => $coursecontext])),
                 'contextid' => $coursecontext->id,
index b86e464..af33f98 100644 (file)
@@ -116,9 +116,17 @@ class data_requests_table extends table_sql {
      *
      * @param stdClass $data The row data.
      * @return string
+     * @throws \moodle_exception
+     * @throws coding_exception
      */
     public function col_select($data) {
         if ($data->status == \tool_dataprivacy\api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
+            if ($data->type == \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE
+                && !api::can_create_data_deletion_request_for_other()) {
+                // Don't show checkbox if request's type is delete and user don't have permission.
+                return false;
+            }
+
             $stringdata = [
                 'username' => $data->foruser->fullname,
                 'requesttype' => \core_text::strtolower($data->typenameshort)
@@ -182,7 +190,7 @@ class data_requests_table extends table_sql {
      * @return mixed
      */
     public function col_status($data) {
-        return html_writer::span($data->statuslabel, 'label ' . $data->statuslabelclass);
+        return html_writer::span($data->statuslabel, 'badge ' . $data->statuslabelclass);
     }
 
     /**
@@ -206,6 +214,7 @@ class data_requests_table extends table_sql {
 
         $requestid = $data->id;
         $status = $data->status;
+        $persistent = $this->datarequests[$requestid];
 
         // Prepare actions.
         $actions = [];
@@ -232,6 +241,11 @@ class data_requests_table extends table_sql {
                 }
                 break;
             case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
+                // Only show "Approve" and "Deny" button for deletion request if current user has permission.
+                if ($persistent->get('type') == api::DATAREQUEST_TYPE_DELETE &&
+                    !api::can_create_data_deletion_request_for_other()) {
+                    break;
+                }
                 // Approve.
                 $actiondata['data-action'] = 'approve';
                 $actiontext = get_string('approverequest', 'tool_dataprivacy');
@@ -253,9 +267,11 @@ class data_requests_table extends table_sql {
         }
 
         if ($this->manage) {
-            $persistent = $this->datarequests[$requestid];
             $canreset = $persistent->is_active() || empty($this->ongoingrequests[$data->foruser->id]->{$data->type});
             $canreset = $canreset && $persistent->is_resettable();
+            // Prevent re-submmit deletion request if current user don't have permission.
+            $canreset = $canreset && ($persistent->get('type') != api::DATAREQUEST_TYPE_DELETE ||
+                    api::can_create_data_deletion_request_for_other());
             if ($canreset) {
                 $reseturl = new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', [
                         'requestid' => $requestid,
index 12db2ea..b32dc15 100644 (file)
@@ -76,6 +76,19 @@ if ($data = $mform->get_data()) {
         }
     }
 
+    if ($data->type == \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE) {
+        if ($data->userid == $USER->id) {
+            if (!\tool_dataprivacy\api::can_create_data_deletion_request_for_self()) {
+                throw new moodle_exception('nopermissions', 'error', '',
+                    get_string('errorcannotrequestdeleteforself', 'tool_dataprivacy'));
+            }
+        } else if (!\tool_dataprivacy\api::can_create_data_deletion_request_for_other()
+            && !\tool_dataprivacy\api::can_create_data_deletion_request_for_children($data->userid)) {
+            throw new moodle_exception('nopermissions', 'error', '',
+                get_string('errorcannotrequestdeleteforother', 'tool_dataprivacy'));
+        }
+    }
+
     \tool_dataprivacy\api::create_data_request($data->userid, $data->type, $data->comments);
 
     if ($manage) {
@@ -93,7 +106,7 @@ $PAGE->set_title($title);
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title);
 
-echo $OUTPUT->box_start();
+echo $OUTPUT->box_start('createrequestform');
 $mform->display();
 echo $OUTPUT->box_end();
 
index 4c94a8a..c91213c 100644 (file)
@@ -45,6 +45,7 @@ class tool_dataprivacy_data_request_form extends moodleform {
      * Form definition.
      *
      * @throws coding_exception
+     * @throws dml_exception
      */
     public function definition() {
         global $USER;
@@ -108,6 +109,24 @@ class tool_dataprivacy_data_request_form extends moodleform {
         // Action buttons.
         $this->add_action_buttons();
 
+        $shouldfreeze = false;
+        if ($this->manage) {
+            $shouldfreeze = !api::can_create_data_deletion_request_for_other();
+        } else {
+            $shouldfreeze = !api::can_create_data_deletion_request_for_self();
+            if ($shouldfreeze && !empty($useroptions)) {
+                foreach ($useroptions as $userid => $useroption) {
+                    if (api::can_create_data_deletion_request_for_children($userid)) {
+                        $shouldfreeze = false;
+                        break;
+                    }
+                }
+            }
+        }
+
+        if ($shouldfreeze) {
+            $mform->freeze('type');
+        }
     }
 
     /**
@@ -120,6 +139,7 @@ class tool_dataprivacy_data_request_form extends moodleform {
      * @throws dml_exception
      */
     public function validation($data, $files) {
+        global $USER;
         $errors = [];
 
         $validrequesttypes = [
@@ -134,6 +154,19 @@ class tool_dataprivacy_data_request_form extends moodleform {
             $errors['type'] = get_string('errorrequestalreadyexists', 'tool_dataprivacy');
         }
 
+        // Check if current user can create data deletion request.
+        $userid = $data['userid'];
+        if ($data['type'] == api::DATAREQUEST_TYPE_DELETE) {
+            if ($userid == $USER->id) {
+                if (!api::can_create_data_deletion_request_for_self()) {
+                    $errors['type'] = get_string('errorcannotrequestdeleteforself', 'tool_dataprivacy');
+                }
+            } else if (!api::can_create_data_deletion_request_for_other()
+                && !api::can_create_data_deletion_request_for_children($userid)) {
+                $errors['type'] = get_string('errorcannotrequestdeleteforother', 'tool_dataprivacy');
+            }
+        }
+
         return $errors;
     }
 }
index ad31867..d70f4a5 100644 (file)
@@ -34,6 +34,15 @@ $capabilities = [
         'archetypes' => []
     ],
 
+    // Capability for create new delete data request. Usually given to the site's Protection Officer.
+    'tool/dataprivacy:requestdeleteforotheruser' => [
+        'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_DATALOSS,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => [],
+        'clonepermissionsfrom' => 'tool/dataprivacy:managedatarequests'
+    ],
+
     // Capability for managing the data registry. Usually given to the site's Data Protection Officer.
     'tool/dataprivacy:managedataregistry' => [
         'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_DATALOSS,
@@ -50,6 +59,15 @@ $capabilities = [
         'archetypes' => []
     ],
 
+    // Capability for parents/guardians to make delete data requests on behalf of their children.
+    'tool/dataprivacy:makedatadeletionrequestsforchildren' => [
+        'riskbitmask' => RISK_SPAM | RISK_PERSONAL,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_USER,
+        'archetypes' => [],
+        'clonepermissionsfrom' => 'tool/dataprivacy:makedatarequestsforchildren'
+    ],
+
     // Capability for users to download the results of their own data request.
     'tool/dataprivacy:downloadownrequest' => [
         'riskbitmask' => 0,
@@ -67,4 +85,14 @@ $capabilities = [
         'contextlevel' => CONTEXT_USER,
         'archetypes' => []
     ],
+
+    // Capability for users to create delete data request for their own.
+    'tool/dataprivacy:requestdelete' => [
+        'riskbitmask' => RISK_DATALOSS,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_USER,
+        'archetypes' => [
+            'user' => CAP_ALLOW
+        ]
+    ]
 ];
index bb3fa70..5a0efd4 100644 (file)
@@ -70,6 +70,7 @@ $string['contextlevelname80'] = 'Blocks';
 $string['contextpurposecategorysaved'] = 'Purpose and category saved.';
 $string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.';
 $string['createcategory'] = 'Create data category';
+$string['createdeletedatarequest'] = 'Create data deletion request';
 $string['createnewdatarequest'] = 'Create a new data request';
 $string['createpurpose'] = 'Create data purpose';
 $string['creationauto'] = 'Automatically';
@@ -81,6 +82,9 @@ $string['dataprivacy:managedatarequests'] = 'Manage data requests';
 $string['dataprivacy:managedataregistry'] = 'Manage data registry';
 $string['dataprivacy:downloadownrequest'] = 'Download your own exported data';
 $string['dataprivacy:downloadallrequests'] = 'Download exported data for everyone';
+$string['dataprivacy:requestdeleteforotheruser'] = 'Request data deletion on behalf of another user';
+$string['dataprivacy:makedatadeletionrequestsforchildren'] = 'Request data deletion for minors';
+$string['dataprivacy:requestdelete'] = 'Request data deletion for yourself';
 $string['dataregistry'] = 'Data registry';
 $string['dataregistryinfo'] = 'The data registry enables categories (types of data) and purposes (the reasons for processing data) to be set for all content on the site - from users and courses down to activities and blocks. For each purpose, a retention period may be set. When a retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
 $string['dataretentionexplanation'] = 'This summary shows the default categories and purposes for retaining user data. Certain areas may have more specific categories and purposes than those listed here.';
@@ -124,6 +128,8 @@ $string['editpurposes'] = 'Edit purposes';
 $string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)';
 $string['effectiveretentionperioduser'] = '{$a} (since the last time the user accessed the site)';
 $string['emailsalutation'] = 'Dear {$a},';
+$string['errorcannotrequestdeleteforself'] = 'You don\'t have permission to create deletion request for yourself.';
+$string['errorcannotrequestdeleteforother'] = 'You don\'t have permission to create deletion request for this user.';
 $string['errorinvalidrequestcreationmethod'] = 'Invalid request creation method!';
 $string['errorinvalidrequeststatus'] = 'Invalid request status!';
 $string['errorinvalidrequesttype'] = 'Invalid request type!';
index 8655f1c..c069301 100644 (file)
@@ -77,8 +77,9 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
 
         // Check if the user has an ongoing data deletion request.
         $hasdeleterequest = \tool_dataprivacy\api::has_ongoing_request($user->id, \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE);
-        // Show data deletion link only if the user doesn't have an ongoing data deletion request.
-        if (!$hasdeleterequest) {
+        // Show data deletion link only if the user doesn't have an ongoing data deletion request and has permission
+        // to create data deletion request.
+        if (!$hasdeleterequest && \tool_dataprivacy\api::can_create_data_deletion_request_for_self()) {
             $deleteparams = ['type' => \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE];
             $deleteurl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', $deleteparams);
             $deletenode = new core_user\output\myprofile\node('privacyandpolicies', 'requestdatadeletion',
index ac7520e..4f3ef55 100644 (file)
@@ -44,6 +44,11 @@ $stringparams = (object) [
     ];
 
 if (null !== $confirm && confirm_sesskey()) {
+    if ($originalrequest->get('type') == \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE
+        && !\tool_dataprivacy\api::can_create_data_deletion_request_for_other()) {
+        throw new required_capability_exception(context_system::instance(),
+            'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
+    }
     $originalrequest->resubmit_request();
     redirect($manageurl, get_string('resubmittedrequest', 'tool_dataprivacy', $stringparams));
 }
index 8e60223..f95dedd 100644 (file)
@@ -2109,4 +2109,90 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
 
         return $request;
     }
+
+    /**
+     * Test user cannot create data deletion request for themselves if they don't have
+     * "tool/dataprivacy:requestdelete" capability.
+     *
+     * @throws coding_exception
+     */
+    public function test_can_create_data_deletion_request_for_self_no() {
+        $this->resetAfterTest();
+        $userid = $this->getDataGenerator()->create_user()->id;
+        $roleid = $this->getDataGenerator()->create_role();
+        assign_capability('tool/dataprivacy:requestdelete', CAP_PROHIBIT, $roleid, context_user::instance($userid));
+        role_assign($roleid, $userid, context_user::instance($userid));
+        $this->setUser($userid);
+        $this->assertFalse(api::can_create_data_deletion_request_for_self());
+    }
+
+    /**
+     * Test user can create data deletion request for themselves if they have
+     * "tool/dataprivacy:requestdelete" capability.
+     *
+     * @throws coding_exception
+     */
+    public function test_can_create_data_deletion_request_for_self_yes() {
+        $this->resetAfterTest();
+        $userid = $this->getDataGenerator()->create_user()->id;
+        $this->setUser($userid);
+        $this->assertTrue(api::can_create_data_deletion_request_for_self());
+    }
+
+    /**
+     * Test user cannot create data deletion request for another user if they
+     * don't have "tool/dataprivacy:requestdeleteforotheruser" capability.
+     *
+     * @throws coding_exception
+     * @throws dml_exception
+     */
+    public function test_can_create_data_deletion_request_for_other_no() {
+        $this->resetAfterTest();
+        $userid = $this->getDataGenerator()->create_user()->id;
+        $this->setUser($userid);
+        $this->assertFalse(api::can_create_data_deletion_request_for_other());
+    }
+
+    /**
+     * Test user can create data deletion request for another user if they
+     * don't have "tool/dataprivacy:requestdeleteforotheruser" capability.
+     *
+     * @throws coding_exception
+     */
+    public function test_can_create_data_deletion_request_for_other_yes() {
+        $this->resetAfterTest();
+        $userid = $this->getDataGenerator()->create_user()->id;
+        $roleid = $this->getDataGenerator()->create_role();
+        $contextsystem = context_system::instance();
+        assign_capability('tool/dataprivacy:requestdeleteforotheruser', CAP_ALLOW, $roleid, $contextsystem);
+        role_assign($roleid, $userid, $contextsystem);
+        $this->setUser($userid);
+        $this->assertTrue(api::can_create_data_deletion_request_for_other($userid));
+    }
+
+    /**
+     * Check parents can create data deletion request for their children but not others.
+     *
+     * @throws coding_exception
+     * @throws dml_exception
+     */
+    public function test_can_create_data_deletion_request_for_children() {
+        $this->resetAfterTest();
+
+        $parent = $this->getDataGenerator()->create_user();
+        $child = $this->getDataGenerator()->create_user();
+        $otheruser = $this->getDataGenerator()->create_user();
+
+        $contextsystem = \context_system::instance();
+        $parentrole = $this->getDataGenerator()->create_role();
+        assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW,
+            $parentrole, $contextsystem);
+        assign_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', CAP_ALLOW,
+            $parentrole, $contextsystem);
+        role_assign($parentrole, $parent->id, \context_user::instance($child->id));
+
+        $this->setUser($parent);
+        $this->assertTrue(api::can_create_data_deletion_request_for_children($child->id));
+        $this->assertFalse(api::can_create_data_deletion_request_for_children($otheruser->id));
+    }
 }
index e6c7fde..5806102 100644 (file)
@@ -6,18 +6,24 @@ Feature: Data delete from the privacy API
 
   Background:
     Given the following "users" exist:
-      | username | firstname      | lastname |
-      | victim   | Victim User    | 1        |
-      | parent   | Long-suffering | Parent   |
+      | username       | firstname       | lastname |
+      | victim         | Victim User     | 1        |
+      | parent         | Long-suffering  | Parent   |
+      | privacyofficer | Privacy Officer | One      |
     And the following "roles" exist:
       | shortname | name  | archetype |
       | tired     | Tired |           |
     And the following "permission overrides" exist:
-      | capability                                   | permission | role  | contextlevel | reference |
-      | tool/dataprivacy:makedatarequestsforchildren | Allow      | tired | System       |           |
+      | capability                                           | permission | role    | contextlevel | reference |
+      | tool/dataprivacy:makedatarequestsforchildren         | Allow      | tired   | System       |           |
+      | tool/dataprivacy:makedatadeletionrequestsforchildren | Allow      | tired   | System       |           |
+      | tool/dataprivacy:managedatarequests                  | Allow      | manager | System       |           |
     And the following "role assigns" exist:
       | user   | role  | contextlevel | reference |
       | parent | tired | User         | victim    |
+    And the following "system role assigns" exist:
+      | user           | role    | contextlevel |
+      | privacyofficer | manager | User         |
     And the following config values are set as admin:
       | contactdataprotectionofficer | 1  | tool_dataprivacy |
     And the following data privacy "categories" exist:
@@ -26,6 +32,10 @@ Feature: Data delete from the privacy API
     And the following data privacy "purposes" exist:
       | name         | retentionperiod |
       | Site purpose | P10Y           |
+    And the following config values are set as admin:
+      | contactdataprotectionofficer | 1  | tool_dataprivacy |
+      | privacyrequestexpiry         | 55 | tool_dataprivacy |
+      | dporoles                     | 1  | tool_dataprivacy |
     And I set the site category and purpose to "Site category" and "Site purpose"
 
   @javascript
@@ -115,3 +125,112 @@ Feature: Data delete from the privacy API
     And I run all adhoc tasks
     And I reload the page
     And I should see "You don't have any personal data requests"
+
+  @javascript
+  Scenario: As a Privacy Officer, I cannot create data deletion request unless I have permission.
+    Given I log in as "privacyofficer"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "New request"
+    And I open the autocomplete suggestions list
+    And I click on "Victim User 1" item in the autocomplete list
+    Then I should see "Export all of my personal data"
+    And "Type" "select" should not be visible
+    And the following "permission overrides" exist:
+      | capability                                 | permission | role    | contextlevel | reference |
+      | tool/dataprivacy:requestdeleteforotheruser | Allow      | manager | System       |           |
+    And I reload the page
+    And I open the autocomplete suggestions list
+    And I click on "Victim User 1" item in the autocomplete list
+    And "Type" "select" should be visible
+
+  @javascript
+  Scenario: As a student, I cannot create data deletion request unless I have permission.
+    Given I log in as "victim"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I follow "New request"
+    Then "Type" "select" should exist
+    And the following "permission overrides" exist:
+      | capability                     | permission | role | contextlevel | reference |
+      | tool/dataprivacy:requestdelete | Prevent    | user | System       |           |
+    And I reload the page
+    And I should see "Export all of my personal data"
+    And "Type" "select" should not exist
+
+  @javascript
+  Scenario: As a parent, I cannot create data deletion request unless I have permission.
+    Given I log in as "parent"
+    And the following "permission overrides" exist:
+      | capability                                           | permission | role  | contextlevel | reference |
+      | tool/dataprivacy:makedatadeletionrequestsforchildren | Prevent    | tired | System       | victim    |
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I follow "New request"
+    And I open the autocomplete suggestions list
+    And I click on "Victim User 1" item in the autocomplete list
+    And I set the field "Type" to "Delete all of my personal data"
+    And I press "Save changes"
+    And I should see "You don't have permission to create deletion request for this user."
+    And the following "permission overrides" exist:
+      | capability                                           | permission | role  | contextlevel | reference |
+      | tool/dataprivacy:makedatadeletionrequestsforchildren | Allow      | tired | System       | victim    |
+      | tool/dataprivacy:requestdelete                       | Prevent    | user  | System       |           |
+    And I open the autocomplete suggestions list
+    And I click on "Long-suffering Parent" item in the autocomplete list
+    And I press "Save changes"
+    And I should see "You don't have permission to create deletion request for yourself."
+
+  @javascript
+  Scenario: As a student, link to create data deletion should not be shown if I don't have permission.
+    Given the following "permission overrides" exist:
+      | capability                     | permission | role | contextlevel | reference |
+      | tool/dataprivacy:requestdelete | Prohibit   | user | System       |           |
+    When I log in as "victim"
+    And I follow "Profile" in the user menu
+    Then I should not see "Delete my account"
+
+  @javascript
+  Scenario: As a Privacy Officer, I cannot Approve to Deny deletion data request without permission.
+    Given the following "permission overrides" exist:
+      | capability                                 | permission | role    | contextlevel | reference |
+      | tool/dataprivacy:requestdeleteforotheruser | Allow      | manager | System       |           |
+    When I log in as "privacyofficer"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "New request"
+    And I open the autocomplete suggestions list
+    And I click on "Victim User 1" item in the autocomplete list
+    And I set the field "Type" to "Delete all of my personal data"
+    And I press "Save changes"
+    And the following "permission overrides" exist:
+      | capability                                 | permission | role    | contextlevel | reference |
+      | tool/dataprivacy:requestdeleteforotheruser | Prohibit   | manager | System       |           |
+    And I reload the page
+    Then ".selectrequests" "css_element" should not exist
+    And I open the action menu in "region-main" "region"
+    And I should not see "Approve request"
+    And I should not see "Deny request"
+    And I choose "View the request" in the open action menu
+    And "Approve" "button" should not exist
+    And "Deny" "button" should not exist
+
+  @javascript
+  Scenario: As a Privacy Officer, I cannot re-submit deletion data request without permission.
+    Given the following "permission overrides" exist:
+      | capability                                 | permission | role    | contextlevel | reference |
+      | tool/dataprivacy:requestdeleteforotheruser | Allow      | manager | System       |           |
+    When I log in as "privacyofficer"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "New request"
+    And I open the autocomplete suggestions list
+    And I click on "Victim User 1" item in the autocomplete list
+    And I set the field "Type" to "Delete all of my personal data"
+    And I press "Save changes"
+    And I open the action menu in "region-main" "region"
+    And I follow "Deny request"
+    And I press "Deny request"
+    And the following "permission overrides" exist:
+      | capability                                 | permission | role    | contextlevel | reference |
+      | tool/dataprivacy:requestdeleteforotheruser | Prohibit   | manager | System       |           |
+    And I reload the page
+    And I open the action menu in "region-main" "region"
+    Then I should not see &