Merge branch 'MDL-65051-master' of git://github.com/lameze/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 27 Mar 2019 21:27:25 +0000 (22:27 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 27 Mar 2019 21:27:25 +0000 (22:27 +0100)
538 files changed:
.eslintignore
.stylelintignore
Gruntfile.js
admin/classes/form/testoutgoingmailconf_form.php [new file with mode: 0644]
admin/message.php
admin/roles/classes/check_users_selector.php
admin/settings/analytics.php
admin/settings/courses.php
admin/settings/messaging.php [new file with mode: 0644]
admin/settings/plugins.php
admin/settings/server.php
admin/settings/subsystems.php
admin/settings/top.php
admin/testoutgoingmailconf.php [new file with mode: 0644]
admin/tests/behat/behat_admin.php
admin/tool/analytics/amd/build/model.min.js
admin/tool/analytics/amd/src/model.js
admin/tool/analytics/classes/output/model_logs.php
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/cli/evaluate_model.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/templates/evaluation_mode_selection.mustache [new file with mode: 0644]
admin/tool/customlang/classes/output/renderer.php [new file with mode: 0644]
admin/tool/customlang/classes/output/translator.php [new file with mode: 0644]
admin/tool/customlang/renderer.php [deleted file]
admin/tool/customlang/styles.css [deleted file]
admin/tool/customlang/templates/translator.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/coverage.php [new file with mode: 0644]
admin/tool/mobile/classes/external.php
admin/tool/mobile/db/services.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/lib.php
admin/tool/mobile/tests/api_test.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/upgrade.txt
admin/tool/mobile/version.php
admin/tool/policy/classes/output/page_agreedocs.php
admin/tool/policy/tests/behat/acceptances.feature
admin/tool/recyclebin/classes/course_bin.php
admin/tool/recyclebin/tests/course_bin_test.php
admin/tool/usertours/classes/manager.php
admin/tool/usertours/db/upgrade.php
admin/tool/usertours/version.php
analytics/classes/classifier.php
analytics/classes/local/target/base.php
analytics/classes/manager.php
analytics/classes/model.php
analytics/classes/regressor.php
analytics/classes/stats.php [new file with mode: 0644]
analytics/tests/prediction_test.php
analytics/tests/stats_test.php [new file with mode: 0644]
analytics/upgrade.txt
backup/moodle2/backup_stepslib.php
badges/index.php
blocks/moodleblock.class.php
blocks/myoverview/db/access.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lang/en/deprecated.txt
blocks/myoverview/templates/nav-sort-selector.mustache
blocks/myoverview/templates/progress-bar.mustache
blocks/myoverview/upgrade.txt [new file with mode: 0644]
blocks/myoverview/version.php
blocks/recentlyaccessedcourses/db/access.php
blocks/recentlyaccessedcourses/lang/en/block_recentlyaccessedcourses.php
blocks/recentlyaccessedcourses/tests/behat/block_recentlyaccessedcourses_dashboard.feature
blocks/recentlyaccessedcourses/upgrade.txt [new file with mode: 0644]
blocks/recentlyaccessedcourses/version.php
blocks/recentlyaccesseditems/db/access.php
blocks/recentlyaccesseditems/lang/en/block_recentlyaccesseditems.php
blocks/recentlyaccesseditems/tests/behat/block_recentlyaccesseditems_dashboard.feature
blocks/recentlyaccesseditems/upgrade.txt [new file with mode: 0644]
blocks/recentlyaccesseditems/version.php
blocks/starredcourses/db/access.php
blocks/starredcourses/lang/en/block_starredcourses.php
blocks/starredcourses/upgrade.txt [new file with mode: 0644]
blocks/starredcourses/version.php
blocks/timeline/db/access.php
blocks/timeline/lang/en/block_timeline.php
blocks/timeline/templates/nav-day-filter.mustache
blocks/timeline/templates/nav-view-selector.mustache
blocks/timeline/tests/behat/block_timeline_courses.feature
blocks/timeline/tests/behat/block_timeline_dates.feature
blocks/timeline/tests/behat/block_timeline_pagelimit_persistence.feature
blocks/timeline/upgrade.txt [new file with mode: 0644]
blocks/timeline/version.php
blocks/upgrade.txt
cache/stores/mongodb/MongoDB/BulkWriteResult.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/ChangeStream.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Client.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Collection.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Database.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/DeleteResult.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Exception/BadMethodCallException.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Exception/Exception.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Exception/InvalidArgumentException.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Exception/ResumeTokenException.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Exception/RuntimeException.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Exception/UnexpectedValueException.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Exception/UnsupportedException.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/GridFS/Bucket.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/GridFS/CollectionWrapper.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/GridFS/Exception/CorruptFileException.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/GridFS/Exception/FileNotFoundException.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/GridFS/ReadableStream.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/GridFS/StreamWrapper.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/GridFS/WritableStream.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/InsertManyResult.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/InsertOneResult.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/MapReduceResult.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/BSONArray.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/BSONDocument.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/BSONIterator.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/CachingIterator.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/CollectionInfo.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/CollectionInfoCommandIterator.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/CollectionInfoIterator.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/DatabaseInfo.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/DatabaseInfoIterator.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/DatabaseInfoLegacyIterator.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/IndexInfo.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/IndexInfoIterator.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/IndexInfoIteratorIterator.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/IndexInput.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/TypeMapArrayIterator.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/Aggregate.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/BulkWrite.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/Count.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/CountDocuments.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/CreateCollection.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/CreateIndexes.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/DatabaseCommand.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/Delete.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/DeleteMany.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/DeleteOne.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/Distinct.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/DropCollection.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/DropDatabase.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/DropIndexes.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/EstimatedDocumentCount.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/Executable.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/Explain.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/Explainable.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/Find.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/FindAndModify.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/FindOne.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/FindOneAndDelete.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/FindOneAndReplace.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/FindOneAndUpdate.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/InsertMany.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/InsertOne.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/ListCollections.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/ListDatabases.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/ListIndexes.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/MapReduce.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/ModifyCollection.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/ReplaceOne.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/Update.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/UpdateMany.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/UpdateOne.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Operation/Watch.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/UpdateResult.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/functions.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/readme_moodle.txt [new file with mode: 0644]
cache/stores/mongodb/addinstanceform.php
cache/stores/mongodb/lib.php
cache/stores/mongodb/thirdpartylibs.xml [new file with mode: 0644]
cache/upgrade.txt
calendar/classes/local/api.php
calendar/externallib.php
calendar/tests/externallib_test.php
calendar/tests/local_api_test.php
completion/classes/progress.php
completion/tests/progress_test.php
config-dist.php
course/externallib.php
course/format/social/format.php
course/format/social/tests/behat/social_adjust_discussion_count.feature
course/renderer.php
course/tests/behat/behat_course.php
course/tests/behat/category_role_assignment.feature
course/tests/behat/course_creation.feature
course/tests/behat/navigate_course_list.feature
course/tests/courselib_test.php
enrol/externallib.php
enrol/tests/enrollib_test.php
enrol/tests/externallib_test.php
enrol/upgrade.txt
files/classes/external/stored_file_exporter.php
files/renderer.php
grade/import/csv/classes/load_data.php
grade/import/csv/tests/load_data_test.php
group/lib.php
group/tests/behat/behat_groups.php
install/lang/hi/moodle.php
lang/en/admin.php
lang/en/analytics.php
lang/en/backup.php
lang/en/deprecated.txt
lang/en/grades.php
lang/en/hub.php
lang/en/message.php
lang/en/plugin.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/templates.min.js
lib/amd/src/templates.js
lib/behat/behat_base.php
lib/behat/classes/partial_named_selector.php
lib/behat/form_field/behat_form_filemanager.php
lib/behat/form_field/behat_form_passwordunmask.php
lib/behat/form_field/behat_form_select.php
lib/classes/component.php
lib/classes/external/exporter.php
lib/classes/external/paged_content_exporter.php [new file with mode: 0644]
lib/classes/hub/registration.php
lib/classes/message/manager.php
lib/classes/output/external.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/output/mustache_template_source_loader.php
lib/classes/output/notification.php
lib/classes/plugin_manager.php
lib/classes/useragent.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/dml/moodle_database.php
lib/dml/tests/dml_test.php
lib/editor/atto/plugins/recordrtc/lib.php
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button-debug.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-debug.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording.js
lib/editor/atto/plugins/recordrtc/yui/src/button/js/button.js
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/audiomodule.js
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/videomodule.js
lib/enrollib.php
lib/externallib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/form/tests/behat/modgrade_validation.feature
lib/mlbackend/php/classes/processor.php
lib/mlbackend/python/classes/processor.php
lib/moodlelib.php
lib/outputcomponents.php
lib/phpunit/classes/coverage_info.php [new file with mode: 0644]
lib/phpunit/classes/util.php
lib/portfolio/constants.php
lib/portfoliolib.php
lib/templates/notification.mustache [new file with mode: 0644]
lib/templates/single_select.mustache
lib/templates/url_select.mustache
lib/tests/accesslib_test.php
lib/tests/behat/behat_app.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/behat_navigation.php
lib/tests/behat/behat_permissions.php
lib/tests/behat/locking.feature
lib/tests/coverage.php [new file with mode: 0644]
lib/tests/exporter_test.php
lib/tests/externallib_test.php
lib/tests/filelib_test.php
lib/tests/messagelib_test.php
lib/tests/outputcomponents_test.php
lib/tests/useragent_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
message/amd/build/message_drawer_events.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_conversation_state_manager.min.js
message/amd/build/message_drawer_view_overview_section.min.js
message/amd/build/message_repository.min.js
message/amd/src/message_drawer_events.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_conversation_state_manager.js
message/amd/src/message_drawer_view_overview_section.js
message/amd/src/message_repository.js
message/classes/api.php
message/classes/privacy/provider.php
message/defaultoutputs.php
message/externallib.php
message/renderer.php
message/templates/message_drawer_conversations_list.mustache
message/templates/message_drawer_view_conversation_body_confirm_dialogue.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/tests/api_test.php
message/tests/behat/behat_message.php
message/tests/behat/group_conversation.feature [new file with mode: 0644]
message/tests/externallib_test.php
message/tests/privacy_provider_test.php
mod/assign/feedback/editpdf/ajax.php
mod/assign/feedback/editpdf/backup/moodle2/backup_assignfeedback_editpdf_subplugin.class.php
mod/assign/feedback/editpdf/backup/moodle2/restore_assignfeedback_editpdf_subplugin.class.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/page_editor.php
mod/assign/feedback/editpdf/classes/pdf.php
mod/assign/feedback/editpdf/classes/privacy/provider.php
mod/assign/feedback/editpdf/classes/renderer.php
mod/assign/feedback/editpdf/db/install.xml
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/pix/rotate_left.svg [new file with mode: 0644]
mod/assign/feedback/editpdf/pix/rotate_right.svg [new file with mode: 0644]
mod/assign/feedback/editpdf/tests/behat/behat_assignfeedback_editpdf.php
mod/assign/feedback/editpdf/version.php
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/globals.js
mod/feedback/tests/behat/behat_mod_feedback.php
mod/forum/amd/build/discussion_list.min.js [new file with mode: 0644]
mod/forum/amd/build/repository.min.js [new file with mode: 0644]
mod/forum/amd/build/selectors.min.js [new file with mode: 0644]
mod/forum/amd/build/subscription_toggle.min.js [new file with mode: 0644]
mod/forum/amd/src/discussion_list.js [new file with mode: 0644]
mod/forum/amd/src/repository.js [new file with mode: 0644]
mod/forum/amd/src/selectors.js [new file with mode: 0644]
mod/forum/amd/src/subscription_toggle.js [new file with mode: 0644]
mod/forum/classes/local/builders/exported_discussion_summaries.php [new file with mode: 0644]
mod/forum/classes/local/builders/exported_posts.php [new file with mode: 0644]
mod/forum/classes/local/container.php [new file with mode: 0644]
mod/forum/classes/local/data_mappers/legacy/author.php [new file with mode: 0644]
mod/forum/classes/local/data_mappers/legacy/discussion.php [new file with mode: 0644]
mod/forum/classes/local/data_mappers/legacy/forum.php [new file with mode: 0644]
mod/forum/classes/local/data_mappers/legacy/post.php [new file with mode: 0644]
mod/forum/classes/local/entities/author.php [new file with mode: 0644]
mod/forum/classes/local/entities/discussion.php [new file with mode: 0644]
mod/forum/classes/local/entities/discussion_summary.php [new file with mode: 0644]
mod/forum/classes/local/entities/forum.php [new file with mode: 0644]
mod/forum/classes/local/entities/post.php [new file with mode: 0644]
mod/forum/classes/local/entities/post_read_receipt_collection.php [new file with mode: 0644]
mod/forum/classes/local/entities/sorter.php [new file with mode: 0644]
mod/forum/classes/local/exporters/author.php [new file with mode: 0644]
mod/forum/classes/local/exporters/discussion.php [new file with mode: 0644]
mod/forum/classes/local/exporters/discussion_summaries.php [new file with mode: 0644]
mod/forum/classes/local/exporters/discussion_summary.php [new file with mode: 0644]
mod/forum/classes/local/exporters/forum.php [new file with mode: 0644]
mod/forum/classes/local/exporters/post.php [new file with mode: 0644]
mod/forum/classes/local/exporters/posts.php [new file with mode: 0644]
mod/forum/classes/local/factories/builder.php [new file with mode: 0644]
mod/forum/classes/local/factories/entity.php [new file with mode: 0644]
mod/forum/classes/local/factories/exporter.php [new file with mode: 0644]
mod/forum/classes/local/factories/legacy_data_mapper.php [new file with mode: 0644]
mod/forum/classes/local/factories/manager.php [new file with mode: 0644]
mod/forum/classes/local/factories/renderer.php [new file with mode: 0644]
mod/forum/classes/local/factories/url.php [new file with mode: 0644]
mod/forum/classes/local/factories/vault.php [new file with mode: 0644]
mod/forum/classes/local/managers/capability.php [new file with mode: 0644]
mod/forum/classes/local/renderers/discussion.php [new file with mode: 0644]
mod/forum/classes/local/renderers/discussion_list.php [new file with mode: 0644]
mod/forum/classes/local/renderers/posts.php [new file with mode: 0644]
mod/forum/classes/local/vaults/author.php [new file with mode: 0644]
mod/forum/classes/local/vaults/db_table_vault.php [new file with mode: 0644]
mod/forum/classes/local/vaults/discussion.php [new file with mode: 0644]
mod/forum/classes/local/vaults/discussion_list.php [new file with mode: 0644]
mod/forum/classes/local/vaults/forum.php [new file with mode: 0644]
mod/forum/classes/local/vaults/post.php [new file with mode: 0644]
mod/forum/classes/local/vaults/post_attachment.php [new file with mode: 0644]
mod/forum/classes/local/vaults/post_read_receipt_collection.php [new file with mode: 0644]
mod/forum/classes/local/vaults/preprocessors/extract_context.php [new file with mode: 0644]
mod/forum/classes/local/vaults/preprocessors/extract_record.php [new file with mode: 0644]
mod/forum/classes/local/vaults/preprocessors/extract_user.php [new file with mode: 0644]
mod/forum/db/caches.php [new file with mode: 0644]
mod/forum/db/services.php
mod/forum/deprecatedlib.php
mod/forum/discuss.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/markposts.php
mod/forum/post.php
mod/forum/search.php
mod/forum/settings.php
mod/forum/templates/blog_discussion_list.mustache [new file with mode: 0644]
mod/forum/templates/discussion_list.mustache [new file with mode: 0644]
mod/forum/templates/discussion_subscription_toggle.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion_nested_post.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion_nested_posts.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion_post.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion_posts.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion_threaded_post.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion_threaded_posts.mustache [new file with mode: 0644]
mod/forum/templates/forum_posts_with_context_links.mustache [new file with mode: 0644]
mod/forum/templates/frontpage_news_discussion_list.mustache [new file with mode: 0644]
mod/forum/templates/frontpage_social_discussion_list.mustache [new file with mode: 0644]
mod/forum/templates/news_discussion_list.mustache [new file with mode: 0644]
mod/forum/templates/qanda_discussion_list.mustache [new file with mode: 0644]
mod/forum/templates/single_discussion_list.mustache [new file with mode: 0644]
mod/forum/templates/social_discussion_list.mustache [new file with mode: 0644]
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/tests/behat/discussion_display.feature
mod/forum/tests/behat/edit_tags.feature
mod/forum/tests/behat/forum_subscriptions_default.feature
mod/forum/tests/behat/groups_in_course_no_groups_in_forum.feature
mod/forum/tests/behat/no_groups_in_course.feature
mod/forum/tests/behat/post_to_multiple_groups.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/builders_exported_posts_test.php [new file with mode: 0644]
mod/forum/tests/coverage.php [new file with mode: 0644]
mod/forum/tests/entities_author_test.php [new file with mode: 0644]
mod/forum/tests/entities_discussion_summary_test.php [new file with mode: 0644]
mod/forum/tests/entities_discussion_test.php [new file with mode: 0644]
mod/forum/tests/entities_forum_test.php [new file with mode: 0644]
mod/forum/tests/entities_post_read_receipt_collection_test.php [new file with mode: 0644]
mod/forum/tests/entities_post_test.php [new file with mode: 0644]
mod/forum/tests/entities_sorter_test.php [new file with mode: 0644]
mod/forum/tests/exporters_author_test.php [new file with mode: 0644]
mod/forum/tests/exporters_discussion_test.php [new file with mode: 0644]
mod/forum/tests/exporters_forum_test.php [new file with mode: 0644]
mod/forum/tests/exporters_post_test.php [new file with mode: 0644]
mod/forum/tests/externallib_test.php
mod/forum/tests/generator/lib.php
mod/forum/tests/lib_test.php
mod/forum/tests/local_container_test.php [new file with mode: 0644]
mod/forum/tests/managers_capability_test.php [new file with mode: 0644]
mod/forum/tests/vaults_author_test.php [new file with mode: 0644]
mod/forum/tests/vaults_discussion_list_test.php [new file with mode: 0644]
mod/forum/tests/vaults_discussion_test.php [new file with mode: 0644]
mod/forum/tests/vaults_forum_test.php [new file with mode: 0644]
mod/forum/tests/vaults_post_attachment_test.php [new file with mode: 0644]
mod/forum/tests/vaults_post_read_receipt_collection_test.php [new file with mode: 0644]
mod/forum/tests/vaults_post_test.php [new file with mode: 0644]
mod/forum/upgrade.txt
mod/forum/user.php
mod/forum/version.php
mod/forum/view.php
mod/lti/lib.php
mod/lti/tests/lib_test.php
mod/quiz/lib.php
mod/quiz/tests/lib_test.php
mod/scorm/classes/external.php
mod/scorm/db/services.php
mod/scorm/tests/externallib_test.php
mod/scorm/version.php
mod/survey/lib.php
mod/survey/tests/lib_test.php
mod/wiki/edit_form.php
mod/wiki/lib.php
mod/wiki/tests/lib_test.php
mod/workshop/allocation/manual/tests/behat/behat_workshopallocation_manual.php
mod/workshop/lib.php
mod/workshop/tests/lib_test.php
notes/externallib.php
notes/tests/externallib_test.php
notes/upgrade.txt [new file with mode: 0644]
phpunit.xml.dist
pix/i/muted.png [new file with mode: 0644]
pix/i/muted.svg [new file with mode: 0644]
pix/t/reload.svg [new file with mode: 0644]
privacy/tests/coverage.php [new file with mode: 0644]
question/format.php
question/format/aiken/format.php
question/format/aiken/tests/behat/aiken_export.feature [new file with mode: 0644]
question/format/aiken/tests/behat/aiken_import.feature [new file with mode: 0644]
question/format/aiken/tests/qformat_aiken_export_test.php [new file with mode: 0644]
question/upgrade.txt
repository/nextcloud/lang/en/repository_nextcloud.php
repository/tests/behat/behat_filepicker.php
repository/upload/tests/behat/behat_repository_upload.php
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/filemanager.scss
theme/boost/style/moodle.css
theme/boost/tests/behat/group_conversation.feature [new file with mode: 0644]
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache
theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private_no_controls.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_public.mustache
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_admin.php
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_filepicker.php
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_repository_upload.php
theme/classic/classes/output/core_renderer.php [new file with mode: 0644]
theme/classic/classes/privacy/provider.php [new file with mode: 0644]
theme/classic/config.php [new file with mode: 0644]
theme/classic/lang/en/theme_classic.php [new file with mode: 0644]
theme/classic/layout/columns.php [new file with mode: 0644]
theme/classic/layout/contentonly.php [new file with mode: 0644]
theme/classic/layout/secure.php [new file with mode: 0644]
theme/classic/lib.php [new file with mode: 0644]
theme/classic/pix/favicon.ico [new file with mode: 0644]
theme/classic/pix/screenshot.png [new file with mode: 0644]
theme/classic/scss/bootstrap.scss [new file with mode: 0644]
theme/classic/scss/classic/body-background.scss [new file with mode: 0644]
theme/classic/scss/classic/navbar-dark.scss [new file with mode: 0644]
theme/classic/scss/classic/navbar-light.scss [new file with mode: 0644]
theme/classic/scss/classic/post.scss [new file with mode: 0644]
theme/classic/scss/classic/pre.scss [new file with mode: 0644]
theme/classic/scss/classicgrunt.scss [new file with mode: 0644]
theme/classic/scss/fontawesome.scss [new file with mode: 0644]
theme/classic/scss/moodle.scss [new file with mode: 0644]
theme/classic/scss/preset/default.scss [new file with mode: 0644]
theme/classic/scss/preset/plain.scss [new file with mode: 0644]
theme/classic/settings.php [new file with mode: 0644]
theme/classic/style/moodle.css [new file with mode: 0644]
theme/classic/templates/columns.mustache [new file with mode: 0644]
theme/classic/templates/contentonly.mustache [new file with mode: 0644]
theme/classic/templates/core/footer.mustache [new file with mode: 0644]
theme/classic/templates/core/full_header.mustache [new file with mode: 0644]
theme/classic/templates/navbar-secure.mustache [new file with mode: 0644]
theme/classic/templates/navbar.mustache [new file with mode: 0644]
theme/classic/templates/secure.mustache [new file with mode: 0644]
theme/classic/tests/behat/behat_theme_classic_behat_admin.php [new file with mode: 0644]
theme/classic/tests/behat/behat_theme_classic_behat_blocks.php [new file with mode: 0644]
theme/classic/tests/behat/behat_theme_classic_behat_course.php [new file with mode: 0644]
theme/classic/tests/behat/behat_theme_classic_behat_grade.php [new file with mode: 0644]
theme/classic/tests/behat/behat_theme_classic_behat_mod_quiz.php [new file with mode: 0644]
theme/classic/tests/behat/behat_theme_classic_behat_navigation.php [new file with mode: 0644]
theme/classic/tests/behat/behat_theme_classic_behat_repository_upload.php [new file with mode: 0644]
theme/classic/tests/behat/blacklist.json [new file with mode: 0644]
theme/classic/tests/behat/courseadministrationmenu.feature [new file with mode: 0644]
theme/classic/tests/behat/pageadministrationmenu.feature [new file with mode: 0644]
theme/classic/tests/scss_test.php [new file with mode: 0644]
theme/classic/version.php [new file with mode: 0644]
user/lib.php
version.php
webservice/externallib.php
webservice/tests/externallib_test.php
webservice/upgrade.txt

index 420eb21..9153e18 100644 (file)
@@ -6,6 +6,7 @@ vendor/
 admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
 admin/tool/usertours/amd/src/tour.js
 auth/cas/CAS/
+cache/stores/mongodb/MongoDB/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
index be8cec8..d8056ab 100644 (file)
@@ -4,11 +4,13 @@ theme/bootstrapbase/style/
 theme/clean/style/custom.css
 theme/more/style/custom.css
 theme/boost/style/moodle.css
+theme/classic/style/moodle.css
 node_modules/
 vendor/
 admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
 admin/tool/usertours/amd/src/tour.js
 auth/cas/CAS/
+cache/stores/mongodb/MongoDB/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
index 29d271c..eb136bb 100644 (file)
@@ -140,11 +140,12 @@ module.exports = function(grunt) {
         sass: {
             dist: {
                 files: {
-                    "theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss"
+                    "theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss",
+                    "theme/classic/style/moodle.css": "theme/classic/scss/classicgrunt.scss"
                 }
             },
             options: {
-                includePaths: ["theme/boost/scss/"]
+                includePaths: ["theme/boost/scss/", "theme/classic/scss/"]
             }
         },
         watch: {
@@ -226,7 +227,8 @@ module.exports = function(grunt) {
           'theme/bootstrapbase/style/',
           'theme/clean/style/custom.css',
           'theme/more/style/custom.css',
-          'theme/boost/style/moodle.css'
+          'theme/boost/style/moodle.css',
+          'theme/classic/style/moodle.css',
       ].concat(thirdPartyPaths);
       grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
     };
diff --git a/admin/classes/form/testoutgoingmailconf_form.php b/admin/classes/form/testoutgoingmailconf_form.php
new file mode 100644 (file)
index 0000000..fbf8ac4
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Testing outgoing mail configuration form
+ *
+ * @package    core
+ * @copyright  2019 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_admin\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Test mail form
+ *
+ * @package    core
+ * @copyright 2019 Victor Deniz <victor@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testoutgoingmailconf_form extends \moodleform {
+    /**
+     * Add elements to form
+     */
+    public function definition() {
+        $mform = $this->_form;
+
+        // Recipient.
+        $options = ['maxlength' => '100', 'size' => '25'];
+        $mform->addElement('text', 'recipient', get_string('testoutgoingmailconf_toemail', 'admin'), $options);
+        $mform->setType('recipient', PARAM_EMAIL);
+        $mform->addRule('recipient', get_string('required'), 'required');
+
+        $buttonarray = array();
+        $buttonarray[] = $mform->createElement('submit', 'send', get_string('testoutgoingmailconf_sendtest', 'admin'));
+        $buttonarray[] = $mform->createElement('cancel');
+
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+
+    }
+}
index c10034a..9a43f61 100644 (file)
@@ -25,47 +25,101 @@ require_once(__DIR__ . '/../config.php');
 require_once($CFG->dirroot . '/message/lib.php');
 require_once($CFG->libdir.'/adminlib.php');
 
-// This is an admin page
+// This is an admin page.
 admin_externalpage_setup('managemessageoutputs');
 
-// Get the submitted params
-$disable    = optional_param('disable', 0, PARAM_INT);
-$enable     = optional_param('enable', 0, PARAM_INT);
+// Fetch processors.
+$allprocessors = get_message_processors();
+$processors = array_filter($allprocessors, function($processor) {
+    return $processor->enabled;
+});
+// Fetch message providers.
+$providers = get_message_providers();
+// Fetch the manage message outputs interface.
+$preferences = get_message_output_default_preferences();
 
-$headingtitle = get_string('managemessageoutputs', 'message');
+if (($form = data_submitted()) && confirm_sesskey()) {
+    $preferences = array();
+    // Prepare default message outputs settings.
+    foreach ($providers as $provider) {
+        $componentproviderbase = $provider->component.'_'.$provider->name;
+        $disableprovidersetting = $componentproviderbase.'_disable';
+        $providerdisabled = false;
+        if (!isset($form->$disableprovidersetting)) {
+            $providerdisabled = true;
+            $preferences[$disableprovidersetting] = 1;
+        } else {
+            $preferences[$disableprovidersetting] = 0;
+        }
 
-if (!empty($disable) && confirm_sesskey()) {
-    if (!$processor = $DB->get_record('message_processors', array('id'=>$disable))) {
-        print_error('outputdoesnotexist', 'message');
+        foreach (array('permitted', 'loggedin', 'loggedoff') as $setting) {
+            $value = null;
+            $componentprovidersetting = $componentproviderbase.'_'.$setting;
+            if ($setting == 'permitted') {
+                // If we deal with permitted select element, we need to create individual
+                // setting for each possible processor. Note that this block will
+                // always be processed first after entring parental foreach iteration
+                // so we can change form values on this stage.
+                foreach ($allprocessors as $processor) {
+                    $value = '';
+                    if (isset($form->{$componentprovidersetting}[$processor->name])) {
+                        $value = $form->{$componentprovidersetting}[$processor->name];
+                    }
+                    // Ensure that loggedin loggedoff options are set correctly for this permission.
+                    if (($value == 'disallowed') || $providerdisabled) {
+                        // It might be better to unset them, but I can't figure out why that cause error.
+                        $form->{$componentproviderbase.'_loggedin'}[$processor->name] = 0;
+                        $form->{$componentproviderbase.'_loggedoff'}[$processor->name] = 0;
+                    } else if ($value == 'forced') {
+                        $form->{$componentproviderbase.'_loggedin'}[$processor->name] = 1;
+                        $form->{$componentproviderbase.'_loggedoff'}[$processor->name] = 1;
+                    }
+                    // Record the site preference.
+                    $preferences[$processor->name.'_provider_'.$componentprovidersetting] = $value;
+                }
+            } else if (array_key_exists($componentprovidersetting, $form)) {
+                // We must be processing loggedin or loggedoff checkboxes. Store
+                // defained comma-separated processors as setting value.
+                // Using array_filter eliminates elements set to 0 above.
+                $value = join(',', array_keys(array_filter($form->{$componentprovidersetting})));
+                if (empty($value)) {
+                    $value = null;
+                }
+            }
+            if ($setting != 'permitted') {
+                // We have already recoded site preferences for 'permitted' type.
+                $preferences['message_provider_'.$componentprovidersetting] = $value;
+            }
+        }
+    }
+
+    // Update database.
+    $transaction = $DB->start_delegated_transaction();
+
+    // Save processors enabled/disabled status.
+    foreach ($allprocessors as $processor) {
+        $enabled = isset($form->{$processor->name});
+        \core_message\api::update_processor_status($processor, $enabled);
     }
-    \core_message\api::update_processor_status($processor, 0);     // Disable output.
-    core_plugin_manager::reset_caches();
-}
 
-if (!empty($enable) && confirm_sesskey()) {
-    if (!$processor = $DB->get_record('message_processors', array('id'=>$enable))) {
-        print_error('outputdoesnotexist', 'message');
+    foreach ($preferences as $name => $value) {
+        set_config($name, $value, 'message');
     }
-    \core_message\api::update_processor_status($processor, 1);      // Enable output.
+    $transaction->allow_commit();
+
     core_plugin_manager::reset_caches();
-}
 
-if ($disable || $enable) {
     $url = new moodle_url('message.php');
     redirect($url);
 }
+
 // Page settings
 $PAGE->set_context(context_system::instance());
+$PAGE->requires->js_init_call('M.core_message.init_defaultoutputs');
 
-// Grab the renderer
 $renderer = $PAGE->get_renderer('core', 'message');
 
-// Display the manage message outputs interface
-$processors = get_message_processors();
-$messageoutputs = $renderer->manage_messageoutputs($processors);
-
-// Display the page
+// Display the page.
 echo $OUTPUT->header();
-echo $OUTPUT->heading($headingtitle);
-echo $messageoutputs;
+echo $renderer->manage_messageoutput_settings($allprocessors, $processors, $providers, $preferences);
 echo $OUTPUT->footer();
index 5ac4c26..31929bd 100644 (file)
@@ -69,8 +69,11 @@ class core_role_check_users_selector extends user_selector_base {
 
         if ($coursecontext and $coursecontext != SITEID) {
             $sql1 = " FROM {user} u
-                      JOIN {user_enrolments} ue ON (ue.userid = u.id)
-                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid1)
+                      JOIN (SELECT DISTINCT subu.id
+                              FROM {user} subu
+                              JOIN {user_enrolments} ue ON (ue.userid = subu.id)
+                              JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid1)
+                           ) subq ON subq.id = u.id
                      WHERE $wherecondition";
             $params['courseid1'] = $coursecontext->instanceid;
 
index d58124c..5f43a52 100644 (file)
@@ -83,8 +83,9 @@ if ($hassiteconfig) {
         foreach ($alltimesplittings as $key => $timesplitting) {
             $timesplittingoptions[$key] = $timesplitting->get_name();
         }
-        $settings->add(new admin_setting_configmultiselect('analytics/timesplittings',
-            new lang_string('enabledtimesplittings', 'analytics'), new lang_string('timesplittingmethod_help', 'analytics'),
+        $settings->add(new admin_setting_configmultiselect('analytics/defaulttimesplittingsevaluation',
+            new lang_string('defaulttimesplittingmethods', 'analytics'),
+            new lang_string('defaulttimesplittingmethods_help', 'analytics'),
             $timesplittingdefaults, $timesplittingoptions)
         );
 
index 739c96e..f214ab4 100644 (file)
@@ -336,7 +336,7 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_skip_modif_prev', new lang_string('skipmodifprev', 'backup'), new lang_string('skipmodifprevhelp', 'backup'), 0));
 
     // Automated defaults section.
-    $temp->add(new admin_setting_heading('automatedsettings', new lang_string('automatedsettings','backup'), ''));
+    $temp->add(new admin_setting_heading('automatedsettings', new lang_string('automatedsettings','backup'), new lang_string('recyclebin_desc', 'backup')));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_users', new lang_string('generalusers', 'backup'), new lang_string('configgeneralusers', 'backup'), 1));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_role_assignments', new lang_string('generalroleassignments','backup'), new lang_string('configgeneralroleassignments','backup'), 1));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_activities', new lang_string('generalactivities','backup'), new lang_string('configgeneralactivities','backup'), 1));
diff --git a/admin/settings/messaging.php b/admin/settings/messaging.php
new file mode 100644 (file)
index 0000000..2221e72
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Adds messaging related settings links for Messaging category to admin tree.
+ *
+ * @copyright 2019 Amaia Anabitarte <amaia@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig) {
+    $temp = new admin_settingpage('messages', new lang_string('messagingssettings', 'admin'));
+    $temp->add(new admin_setting_configcheckbox('messaging',
+        new lang_string('messaging', 'admin'),
+        new lang_string('configmessaging', 'admin'),
+        1));
+    $temp->add(new admin_setting_configcheckbox('messagingallusers',
+            new lang_string('messagingallusers', 'admin'),
+            new lang_string('configmessagingallusers', 'admin'),
+             0)
+    );
+    $temp->add(new admin_setting_configcheckbox('messagingdefaultpressenter',
+            new lang_string('messagingdefaultpressenter', 'admin'),
+            new lang_string('configmessagingdefaultpressenter', 'admin'),
+            1)
+    );
+    $options = array(
+        DAYSECS => new lang_string('secondstotime86400'),
+        WEEKSECS => new lang_string('secondstotime604800'),
+        2620800 => new lang_string('nummonths', 'moodle', 1),
+        7862400 => new lang_string('nummonths', 'moodle', 3),
+        15724800 => new lang_string('nummonths', 'moodle', 6),
+        0 => new lang_string('never')
+    );
+    $temp->add(new admin_setting_configselect(
+            'messagingdeletereadnotificationsdelay',
+            new lang_string('messagingdeletereadnotificationsdelay', 'admin'),
+            new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'),
+            604800,
+            $options)
+    );
+    $temp->add(new admin_setting_configselect(
+            'messagingdeleteallnotificationsdelay',
+            new lang_string('messagingdeleteallnotificationsdelay', 'admin'),
+            new lang_string('configmessagingdeleteallnotificationsdelay', 'admin'),
+            2620800,
+            $options)
+    );
+    $temp->add(new admin_setting_configcheckbox('messagingallowemailoverride',
+        new lang_string('messagingallowemailoverride', 'admin'),
+        new lang_string('configmessagingallowemailoverride', 'admin'),
+        0));
+    $ADMIN->add('messaging', $temp);
+    $ADMIN->add('messaging', new admin_page_managemessageoutputs());
+
+    // Notification outputs plugins.
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('message');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
+        /** @var \core\plugininfo\message $plugin */
+        $plugin->load_settings($ADMIN, 'messaging', $hassiteconfig);
+    }
+}
index 485eb01..925829c 100644 (file)
@@ -81,17 +81,6 @@ if ($hassiteconfig) {
         $plugin->load_settings($ADMIN, 'blocksettings', $hassiteconfig);
     }
 
-    // message outputs
-    $ADMIN->add('modules', new admin_category('messageoutputs', new lang_string('messageoutputs', 'message')));
-    $ADMIN->add('messageoutputs', new admin_page_managemessageoutputs());
-    $ADMIN->add('messageoutputs', new admin_page_defaultmessageoutputs());
-    $plugins = core_plugin_manager::instance()->get_plugins_of_type('message');
-    core_collator::asort_objects_by_property($plugins, 'displayname');
-    foreach ($plugins as $plugin) {
-        /** @var \core\plugininfo\message $plugin */
-        $plugin->load_settings($ADMIN, 'messageoutputs', $hassiteconfig);
-    }
-
     // authentication plugins
     $ADMIN->add('modules', new admin_category('authsettings', new lang_string('authentication', 'admin')));
 
index 96c7871..8c47a56 100644 (file)
@@ -176,6 +176,8 @@ $ADMIN->add('server', $temp);
 
 $ADMIN->add('server', new admin_externalpage('environment', new lang_string('environment','admin'), "$CFG->wwwroot/$CFG->admin/environment.php"));
 $ADMIN->add('server', new admin_externalpage('phpinfo', new lang_string('phpinfo'), "$CFG->wwwroot/$CFG->admin/phpinfo.php"));
+$ADMIN->add('server', new admin_externalpage('testoutgoingmailconf', new lang_string('testoutgoingmailconf', 'admin'),
+            new moodle_url("$CFG->wwwroot/$CFG->admin/testoutgoingmailconf.php"), 'moodle/site:config', true));
 
 
 // "performance" settingpage
@@ -326,6 +328,10 @@ $temp->add(new admin_setting_configtextarea('allowedemaildomains',
         new lang_string('allowedemaildomains', 'admin'),
         new lang_string('configallowedemaildomains', 'admin'),
         ''));
+$url = new moodle_url('/admin/testoutgoingmailconf.php');
+$link = html_writer::link($url, get_string('testoutgoingmailconf', 'admin'));
+$temp->add(new admin_setting_heading('testoutgoinmailc', new lang_string('testoutgoingmailconf', 'admin'),
+        new lang_string('testoutgoingmaildetail', 'admin', $link)));
 $temp->add(new admin_setting_heading('emaildoesnotfit', new lang_string('doesnotfit', 'admin'),
         new lang_string('doesnotfitdetail', 'admin')));
 $charsets = get_list_of_charsets();
index b559de3..de5f75b 100644 (file)
@@ -13,45 +13,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablewebservices', new lang_string('enablewebservices', 'admin'), new lang_string('configenablewebservices', 'admin'), 0));
 
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messaging', new lang_string('messaging', 'admin'), new lang_string('configmessaging','admin'), 1));
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallusers',
-        new lang_string('messagingallusers', 'admin'),
-        new lang_string('configmessagingallusers', 'admin'),
-        0)
-    );
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingdefaultpressenter',
-        new lang_string('messagingdefaultpressenter', 'admin'),
-        new lang_string('configmessagingdefaultpressenter', 'admin'),
-        1)
-    );
-
-    $options = array(
-        DAYSECS => new lang_string('secondstotime86400'),
-        WEEKSECS => new lang_string('secondstotime604800'),
-        2620800 => new lang_string('nummonths', 'moodle', 1),
-        7862400 => new lang_string('nummonths', 'moodle', 3),
-        15724800 => new lang_string('nummonths', 'moodle', 6),
-        0 => new lang_string('never')
-    );
-    $optionalsubsystems->add(new admin_setting_configselect(
-        'messagingdeletereadnotificationsdelay',
-        new lang_string('messagingdeletereadnotificationsdelay', 'admin'),
-        new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'),
-        604800,
-        $options)
-    );
-    $optionalsubsystems->add(new admin_setting_configselect(
-        'messagingdeleteallnotificationsdelay',
-        new lang_string('messagingdeleteallnotificationsdelay', 'admin'),
-        new lang_string('configmessagingdeleteallnotificationsdelay', 'admin'),
-        2620800,
-        $options)
-    );
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallowemailoverride', new lang_string('messagingallowemailoverride', 'admin'), new lang_string('configmessagingallowemailoverride','admin'), 0));
-
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablestats', new lang_string('enablestats', 'admin'), new lang_string('configenablestats', 'admin'), 0));
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablerssfeeds', new lang_string('enablerssfeeds', 'admin'), new lang_string('configenablerssfeeds', 'admin'), 0));
index 102b758..32d91c6 100644 (file)
@@ -33,6 +33,7 @@ $ADMIN->add('root', new admin_category('competencies', new lang_string('competen
 $ADMIN->add('root', new admin_category('badges', new lang_string('badges'), empty($CFG->enablebadges)));
 $ADMIN->add('root', new admin_category('location', new lang_string('location','admin')));
 $ADMIN->add('root', new admin_category('language', new lang_string('language')));
+$ADMIN->add('root', new admin_category('messaging', new lang_string('messagingcategory', 'admin')));
 $ADMIN->add('root', new admin_category('modules', new lang_string('plugins', 'admin')));
 $ADMIN->add('root', new admin_category('security', new lang_string('security','admin')));
 $ADMIN->add('root', new admin_category('appearance', new lang_string('appearance','admin')));
diff --git a/admin/testoutgoingmailconf.php b/admin/testoutgoingmailconf.php
new file mode 100644 (file)
index 0000000..ce5857f
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Test output mail configuration page
+ *
+ * @copyright 2019 Victor Deniz <victor@moodle.com>, based on Michael Milette <michael.milette@tngconsulting.ca> code
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+// This is an admin page.
+admin_externalpage_setup('testoutgoingmailconf');
+
+$headingtitle = get_string('testoutgoingmailconf', 'admin');
+$homeurl = new moodle_url('/admin/category.php', array('category' => 'email'));
+$returnurl = new moodle_url('/admin/testoutgoingconf.php');
+
+$form = new core_admin\form\testoutgoingmailconf_form(null, ['returnurl' => $returnurl]);
+if ($form->is_cancelled()) {
+    redirect($homeurl);
+}
+
+// Display the page.
+echo $OUTPUT->header();
+echo $OUTPUT->heading($headingtitle);
+
+$data = $form->get_data();
+if ($data) {
+    $emailuser = new stdClass();
+    $emailuser->email = $data->recipient;
+    $emailuser->id = -99;
+
+    $subject = get_string('testoutgoingmailconf_subject', 'admin', $SITE->fullname);
+    $messagetext = get_string('testoutgoingmailconf_message', 'admin');
+
+    // Manage Moodle debugging options.
+    $debuglevel = $CFG->debug;
+    $debugdisplay = $CFG->debugdisplay;
+    $debugsmtp = $CFG->debugsmtp;
+    $CFG->debugdisplay = true;
+    $CFG->debugsmtp = true;
+    $CFG->debug = 15;
+
+    // Send test email.
+    ob_start();
+    $success = email_to_user($emailuser, $USER, $subject, $messagetext);
+    $smtplog = ob_get_contents();
+    ob_end_clean();
+
+    // Restore Moodle debugging options.
+    $CFG->debug = $debuglevel;
+    $CFG->debugdisplay = $debugdisplay;
+    $CFG->debugsmtp = $debugsmtp;
+
+    if ($success) {
+        $msgparams = new stdClass();
+        $msgparams->fromemail = $USER->email;
+        $msgparams->toemail = $emailuser->email;
+        $msg = get_string('testoutgoingmailconf_sentmail', 'admin', $msgparams);
+        $notificationtype = 'notifysuccess';
+    } else {
+        $notificationtype = 'notifyproblem';
+        // No communication between Moodle and the SMTP server - no error output.
+        if (trim($smtplog) == false) {
+            $msg = get_string('testoutgoingmailconf_errorcommunications', 'admin');
+        } else {
+            $msg = $smtplog;
+        }
+    }
+
+    // Show result.
+    echo $OUTPUT->notification($msg, $notificationtype);
+}
+
+$form->display();
+echo $OUTPUT->footer();
index b2fd070..87a00b6 100644 (file)
@@ -58,12 +58,8 @@ class behat_admin extends behat_base {
             $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', [get_string('administrationsite')]);
 
             // Search by label.
-            $searchbox = $this->find_field(get_string('query', 'admin'));
-            $searchbox->setValue($label);
-            $submitsearch = $this->find('css', 'form input[type=submit][name=search]');
-            $submitsearch->press();
-
-            $this->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
+            $this->execute('behat_forms::i_set_the_field_to', [get_string('query', 'admin'), $label]);
+            $this->execute("behat_forms::press_button", get_string('search', 'admin'));
 
             // Admin settings does not use the same DOM structure than other moodle forms
             // but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
index c66254a..180805d 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 2f0b605..f39c577 100644 (file)
@@ -20,8 +20,8 @@
  * @copyright  2017 David Monllao
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_factory', 'core/modal_events'],
-    function($, Str, log, Notification, ModalFactory, ModalEvents) {
+define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/templates'],
+    function($, Str, log, Notification, ModalFactory, ModalEvents, Templates) {
 
     /**
      * List of actions that require confirmation and confirmation message.
@@ -94,10 +94,65 @@ define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_facto
                     modal.getRoot().on(ModalEvents.save, function() {
                         window.location.href = a.attr('href');
                     });
+                    modal.show();
+                    return modal;
+                }).fail(Notification.exception);
+            });
+        },
+
+        /**
+         * Displays a select-evaluation-mode choice.
+         *
+         * @param  {String}  actionId
+         * @param  {Boolean} trainedOnlyExternally
+         */
+        selectEvaluationMode: function(actionId, trainedOnlyExternally) {
+            $('[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',
+                        component: 'tool_analytics'
+                    }
+                ]);
+                var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});
+                var bodyPromise = Templates.render('tool_analytics/evaluation_mode_selection', {});
+
+                $.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.setBody(bodyPromise);
+
+                    modal.getRoot().on(ModalEvents.save, function() {
+                        var evaluationMode = $("input[name='evaluationmode']:checked").val();
+                        if (evaluationMode == 'trainedmodel') {
+                            a.attr('href', a.attr('href') + '&mode=trainedmodel');
+                        }
+                        window.location.href = a.attr('href');
+                        return;
+                    });
+
                     modal.show();
                     return modal;
                 }).fail(Notification.exception);
             });
         }
     };
-});
+});
\ No newline at end of file
index df59038..2e3a552 100644 (file)
@@ -41,6 +41,11 @@ class model_logs extends \table_sql {
      */
     protected $model = null;
 
+    /**
+     * @var string|false
+     */
+    protected $evaluationmode = false;
+
     /**
      * Sets up the table_log parameters.
      *
@@ -57,21 +62,32 @@ class model_logs extends \table_sql {
         $this->set_attribute('class', 'modellog generaltable generalbox');
         $this->set_attribute('aria-live', 'polite');
 
-        $this->define_columns(array('time', 'version', 'indicators', 'timesplitting', 'accuracy', 'info', 'usermodified'));
+        $this->define_columns(array('time', 'version', 'evaluationmode', 'indicators', 'timesplitting',
+            'accuracy', 'info', 'usermodified'));
         $this->define_headers(array(
             get_string('time'),
             get_string('version'),
+            get_string('evaluationmode', 'tool_analytics'),
             get_string('indicators', 'tool_analytics'),
             get_string('timesplittingmethod', 'analytics'),
             get_string('accuracy', 'tool_analytics'),
             get_string('info', 'tool_analytics'),
             get_string('fullnameuser'),
         ));
+
+        $evaluationmodehelp = new \help_icon('evaluationmode', 'tool_analytics');
+        $this->define_help_for_headers([null, null, $evaluationmodehelp, null, null, null, null, null]);
+
         $this->pageable(true);
         $this->collapsible(false);
         $this->sortable(false);
         $this->is_downloadable(false);
 
+        $this->evaluationmode = optional_param('evaluationmode', false, PARAM_ALPHANUM);
+        if ($this->evaluationmode && $this->evaluationmode != 'configuration' && $this->evaluationmode != 'trainedmodel') {
+            $this->evaluationmode = '';
+        }
+
         $this->define_baseurl($PAGE->url);
     }
 
@@ -86,6 +102,15 @@ class model_logs extends \table_sql {
         return userdate($log->version, $recenttimestr);
     }
 
+    /**
+     * Generate the evaluation mode column.
+     *
+     * @param \stdClass $log log data.
+     * @return string HTML for the evaluationmode column
+     */
+    public function col_evaluationmode($log) {
+        return get_string('evaluationmodecol' . $log->evaluationmode, 'tool_analytics');
+    }
     /**
      * Generate the time column.
      *
index b3a1c43..351dae4 100644 (file)
@@ -187,15 +187,21 @@ class models_list implements \renderable, \templatable {
 
             // Evaluate machine-learning-based models.
             if (!$onlycli && $model->get_indicators() && !$model->is_static()) {
+
+                // Extra is_trained call as trained_locally returns false if the model has not been trained yet.
+                $trainedonlyexternally = !$model->trained_locally() && $model->is_trained();
+
+                $actionid = 'evaluate-' . $model->get_id();
+                $PAGE->requires->js_call_amd('tool_analytics/model', 'selectEvaluationMode', [$actionid, $trainedonlyexternally]);
                 $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')),
-                    get_string('evaluate', 'tool_analytics'));
+                    get_string('evaluate', 'tool_analytics'), ['data-action-id' => $actionid]);
                 $actionsmenu->add($icon);
             }
 
             // Machine-learning-based models evaluation log.
-            if (!$model->is_static()) {
+            if (!$model->is_static() && $model->get_logs()) {
                 $urlparams['action'] = 'log';
                 $url = new \moodle_url('model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_analytics')),
index 25d182f..2836ca7 100644 (file)
@@ -35,6 +35,8 @@ Options:
 --non-interactive      Not interactive questions
 --timesplitting        Restrict the evaluation to 1 single time splitting method (Optional)
 --filter               Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional)
+--mode                 'configuration' or 'trainedmodel'. You can only use mode=trainedmodel when the trained" .
+    " model was imported" . "
 --reuse-prev-analysed  Reuse recently analysed courses instead of analysing the whole site. Set it to false while" .
     " coding indicators. Defaults to true (Optional)" . "
 -h, --help             Print out this help
@@ -50,6 +52,7 @@ list($options, $unrecognized) = cli_get_params(
         'modelid'               => false,
         'list'                  => false,
         'timesplitting'         => false,
+        'mode'                  => 'configuration',
         'reuse-prev-analysed'   => true,
         'non-interactive'       => false,
         'filter'                => false
@@ -64,16 +67,30 @@ if ($options['help']) {
     exit(0);
 }
 
-if ($options['list'] || $options['modelid'] === false) {
+if ($options['list']) {
     \tool_analytics\clihelper::list_models();
     exit(0);
 }
 
+if ($options['modelid'] === false) {
+    // All actions but --list require a modelid.
+    echo $help;
+    exit(0);
+}
+
 // Reformat them as an array.
 if ($options['filter'] !== false) {
     $options['filter'] = explode(',', $options['filter']);
 }
 
+if ($options['mode'] !== 'configuration' && $options['mode'] !== 'trainedmodel') {
+    cli_error('Error: The provided mode is not supported');
+}
+
+if ($options['mode'] == 'trainedmodel' && $options['timesplitting']) {
+    cli_error('Sorry, no time splitting method can be specified when using \'trainedmodel\' mode.');
+}
+
 // We need admin permissions.
 \core\session\manager::set_user(get_admin());
 
@@ -89,6 +106,7 @@ $analyseroptions = array(
     'filter' => $options['filter'],
     'timesplitting' => $options['timesplitting'],
     'reuseprevanalysed' => $options['reuse-prev-analysed'],
+    'mode' => $options['mode'],
 );
 // Evaluate its suitability to predict accurately.
 $results = $model->evaluate($analyseroptions);
index 47b40d0..354dc96 100644 (file)
@@ -53,11 +53,23 @@ $string['erroronlycli'] = 'Execution only allowed via command line';
 $string['errortrainingdataexport'] = 'The model training data could not be exported';
 $string['evaluate'] = 'Evaluate';
 $string['evaluatemodel'] = 'Evaluate model';
+$string['evaluationmode'] = 'Evaluation mode';
+$string['evaluationmode_help'] = 'There are two evaluation modes:
+
+* Trained model -  Site data is used as testing data to evaluate the accuracy of the trained model.
+* Configuration - Site data is split into training and testing data, to both train and test the accuracy of the model configuration.
+
+Trained model is only available if a trained model has been imported into the site, and has not yet been re-trained using site data.';
+$string['evaluationmodeinfo'] = 'This model has been imported into the site. You can either evaluate the performance of the model, or you can evaluate the performance of the model configuration using site data.';
+$string['evaluationmodetrainedmodel'] = 'Evaluate the trained model';
+$string['evaluationmodecoltrainedmodel'] = 'Trained model';
+$string['evaluationmodecolconfiguration'] = 'Configuration';
+$string['evaluationmodeconfiguration'] = 'Evaluate the model configuration';
 $string['evaluationinbatches'] = 'The site contents are calculated and stored in batches. The evaluation process may be stopped at any time. The next time it is run, it will continue from the point when it was stopped.';
 $string['exportmodel'] = 'Export configuration';
 $string['exporttrainingdata'] = 'Export training data';
-$string['getpredictionsresultscli'] = 'Results using {$a->name} (id: {$a->id}) course duration splitting';
-$string['getpredictionsresults'] = 'Results using {$a->name} course duration splitting';
+$string['getpredictionsresultscli'] = 'Results using {$a->name} (id: {$a->id}) time-splitting method';
+$string['getpredictionsresults'] = 'Results using {$a->name} time-splitting method';
 $string['extrainfo'] = 'Info';
 $string['generalerror'] = 'Evaluation error. Status code {$a}';
 $string['getpredictions'] = 'Get predictions';
@@ -104,7 +116,7 @@ $string['trainingprocessfinished'] = 'Training process finished';
 $string['trainingresults'] = 'Training results';
 $string['trainmodels'] = 'Train models';
 $string['versionnotsame'] = 'Imported file was from a different moodle version ({$a->importedversion}) than the current one ({$a->version})';
-$string['viewlog'] = 'Log';
+$string['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.';
 $string['privacy:metadata'] = 'The Analytic models plugin does not store any personal data.';
index 23d8454..a58b5d7 100644 (file)
@@ -119,7 +119,7 @@ switch ($action) {
             'id' => $model->get_id(),
             'trainedmodel' => $model->is_trained(),
             'indicators' => $model->get_potential_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);
@@ -169,7 +169,13 @@ switch ($action) {
         // Web interface is used by people who can not use CLI nor code stuff, always use
         // cached stuff as they will change the model through the web interface as well
         // which invalidates the previously analysed stuff.
-        $results = $model->evaluate(array('reuseprevanalysed' => true));
+        $options = ['reuseprevanalysed' => true];
+
+        $mode = optional_param('mode', false, PARAM_ALPHANUM);
+        if ($mode == 'trainedmodel') {
+            $options['mode'] = 'trainedmodel';
+        }
+        $results = $model->evaluate($options);
         $renderer = $PAGE->get_renderer('tool_analytics');
         echo $renderer->render_evaluate_results($results, $model->get_analyser()->get_logs());
         break;
diff --git a/admin/tool/analytics/templates/evaluation_mode_selection.mustache b/admin/tool/analytics/templates/evaluation_mode_selection.mustache
new file mode 100644 (file)
index 0000000..e9b32ce
--- /dev/null
@@ -0,0 +1,42 @@
+{{!
+    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_mode_selector
+
+    Evaluation mode selector.
+
+    The purpose of this template is to render the evaluation mode radio button.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div class="box mb-4">{{#str}} evaluationmodeinfo, tool_analytics {{/str}}</div>
+<div class="form-check">
+    <input class="form-check-input" type="radio" name="evaluationmode" id="id-mode-trainedmodel" value="trainedmodel" checked>
+    <label class="form-check-label" for="id-mode-trainedmodel">{{#str}} evaluationmodetrainedmodel, tool_analytics {{/str}}</label>
+</div>
+<div class="form-check">
+    <input class="form-check-input" type="radio" name="evaluationmode" id="id-mode-configuration" value="configuration">
+    <label class="form-check-label" for="id-mode-configuration">{{#str}} evaluationmodeconfiguration, tool_analytics {{/str}}</label>
+</div>
\ No newline at end of file
diff --git a/admin/tool/customlang/classes/output/renderer.php b/admin/tool/customlang/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..b183daf
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderer class for tool customlang
+ *
+ * @package     tool_customlang
+ * @category    output
+ * @copyright   2019 Bas Brands <bas@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_customlang\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Renderer for the customlang tool.
+ *
+ * @copyright 2019 Bas Brands <bas@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+    /**
+     * Defer to template.
+     *
+     * @param tool_customlang_translator $translator
+     * @return string Html for the translator
+     */
+    protected function render_tool_customlang_translator(\tool_customlang_translator $translator) {
+        $renderabletranslator = new translator($translator);
+        $templatevars = $renderabletranslator->export_for_template($this);
+        return $this->render_from_template('tool_customlang/translator', $templatevars);
+    }
+
+    /**
+     * Defer to template.
+     *
+     * @param tool_customlang_menu $menu
+     * @return string html the customlang menu buttons
+     */
+    protected function render_tool_customlang_menu(\tool_customlang_menu $menu) {
+        $output = '';
+        foreach ($menu->get_items() as $item) {
+            $output .= $this->single_button($item->url, $item->title, $item->method);
+        }
+        return $this->box($output, 'menu');
+    }
+}
diff --git a/admin/tool/customlang/classes/output/translator.php b/admin/tool/customlang/classes/output/translator.php
new file mode 100644 (file)
index 0000000..9b7ac7d
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * customlang specific renderers.
+ *
+ * @package   tool_customlang
+ * @copyright 2019 Moodle
+ * @author    Bas Brands
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_customlang\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use templatable;
+use renderer_base;
+use stdClass;
+
+/**
+ * Class containing data for customlang translator page
+ *
+ * @copyright  2019 Bas Brands
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class translator implements renderable, templatable {
+
+    /**
+     * @var tool_customlang_translator $translator object.
+     */
+    private $translator;
+
+    /**
+     * Construct this renderable.
+     *
+     * @param tool_customlang_translator $translator The translator object.
+     */
+    public function __construct(\tool_customlang_translator $translator) {
+        $this->translator = $translator;
+    }
+
+    /**
+     * Export the data.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+        $data = new stdClass();
+
+        $data->nostrings = $output->notification(get_string('nostringsfound', 'tool_customlang'));
+        $data->formurl = $this->translator->handler;
+        $data->currentpage = $this->translator->currentpage;
+        $data->sesskey = sesskey();
+        $data->strings = [];
+
+        if (!empty($this->translator->strings)) {
+            $data->hasstrings = true;
+            foreach ($this->translator->strings as $string) {
+                // Find strings that use placeholders.
+                if (preg_match('/\{\$a(->.+)?\}/', $string->master)) {
+                    $string->placeholderhelp = $output->help_icon('placeholder', 'tool_customlang',
+                            get_string('placeholderwarning', 'tool_customlang'));
+                }
+                if (!is_null($string->local) and $string->outdated) {
+                    $string->outdatedhelp = $output->help_icon('markinguptodate', 'tool_customlang');
+                    $string->checkupdated = true;
+                }
+                if ($string->original !== $string->master) {
+                    $string->showoriginalvsmaster = true;
+                }
+                $string->local = s($string->local);
+                $data->strings[] = $string;
+            }
+        }
+        return $data;
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/customlang/renderer.php b/admin/tool/customlang/renderer.php
deleted file mode 100644 (file)
index aea7fd6..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Output rendering of Language customization admin tool
- *
- * @package    tool
- * @subpackage customlang
- * @copyright  2010 David Mudrak <david@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Rendering methods for the tool widgets
- */
-class tool_customlang_renderer extends plugin_renderer_base {
-
-    /**
-     * Renders customlang tool menu
-     *
-     * @return string HTML
-     */
-    protected function render_tool_customlang_menu(tool_customlang_menu $menu) {
-        $output = '';
-        foreach ($menu->get_items() as $item) {
-            $output .= $this->single_button($item->url, $item->title, $item->method);
-        }
-        return $this->box($output, 'menu');
-    }
-
-    /**
-     * Renders customlang translation table
-     *
-     * @param tool_customlang_translator $translator
-     * @return string HTML
-     */
-    protected function render_tool_customlang_translator(tool_customlang_translator $translator) {
-        $output = '';
-
-        if (empty($translator->strings)) {
-            return $this->notification(get_string('nostringsfound', 'tool_customlang'));
-        }
-
-        $table = new html_table();
-        $table->id = 'translator';
-        $table->head = array(
-            get_string('headingcomponent', 'tool_customlang'),
-            get_string('headingstringid', 'tool_customlang'),
-            get_string('headingstandard', 'tool_customlang'),
-            get_string('headinglocal', 'tool_customlang'),
-        );
-
-        foreach ($translator->strings as $string) {
-            $cells = array();
-            // component name
-            $cells[0] = new html_table_cell($string->component);
-            $cells[0]->attributes['class'] = 'component';
-            // string identification code
-            $cells[1] = new html_table_cell(html_writer::tag('div', s($string->stringid), array('class' => 'stringid')));
-            $cells[1]->attributes['class'] = 'stringid';
-            // master translation of the string
-            $master = html_writer::tag('div', s($string->master), array('class' => 'preformatted'));
-            $minheight = strlen($string->master) / 200;
-            if (preg_match('/\{\$a(->.+)?\}/', $string->master)) {
-                $master .= html_writer::tag('div', $this->help_icon('placeholder', 'tool_customlang',
-                        get_string('placeholderwarning', 'tool_customlang')), array('class' => 'placeholderinfo'));
-            }
-            $cells[2] = new html_table_cell($master);
-            $cells[2]->attributes['class'] = 'standard master';
-            // local customization of the string
-            $textareaattributes = array('name'=>'cust['.$string->id.']', 'cols'=>40, 'rows'=>3);
-            if ($minheight>1) {
-               $textareaattributes['style'] = 'min-height:' . (int) 4*$minheight . 'em;';
-            }
-            $textarea = html_writer::tag('textarea', s($string->local), $textareaattributes);
-            $cells[3] = new html_table_cell($textarea);
-            if (!is_null($string->local) and $string->outdated) {
-                $mark  = html_writer::empty_tag('input', array('type' => 'checkbox', 'id' => 'update_' . $string->id,
-                                                               'name' => 'updates[]', 'value' => $string->id));
-                $help  = $this->help_icon('markinguptodate', 'tool_customlang');
-                $mark .= html_writer::tag('label', get_string('markuptodate', 'tool_customlang') . $help,
-                                          array('for' => 'update_' . $string->id));
-                $mark  = html_writer::tag('div', $mark, array('class' => 'uptodatewrapper'));
-            } else {
-                $mark  = '';
-            }
-            $cells[3] = new html_table_cell($textarea."\n".$mark);
-            $cells[3]->attributes['class'] = 'local';
-            $cells[3]->id = 'id_'.$string->id;
-            if (!is_null($string->local)) {
-                $cells[3]->attributes['class'] .= ' customized';
-            }
-            if ($string->outdated) {
-                $cells[3]->attributes['class'] .= ' outdated';
-            }
-            if ($string->modified) {
-                $cells[3]->attributes['class'] .= ' modified';
-            }
-
-            if ($string->original !== $string->master) {
-                $cells[0]->rowspan = $cells[1]->rowspan = $cells[3]->rowspan = 2;
-            }
-
-            $row = new html_table_row($cells);
-            $table->data[] = $row;
-
-            if ($string->original !== $string->master) {
-                $cells = array();
-                // original of the string
-                $cells[2] = new html_table_cell(html_writer::tag('div', s($string->original), array('class' => 'preformatted')));
-                $cells[2]->attributes['class'] = 'standard original';
-                $row = new html_table_row($cells);
-                $table->data[] = $row;
-            }
-        }
-
-        $output .= html_writer::start_tag('form', array('method'=>'post', 'action'=>$translator->handler->out()));
-        $output .= html_writer::start_tag('div');
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'translatorsubmitted', 'value'=>1));
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'p', 'value'=>$translator->currentpage));
-        $save1   = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecontinue',
-            'value' => get_string('savecontinue', 'tool_customlang'), 'class' => 'btn btn-secondary'));
-        $save2   = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecheckin',
-            'value' => get_string('savecheckin', 'tool_customlang'), 'class' => 'btn btn-secondary'));
-        $output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
-        $output .= html_writer::table($table);
-        $output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
-        $output .= html_writer::end_tag('div');
-        $output .= html_writer::end_tag('form');
-
-        return $output;
-    }
-}
diff --git a/admin/tool/customlang/styles.css b/admin/tool/customlang/styles.css
deleted file mode 100644 (file)
index 9f9fa98..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-.path-admin-tool-customlang .langselectorbox,
-.path-admin-tool-customlang fieldset.buttonsbar,
-.path-admin-tool-customlang .menu {
-    margin: 5px auto;
-    text-align: center;
-}
-
-.path-admin-tool-customlang .menu .singlebutton,
-.path-admin-tool-customlang .menu .singlebutton form,
-.path-admin-tool-customlang .menu .singlebutton form div {
-    display: inline;
-}
-
-.path-admin-tool-customlang .mform.filterform {
-    width: 70%;
-    margin-left: auto;
-    margin-right: auto;
-}
-
-.path-admin-tool-customlang .mform.filterform .fitem .fitemtitle {
-    width: 30%;
-}
-
-.path-admin-tool-customlang .mform.filterform .fitem .felement {
-    width: 60%;
-    margin-left: 31%;
-}
-
-.path-admin-tool-customlang #translator {
-    width: 100%;
-}
-
-.path-admin-tool-customlang #translator .standard,
-.path-admin-tool-customlang #translator .local {
-    min-width: 35%;
-}
-
-.path-admin-tool-customlang #translator .customized {
-    background-color: #e7f1c3;
-}
-
-.path-admin-tool-customlang #translator .customized.outdated {
-    background-color: #f3f2aa;
-}
-
-.path-admin-tool-customlang #translator .modified {
-    background-color: #ffd3d9;
-}
-
-.path-admin-tool-customlang #translator .customized.modified {
-    background-color: #d2ebff;
-}
-
-.path-admin-tool-customlang #translator textarea {
-    width: 100%;
-    min-height: 4em;
-}
-
-.path-admin-tool-customlang #translator .placeholderinfo {
-    text-align: center;
-    border: 1px dotted #ddd;
-    background-color: #f6f6f6;
-    margin-top: 0.5em;
-}
-
-#page-admin-tool-customlang-index .continuebutton {
-    margin-top: 1em;
-}
-
-.path-admin-tool-customlang #translator .standard.master.cell.c2 {
-    word-break: break-all;
-}
diff --git a/admin/tool/customlang/templates/translator.mustache b/admin/tool/customlang/templates/translator.mustache
new file mode 100644 (file)
index 0000000..8864f83
--- /dev/null
@@ -0,0 +1,150 @@
+{{!
+    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_customlang/translator
+
+    Template for the custom language translator page.
+
+    Classes required for JS:
+    -
+
+    Data attributes required for JS:
+    -
+
+    Context variables required for this template:
+    * strings
+
+    Example context (json):
+    {
+        "hasstrings": true,
+        "formurl": "admin/tool/customlang/edit.php?lng=en",
+        "currentpage": 0,
+        "sesskey" : "AZyeeQgmcs",
+        "strings": [
+            {
+                "id": 11,
+                "component": "core",
+                "componentid": 1,
+                "stringid": "course",
+                "original": "Course",
+                "master": "Cursus",
+                "local": "Hoofdstuk",
+                "outdated": 0,
+                "modified": 1
+            }
+        ]
+    }
+}}
+
+{{^hasstrings}}
+    {{{ nostrings }}}
+{{/hasstrings}}
+{{#hasstrings}}
+<form method="post" action="{{{formurl}}}">
+    <input type="hidden" name="translatorsubmitted" value="1">
+    <input type="hidden" name="sesskey" value="{{{ sesskey }}}">
+    <input type="hidden" name="p" value="{{ currentpage }}">
+
+    <fieldset class="m-a-1 m-3">
+        <button type="submit" name="savecontinue" class="btn btn-secondary">
+            {{#str}}savecontinue, tool_customlang{{/str}}
+        </button>
+        <button type="submit" name="savecheckin" class="btn btn-secondary">
+            {{#str}}savecheckin, tool_customlang{{/str}}
+        </button>
+    </fieldset>
+
+    <div class="list-group">
+        <div class="container-fluid d-none d-md-block list-group-item border-bottom-0">
+            <div class="row-fluid">
+                <div class="col-sm-4 col-md-2 span2">
+                    <strong>{{#str}}headingcomponent, tool_customlang{{/str}}</strong>
+                </div>
+                <div class="col-sm-4 col-md-2 span2">
+                    <strong>{{#str}}headingstringid, tool_customlang{{/str}}</strong>
+                </div>
+                <div class="col-sm-4 col-md-2 span2">
+                    <strong>{{#str}}headingstandard, tool_customlang{{/str}}</strong>
+                </div>
+                <div class="col-sm-12 col-md-6 span6">
+                    <span class="p-l-1 pl-3">
+                        <strong>{{#str}}headinglocal, tool_customlang{{/str}}</strong>
+                    </span>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="list-group">
+    {{#strings}}
+        <div class="container-fluid list-group-item
+                {{#local}}list-group-item-info{{/local}}
+                {{#outdated}}list-group-item-warning{{/outdated}}
+                {{#modified}}list-group-item-info{{/modified}}"
+            >
+            <div class="row-fluid ">
+                <div class="col-sm-4 col-md-2 span2">
+                    <div class="d-md-none">
+                        <strong>{{#str}}headingcomponent, tool_customlang{{/str}}</strong>
+                    </div>
+                    {{{ component }}}
+                </div>
+                <div class="col-sm-4 col-md-2 span2 text-break">
+                    <div class="d-md-none">
+                        <strong>{{#str}}headingstringid, tool_customlang{{/str}}</strong>
+                    </div>
+                    {{{ stringid }}}
+                </div>
+                <div class="col-sm-4 col-md-2 span2">
+                    <div class="d-md-none">
+                        <strong>{{#str}}headingstandard, tool_customlang{{/str}}</strong>
+                    </div>
+                    {{{ master }}}
+                    <div class="info">
+                        {{{ placeholderhelp }}}
+                        {{{ outdatedhelp}}}
+                    </div>
+                    {{#showoriginalvsmaster}}
+                    <div class="alert-secondary mt-3 m-t-1">
+                        {{{ original }}}
+                    </div>
+                    {{/showoriginalvsmaster}}
+                </div>
+                <div class="col-sm-12 col-md-6 mt-sm-3 mt-md-0 span6">
+                    <div class="d-md-none">
+                        <strong>{{#str}}headinglocal, tool_customlang{{/str}}</strong>
+                    </div>
+                    <div class="py-2 py-md-0 px-md-3">
+                        <textarea class="form-control w-100 border-box" name="cust[{{id}}]" cols="40" rows="3">{{{ local }}}</textarea>
+
+                        {{#checkupdated}}
+                        <div class="uptodatewrapper">
+                            <div class="form-check">
+                                <input id="update_{{id}}" class="form-check-input" name="updates[]" type="checkbox" value="{{id}}">
+                                <label for="update_{{id}}" class="form-check-label">{{#str}}markuptodate, tool_customlang{{/str}}</label>
+                                {{{ outdatedhelp }}}
+                            </div>
+                        </div>
+                        {{/checkupdated}}
+                    </div>
+                </div>
+            </div>
+        </div>
+    {{/strings}}
+    </div>
+</form>
+{{/hasstrings}}
diff --git a/admin/tool/dataprivacy/tests/coverage.php b/admin/tool/dataprivacy/tests/coverage.php
new file mode 100644 (file)
index 0000000..9af4b87
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Coverage information for the tool_dataprivacy plugin.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Coverage information for the tool_dataprivacy plugin.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+return new class extends phpunit_coverage_info {
+    /** @var array The list of folders relative to the plugin root to whitelist in coverage generation. */
+    protected $whitelistfolders = [
+        'classes',
+    ];
+
+    /** @var array The list of files relative to the plugin root to whitelist in coverage generation. */
+    protected $whitelistfiles = [];
+
+    /** @var array The list of folders relative to the plugin root to excludelist in coverage generation. */
+    protected $excludelistfolders = [
+    ];
+
+    /** @var array The list of files relative to the plugin root to excludelist in coverage generation. */
+    protected $excludelistfiles = [];
+};
index 6a46264..1795635 100644 (file)
@@ -26,6 +26,7 @@ namespace tool_mobile;
 defined('MOODLE_INTERNAL') || die();
 
 require_once("$CFG->libdir/externallib.php");
+require_once("$CFG->dirroot/webservice/lib.php");
 
 use external_api;
 use external_files;
@@ -460,4 +461,119 @@ class external extends external_api {
             )
         );
     }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.7
+     */
+    public static function call_external_functions_parameters() {
+        return new external_function_parameters([
+            'requests' => new external_multiple_structure(
+                new external_single_structure([
+                    'function' => new external_value(PARAM_ALPHANUMEXT, 'Function name'),
+                    'arguments' => new external_value(PARAM_RAW, 'JSON-encoded object with named arguments', VALUE_DEFAULT, '{}'),
+                    'settingraw' => new external_value(PARAM_BOOL, 'Return raw text', VALUE_DEFAULT, false),
+                    'settingfilter' => new external_value(PARAM_BOOL, 'Filter text', VALUE_DEFAULT, false),
+                    'settingfileurl' => new external_value(PARAM_BOOL, 'Rewrite plugin file URLs', VALUE_DEFAULT, true),
+                    'settinglang' => new external_value(PARAM_LANG, 'Session language', VALUE_DEFAULT, ''),
+                ])
+            )
+        ]);
+    }
+
+    /**
+     * Call multiple external functions and return all responses.
+     *
+     * @param array $requests List of requests.
+     * @return array Responses.
+     * @since Moodle 3.7
+     */
+    public static function call_external_functions($requests) {
+        global $SESSION;
+
+        $params = self::validate_parameters(self::call_external_functions_parameters(), ['requests' => $requests]);
+
+        // We need to check if the functions being called are included in the service of the current token.
+        // This function only works when using mobile services via REST (this is intended).
+        $webservicemanager = new \webservice;
+        $token = $webservicemanager->get_user_ws_token(required_param('wstoken', PARAM_ALPHANUM));
+
+        $settings = \external_settings::get_instance();
+        $defaultlang = current_language();
+        $responses = [];
+
+        foreach ($params['requests'] as $request) {
+            // Some external functions modify _GET or $_POST data, we need to restore the original data after each call.
+            $originalget = fullclone($_GET);
+            $originalpost = fullclone($_POST);
+
+            // Set external settings and language.
+            $settings->set_raw($request['settingraw']);
+            $settings->set_filter($request['settingfilter']);
+            $settings->set_fileurl($request['settingfileurl']);
+            $settings->set_lang($request['settinglang']);
+            $SESSION->lang = $request['settinglang'] ?: $defaultlang;
+
+            // Parse arguments to an array, validation is done in external_api::call_external_function.
+            $args = @json_decode($request['arguments'], true);
+            if (!is_array($args)) {
+                $args = [];
+            }
+
+            if ($webservicemanager->service_function_exists($request['function'], $token->externalserviceid)) {
+                $response = external_api::call_external_function($request['function'], $args, false);
+            } else {
+                // Function not included in the service, return an access exception.
+                $response = [
+                    'error' => true,
+                    'exception' => [
+                        'errorcode' => 'accessexception',
+                        'module' => 'webservice'
+                    ]
+                ];
+                if (debugging('', DEBUG_DEVELOPER)) {
+                    $response['exception']['debuginfo'] = 'Access to the function is not allowed.';
+                }
+            }
+
+            if (isset($response['data'])) {
+                $response['data'] = json_encode($response['data']);
+            }
+            if (isset($response['exception'])) {
+                $response['exception'] = json_encode($response['exception']);
+            }
+            $responses[] = $response;
+
+            // Restore original $_GET and $_POST.
+            $_GET = $originalget;
+            $_POST = $originalpost;
+
+            if ($response['error']) {
+                // Do not process the remaining requests.
+                break;
+            }
+        }
+
+        return ['responses' => $responses];
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_single_structure
+     * @since Moodle 3.7
+     */
+    public static function call_external_functions_returns() {
+        return new external_function_parameters([
+            'responses' => new external_multiple_structure(
+                new external_single_structure([
+                    'error' => new external_value(PARAM_BOOL, 'Whether an exception was thrown.'),
+                    'data' => new external_value(PARAM_RAW, 'JSON-encoded response data', VALUE_OPTIONAL),
+                    'exception' => new external_value(PARAM_RAW, 'JSON-encoed exception info', VALUE_OPTIONAL),
+                ])
+             )
+        ]);
+    }
 }
index 5e329c6..0b50d68 100644 (file)
@@ -61,6 +61,7 @@ $functions = array(
         'type'        => 'write',
         'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+
     'tool_mobile_get_content' => array(
         'classname'   => 'tool_mobile\external',
         'methodname'  => 'get_content',
@@ -68,5 +69,13 @@ $functions = array(
         'type'        => 'read',
         'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+
+    'tool_mobile_call_external_functions' => array(
+        'classname'   => 'tool_mobile\external',
+        'methodname'  => 'call_external_functions',
+        'description' => 'Call multiple external functions and return all responses.',
+        'type'        => 'write',
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
 );
 
index f2d08ac..a373878 100644 (file)
@@ -86,6 +86,7 @@ $string['mobilesettings'] = 'Mobile settings';
 $string['offlineuse'] = 'Offline use';
 $string['pluginname'] = 'Moodle app tools';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
+$string['readingthisemailgettheapp'] = 'Reading this in your e-mail? <a href="{$a}">Download the mobile app and receive notifications on your mobile devices</a>.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
 $string['setuplink'] = 'App download page';
index 0ecfeff..74b3c0b 100644 (file)
@@ -66,6 +66,12 @@ function tool_mobile_create_app_download_url() {
     }
 
     $downloadurl = new moodle_url($mobilesettings->setuplink);
+
+    // Do not update the URL if it is a custom one (we may break it completely).
+    if ($mobilesettings->setuplink != 'https://download.moodle.org/mobile') {
+        return $downloadurl;
+    }
+
     $downloadurl->param('version', $CFG->version);
     $downloadurl->param('lang', current_language());
 
@@ -80,6 +86,25 @@ function tool_mobile_create_app_download_url() {
     return $downloadurl;
 }
 
+/**
+ * Checks if the given user has a mobile token (has used recently the app).
+ *
+ * @param  int $userid the user to check
+ * @return bool        true if the user has a token, false otherwise.
+ */
+function tool_mobile_user_has_token($userid) {
+    global $DB;
+
+    $sql = "SELECT 1
+              FROM {external_tokens} t, {external_services} s
+             WHERE t.externalserviceid = s.id
+               AND s.enabled = 1
+               AND s.shortname IN ('moodle_mobile_app', 'local_mobile')
+               AND t.userid = ?";
+
+    return $DB->record_exists_sql($sql, [$userid]);
+}
+
 /**
  * User profile page callback.
  *
@@ -91,7 +116,7 @@ function tool_mobile_create_app_download_url() {
  * @return void Return if the mobile web services setting is disabled or if not the current user.
  */
 function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree, $user, $iscurrentuser) {
-    global $CFG, $DB;
+    global $CFG;
 
     if (empty($CFG->enablemobilewebservice)) {
         return;
@@ -105,13 +130,7 @@ function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree
         return;
     }
 
-    $sql = "SELECT 1
-              FROM {external_tokens} t, {external_services} s
-             WHERE t.externalserviceid = s.id
-               AND s.enabled = 1
-               AND s.shortname IN ('moodle_mobile_app', 'local_mobile')
-               AND t.userid = ?";
-    $userhastoken = $DB->record_exists_sql($sql, [$user->id]);
+    $userhastoken = tool_mobile_user_has_token($user->id);
 
     $mobilecategory = new core_user\output\myprofile\category('mobile', get_string('mobileapp', 'tool_mobile'),
             'loginactivity');
@@ -141,3 +160,42 @@ function tool_mobile_standard_footer_html() {
     }
     return $output;
 }
+
+/**
+ * Callback to be able to change a message/notification data per processor.
+ *
+ * @param  str $procname    processor name
+ * @param  stdClass $data   message or notification data
+ */
+function tool_mobile_pre_processor_message_send($procname, $data) {
+    global $CFG;
+
+    if (empty($CFG->enablemobilewebservice)) {
+        return;
+    }
+
+    if (empty($data->userto)) {
+        return;
+    }
+
+    // Only hack email.
+    if ($procname == 'email') {
+
+        // Send a message only when there is an HTML version of the email, mobile services are enabled,
+        // the user receiving the message has not used the app and there is an app download URL set.
+        if (empty($data->fullmessagehtml)) {
+            return;
+        }
+
+        if (!$url = tool_mobile_create_app_download_url()) {
+            return;
+        }
+
+        $userto = is_object($data->userto) ? $data->userto->id : $data->userto;
+        if (tool_mobile_user_has_token($userto)) {
+            return;
+        }
+
+        $data->fullmessagehtml .= html_writer::tag('p', get_string('readingthisemailgettheapp', 'tool_mobile', $url->out()));
+    }
+}
index f5ee434..4d9a07d 100644 (file)
@@ -94,4 +94,80 @@ class tool_mobile_api_testcase extends externallib_advanced_testcase {
             $this->assertTrue(in_array($issue[0], $expectedissues));
         }
     }
+
+    /**
+     * Test pre_processor_message_send callback.
+     */
+    public function test_pre_processor_message_send_callback() {
+        global $DB, $CFG;
+        require_once($CFG->libdir . '/externallib.php');
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        // Enable mobile services and required configuration.
+        $CFG->enablewebservices = 1;
+        $CFG->enablemobilewebservice = 1;
+        $mobileappdownloadpage = 'htt://mobileappdownloadpage';
+        set_config('setuplink', $mobileappdownloadpage, 'tool_mobile');
+
+        $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
+        $user2 = $this->getDataGenerator()->create_user();
+        set_config('allowedemaildomains', 'example.com');
+
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
+
+        // Extra content for all types of messages.
+        $message = new \core\message\message();
+        $message->courseid          = 1;
+        $message->component         = 'moodle';
+        $message->name              = 'instantmessage';
+        $message->userfrom          = $user1;
+        $message->userto            = $user2;
+        $message->subject           = 'message subject 1';
+        $message->fullmessage       = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml   = '<p>message body</p>';
+        $message->smallmessage      = 'small message';
+        $message->notification      = '0';
+        $content = array('*' => array('header' => ' test ', 'footer' => ' test '));
+        $message->set_additional_content('email', $content);
+
+        $sink = $this->redirectEmails();
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+
+        // Check we got the promotion text.
+        $this->assertContains($mobileappdownloadpage, quoted_printable_decode($email->body));
+        $sink->clear();
+
+        // Disable mobile so we don't get mobile promotions.
+        $CFG->enablemobilewebservice = 0;
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+        // Check we don't get the promotion text.
+        $this->assertNotContains($mobileappdownloadpage, quoted_printable_decode($email->body));
+        $sink->clear();
+
+        // Enable mobile again and set current user mobile token so we don't get mobile promotions.
+        $CFG->enablemobilewebservice = 1;
+        $user3 = $this->getDataGenerator()->create_user();
+        $this->setUser($user3);
+        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
+        $token = external_generate_token_for_current_user($service);
+
+        $message->userto = $user3;
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+        // Check we don't get the promotion text.
+        $this->assertNotContains($mobileappdownloadpage, quoted_printable_decode($email->body));
+        $sink->clear();
+        $sink->close();
+    }
 }
index 00db97e..4045ab1 100644 (file)
@@ -30,6 +30,7 @@ global $CFG;
 
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 require_once($CFG->dirroot . '/admin/tool/mobile/tests/fixtures/output/mobile.php');
+require_once($CFG->dirroot . '/webservice/lib.php');
 
 use tool_mobile\external;
 use tool_mobile\api;
@@ -358,4 +359,147 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $this->expectException('moodle_exception');
         $result = external::get_content('tool_blahblahblah', 'test_view');
     }
+
+    public function test_call_external_functions() {
+        global $SESSION;
+
+        $this->resetAfterTest(true);
+
+        $category = self::getDataGenerator()->create_category(array('name' => 'Category 1'));
+        $course = self::getDataGenerator()->create_course([
+            'category' => $category->id,
+            'shortname' => 'c1',
+            'summary' => '<span lang="en" class="multilang">Course summary</span>'
+                . '<span lang="eo" class="multilang">Kurso resumo</span>'
+                . '@@PLUGINFILE@@/filename.txt'
+                . '<!-- Comment stripped when formatting text -->',
+            'summaryformat' => FORMAT_MOODLE
+        ]);
+        $user1 = self::getDataGenerator()->create_user(['username' => 'user1', 'lastaccess' => time()]);
+        $user2 = self::getDataGenerator()->create_user(['username' => 'user2', 'lastaccess' => time()]);
+
+        self::setUser($user1);
+
+        // Setup WS token.
+        $webservicemanager = new \webservice;
+        $service = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE);
+        $token = external_generate_token_for_current_user($service);
+        $_POST['wstoken'] = $token->token;
+
+        // Workaround for external_api::call_external_function requiring sesskey.
+        $_POST['sesskey'] = sesskey();
+
+        // Call some functions.
+
+        $requests = [
+            [
+                'function' => 'core_course_get_courses_by_field',
+                'arguments' => json_encode(['field' => 'id', 'value' => $course->id])
+            ],
+            [
+                'function' => 'core_user_get_users_by_field',
+                'arguments' => json_encode(['field' => 'id', 'values' => [$user1->id]])
+            ],
+            [
+                'function' => 'core_user_get_user_preferences',
+                'arguments' => json_encode(['name' => 'some_setting', 'userid' => $user2->id])
+            ],
+            [
+                'function' => 'core_course_get_courses_by_field',
+                'arguments' => json_encode(['field' => 'shortname', 'value' => $course->shortname])
+            ],
+        ];
+        $result = external::call_external_functions($requests);
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(external::call_external_functions_returns(), $result);
+
+        // Only 3 responses, the 4th request is not executed because the 3rd throws an exception.
+        $this->assertCount(3, $result['responses']);
+
+        $this->assertFalse($result['responses'][0]['error']);
+        $coursedata = external_api::clean_returnvalue(
+            core_course_external::get_courses_by_field_returns(),
+            core_course_external::get_courses_by_field('id', $course->id));
+         $this->assertEquals(json_encode($coursedata), $result['responses'][0]['data']);
+
+        $this->assertFalse($result['responses'][1]['error']);
+        $userdata = external_api::clean_returnvalue(
+            core_user_external::get_users_by_field_returns(),
+            core_user_external::get_users_by_field('id', [$user1->id]));
+        $this->assertEquals(json_encode($userdata), $result['responses'][1]['data']);
+
+        $this->assertTrue($result['responses'][2]['error']);
+        $exception = json_decode($result['responses'][2]['exception'], true);
+        $this->assertEquals('nopermissions', $exception['errorcode']);
+
+        // Call a function not included in the external service.
+
+        $_POST['wstoken'] = $token->token;
+        $functions = $webservicemanager->get_not_associated_external_functions($service->id);
+        $requests = [['function' => current($functions)->name]];
+        $result = external::call_external_functions($requests);
+
+        $this->assertTrue($result['responses'][0]['error']);
+        $exception = json_decode($result['responses'][0]['exception'], true);
+        $this->assertEquals('accessexception', $exception['errorcode']);
+        $this->assertEquals('webservice', $exception['module']);
+
+        // Call a function with different external settings.
+
+        filter_set_global_state('multilang', TEXTFILTER_ON);
+        $_POST['wstoken'] = $token->token;
+        $SESSION->lang = 'eo'; // Change default language, so we can test changing it to "en".
+        $requests = [
+            [
+                'function' => 'core_course_get_courses_by_field',
+                'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
+            ],
+            [
+                'function' => 'core_course_get_courses_by_field',
+                'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
+                'settingraw' => '1'
+            ],
+            [
+                'function' => 'core_course_get_courses_by_field',
+                'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
+                'settingraw' => '1',
+                'settingfileurl' => '0'
+            ],
+            [
+                'function' => 'core_course_get_courses_by_field',
+                'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
+                'settingfilter' => '1',
+                'settinglang' => 'en'
+            ],
+        ];
+        $result = external::call_external_functions($requests);
+
+        $this->assertCount(4, $result['responses']);
+
+        $context = \context_course::instance($course->id);
+        $pluginfile = 'webservice/pluginfile.php';
+
+        $this->assertFalse($result['responses'][0]['error']);
+        $data = json_decode($result['responses'][0]['data']);
+        $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
+        $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => false]);
+        $this->assertEquals($expected, $data->courses[0]->summary);
+
+        $this->assertFalse($result['responses'][1]['error']);
+        $data = json_decode($result['responses'][1]['data']);
+        $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
+        $this->assertEquals($expected, $data->courses[0]->summary);
+
+        $this->assertFalse($result['responses'][2]['error']);
+        $data = json_decode($result['responses'][2]['data']);
+        $this->assertEquals($course->summary, $data->courses[0]->summary);
+
+        $this->assertFalse($result['responses'][3]['error']);
+        $data = json_decode($result['responses'][3]['data']);
+        $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
+        $SESSION->lang = 'en'; // We expect filtered text in english.
+        $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => true]);
+        $this->assertEquals($expected, $data->courses[0]->summary);
+    }
 }
index 2149382..ec720c3 100644 (file)
@@ -1,6 +1,10 @@
 This files describes changes in tool_mobile code.
 Information provided here is intended especially for developers.
 
+=== 3.7 ===
+
+ * New external function tool_mobile::tool_mobile_call_external_function allows calling multiple external functions and returns all responses.
+
 === 3.5 ===
 
  * External function tool_mobile::tool_mobile_get_plugins_supporting_mobile now returns additional plugins information required by
index bc7dc20..766dfb2 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2019021100; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019021101; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2018112800; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
 $plugin->dependencies = array(
index 9a62976..8681715 100644 (file)
@@ -301,6 +301,8 @@ class page_agreedocs implements renderable, templatable {
                 redirect(new moodle_url('/admin/tool/policy/view.php', $urlparams));
             }
         } else {
+            // Update the policyagreed for the user to avoid infinite loop because there are no policies to-be-accepted.
+            api::update_policyagreed($userid);
             $this->redirect_to_previous_url();
         }
     }
index b35a20e..85df22d 100644 (file)
@@ -82,7 +82,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I press "Next"
     And I set the field "I agree to the This site policy" to "1"
     And I press "Next"
-    And I should not see "Next"
+    And I should see "Course overview"
     And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
     And I click on "Accept This site policy" "link" in the "User One" "table_row"
@@ -292,3 +292,23 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     When I press "Give consent"
     Then "Accepted on user's behalf" "text" should exist in the "User One" "table_row"
     And "Accepted on user's behalf" "text" should exist in the "User Two" "table_row"
+
+  Scenario: View acceptances made by users on their own after inactivating a policy
+    Given I log in as "user1"
+    And I should see "This site policy"
+    And I should not see "Course overview"
+    And I press "Next"
+    And I set the field "I agree to the This site policy" to "1"
+    And I press "Next"
+    And I should see "Course overview"
+    And I log out
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Manage policies" in site administration
+    And I click on "Actions" "link_or_button" in the "This privacy policy" "table_row"
+    And I click on "Set status to \"Active\"" "link" in the "This privacy policy" "table_row"
+    And I press "Continue"
+    And I click on "Set status to \"Inactive\"" "link" in the "This privacy policy" "table_row"
+    And I press "Continue"
+    And I log out
+    When I log in as "user1"
+    Then I should see "Course overview"
index fb5d779..d58d1a7 100644 (file)
@@ -119,7 +119,7 @@ class course_bin extends base_bin {
             $cm->id,
             \backup::FORMAT_MOODLE,
             \backup::INTERACTIVE_NO,
-            \backup::MODE_GENERAL,
+            \backup::MODE_AUTOMATED,
             $user->id
         );
         $controller->execute_plan();
@@ -224,7 +224,7 @@ class course_bin extends base_bin {
             $tempdir,
             $this->_courseid,
             \backup::INTERACTIVE_NO,
-            \backup::MODE_GENERAL,
+            \backup::MODE_AUTOMATED,
             $user->id,
             \backup::TARGET_EXISTING_ADDING
         );
@@ -344,4 +344,4 @@ class course_bin extends base_bin {
         $context = \context_course::instance($this->_courseid);
         return has_capability('tool/recyclebin:deleteitems', $context);
     }
-}
+}
\ No newline at end of file
index 9a4e0c3..9d99326 100644 (file)
@@ -55,7 +55,7 @@ class tool_recyclebin_course_bin_tests extends advanced_testcase {
 
         $this->course = $this->getDataGenerator()->create_course();
         $this->quiz = $this->getDataGenerator()->get_plugin_generator('mod_quiz')->create_instance(array(
-            'course' => $this->course->id
+            'course' => $this->course->id, 'grade' => 100.0, 'sumgrades' => 1
         ));
     }
 
@@ -173,4 +173,99 @@ class tool_recyclebin_course_bin_tests extends advanced_testcase {
         $deletedbook = reset($items);
         $this->assertEquals($book->name, $deletedbook->name);
     }
+
+    /**
+     * Tests that user data is restored when module is restored.
+     */
+    public function test_coursemodule_restore_with_userdata() {
+        $student = $this->getDataGenerator()->create_and_enrol($this->course, 'student');
+        $this->setUser($student);
+
+        set_config('backup_auto_users', true, 'backup');
+        $this->create_quiz_attempt($this->quiz, $student);
+
+        // Delete quiz.
+        $cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
+        course_delete_module($cm->id);
+        phpunit_util::run_all_adhoc_tasks();
+        $quizzes = get_coursemodules_in_course('quiz', $this->course->id);
+        $this->assertEquals(0, count($quizzes));
+
+        // Restore quiz.
+        $recyclebin = new \tool_recyclebin\course_bin($this->course->id);
+        foreach ($recyclebin->get_items() as $item) {
+            $recyclebin->restore_item($item);
+        }
+        $quizzes = get_coursemodules_in_course('quiz', $this->course->id);
+        $this->assertEquals(1, count($quizzes));
+        $cm = array_pop($quizzes);
+
+        // Check if user quiz attempt data is restored.
+        $attempts = quiz_get_user_attempts($cm->instance, $student->id);
+        $this->assertEquals(1, count($attempts));
+        $attempt = array_pop($attempts);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $this->assertEquals($student->id, $attemptobj->get_userid());
+        $this->assertEquals(true, $attemptobj->is_finished());
+    }
+
+    /**
+     * Tests that user data is not restored when module is restored.
+     */
+    public function test_coursemodule_restore_without_userdata() {
+        $student = $this->getDataGenerator()->create_and_enrol($this->course, 'student');
+        $this->setUser($student);
+
+        set_config('backup_auto_users', false, 'backup');
+        $this->create_quiz_attempt($this->quiz, $student);
+
+        // Delete quiz.
+        $cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
+        course_delete_module($cm->id);
+        phpunit_util::run_all_adhoc_tasks();
+        $quizzes = get_coursemodules_in_course('quiz', $this->course->id);
+        $this->assertEquals(0, count($quizzes));
+
+        // Restore quiz.
+        $recyclebin = new \tool_recyclebin\course_bin($this->course->id);
+        foreach ($recyclebin->get_items() as $item) {
+            $recyclebin->restore_item($item);
+        }
+        $quizzes = get_coursemodules_in_course('quiz', $this->course->id);
+        $this->assertEquals(1, count($quizzes));
+        $cm = array_pop($quizzes);
+
+        // Check if user quiz attempt data is restored.
+        $attempts = quiz_get_user_attempts($cm->instance, $student->id);
+        $this->assertEquals(0, count($attempts));
+    }
+
+    /**
+     * Add a question to quiz and create a quiz attempt.
+     * @param \stdClass $quiz Quiz
+     * @param \stdClass $student User
+     * @throws coding_exception
+     * @throws moodle_exception
+     */
+    private function create_quiz_attempt($quiz, $student) {
+        // Add Question.
+        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $cat = $questiongenerator->create_question_category();
+        $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
+        quiz_add_quiz_question($numq->id, $quiz);
+
+        // Create quiz attempt.
+        $quizobj = quiz::create($quiz->id, $student->id);
+        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+        $timenow = time();
+        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $student->id);
+        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj, $quba, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $tosubmit = array(1 => array('answer' => '0'));
+        $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_finish($timenow, false);
+    }
 }
index 64955f2..f29f481 100644 (file)
@@ -796,8 +796,7 @@ class manager {
         // the format filename => version. The version value needs to
         // be increased if the tour has been updated.
         $shippedtours = [
-            '36_dashboard.json' => 3,
-            '36_messaging.json' => 3,
+            '36_dashboard.json' => 3
         ];
 
         // These are tours that we used to ship but don't ship any longer.
@@ -807,6 +806,12 @@ class manager {
             'boost_course_view.json' => 1,
         ];
 
+        if ($CFG->messaging) {
+            $shippedtours['36_messaging.json'] = 3;
+        } else {
+            $unshippedtours['36_messaging.json'] = 3;
+        }
+
         $existingtourrecords = $DB->get_recordset('tool_usertours_tours');
 
         // Get all of the existing shipped tours and check if they need to be
index a8582de..47e4994 100644 (file)
@@ -54,5 +54,12 @@ function xmldb_tool_usertours_upgrade($oldversion) {
     // Automatically generated Moodle v3.6.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2019030600) {
+        // Update the tours shipped with Moodle.
+        manager::update_shipped_tours();
+
+        upgrade_plugin_savepoint(true, 2019030600, 'tool', 'usertours');
+    }
+
     return true;
 }
index 2f00da1..c678cb9 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;            // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2019030600;            // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2018112800;            // Requires this Moodle version.
 $plugin->component = 'tool_usertours';      // Full name of the plugin (used for diagnostics).
index be0d3a0..919e9f6 100644 (file)
@@ -63,7 +63,9 @@ interface classifier extends predictor {
      * @param int $niterations
      * @param \stored_file $dataset
      * @param string $outputdir
+     * @param  string $trainedmodeldir
      * @return \stdClass
      */
-    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
+    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
+            $outputdir, $trainedmodeldir);
 }
index 638b18b..142518e 100644 (file)
@@ -196,7 +196,7 @@ abstract class base extends \core_analytics\calculable {
                 $message->component = 'moodle';
                 $message->name = 'insights';
 
-                $message->userfrom = get_admin();
+                $message->userfrom = \core_user::get_noreply_user();
                 $message->userto = $user;
 
                 $insighturl = new \moodle_url('/report/insights/insights.php?modelid=' . $modelid . '&contextid=' . $context->id);
index 1c10cde..8fcf6e5 100644 (file)
@@ -254,11 +254,26 @@ class manager {
     /**
      * Returns the enabled time splitting methods.
      *
+     * @deprecated since Moodle 3.7
+     * @todo MDL-65086 This will be deleted in Moodle 4.1
+     * @see \core_analytics\manager::get_time_splitting_methods_for_evaluation
      * @return \core_analytics\local\time_splitting\base[]
      */
     public static function get_enabled_time_splitting_methods() {
+        debugging('This function has been deprecated. You can use self::get_time_splitting_methods_for_evaluation if ' .
+            'you want to get the default time splitting methods for evaluation, or you can use self::get_all_time_splittings if ' .
+            'you want to get all the time splitting methods available on this site.');
+        return self::get_time_splitting_methods_for_evaluation();
+    }
+
+    /**
+     * Returns the default time splitting methods for model evaluation.
+     *
+     * @return \core_analytics\local\time_splitting\base[]
+     */
+    public static function get_time_splitting_methods_for_evaluation() {
 
-        if ($enabledtimesplittings = get_config('analytics', 'timesplittings')) {
+        if ($enabledtimesplittings = get_config('analytics', 'defaulttimesplittingsevaluation')) {
             $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
         }
 
index 3b6c6e4..3756b90 100644 (file)
@@ -289,7 +289,7 @@ class model {
                     $timesplitting = \core_analytics\manager::get_time_splitting($options['timesplitting']);
                     $timesplittings = array($timesplitting->get_id() => $timesplitting);
                 } else {
-                    $timesplittings = \core_analytics\manager::get_enabled_time_splitting_methods();
+                    $timesplittings = \core_analytics\manager::get_time_splitting_methods_for_evaluation();
                 }
             } else {
 
@@ -537,6 +537,29 @@ class model {
         }
 
         $options['evaluation'] = true;
+
+        if (empty($options['mode'])) {
+            $options['mode'] = 'configuration';
+        }
+
+        switch ($options['mode']) {
+            case 'trainedmodel':
+
+                // We are only interested on the time splitting method used by the trained model.
+                $options['timesplitting'] = $this->model->timesplitting;
+
+                // Provide the trained model directory to the ML backend if that is what we want to evaluate.
+                $trainedmodeldir = $this->get_output_dir(['execution']);
+                break;
+            case 'configuration':
+
+                $trainedmodeldir = false;
+                break;
+
+            default:
+                throw new \moodle_exception('errorunknownaction', 'analytics');
+        }
+
         $this->init_analyser($options);
 
         if (empty($this->get_indicators())) {
@@ -575,10 +598,10 @@ class model {
             // Evaluate the dataset, the deviation we accept in the results depends on the amount of iterations.
             if ($this->get_target()->is_linear()) {
                 $predictorresult = $predictor->evaluate_regression($this->get_unique_id(), self::ACCEPTED_DEVIATION,
-                self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+                    self::EVALUATION_ITERATIONS, $dataset, $outputdir, $trainedmodeldir);
             } else {
                 $predictorresult = $predictor->evaluate_classification($this->get_unique_id(), self::ACCEPTED_DEVIATION,
-                self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+                    self::EVALUATION_ITERATIONS, $dataset, $outputdir, $trainedmodeldir);
             }
 
             $result->status = $predictorresult->status;
@@ -596,7 +619,7 @@ class model {
                 $dir = $predictorresult->dir;
             }
 
-            $result->logid = $this->log_result($timesplitting->get_id(), $result->score, $dir, $result->info);
+            $result->logid = $this->log_result($timesplitting->get_id(), $result->score, $dir, $result->info, $options['mode']);
 
             $results[$timesplitting->get_id()] = $result;
         }
@@ -1462,6 +1485,29 @@ class model {
         return \core_analytics\dataset_manager::export_training_data($this->get_id(), $timesplittingid);
     }
 
+    /**
+     * Has the model been trained using data from this site?
+     *
+     * This method is useful to determine if a trained model can be evaluated as
+     * we can not use the same data for training and for evaluation.
+     *
+     * @return bool
+     */
+    public function trained_locally() : bool {
+        global $DB;
+
+        if (!$this->is_trained() || $this->is_static()) {
+            // Early exit.
+            return false;
+        }
+
+        if ($DB->record_exists('analytics_train_samples', ['modelid' => $this->model->id])) {
+            return true;
+        }
+
+        return false;
+    }
+
     /**
      * Flag the provided file as used for training or prediction.
      *
@@ -1487,14 +1533,16 @@ class model {
      * @param float $score
      * @param string $dir
      * @param array $info
+     * @param string $evaluationmode
      * @return int The inserted log id
      */
-    protected function log_result($timesplittingid, $score, $dir = false, $info = false) {
+    protected function log_result($timesplittingid, $score, $dir = false, $info = false, $evaluationmode = 'configuration') {
         global $DB, $USER;
 
         $log = new \stdClass();
         $log->modelid = $this->get_id();
         $log->version = $this->model->version;
+        $log->evaluationmode = $evaluationmode;
         $log->target = $this->model->target;
         $log->indicators = $this->model->indicators;
         $log->timesplitting = $timesplittingid;
index c2d2a89..c8e0bcf 100644 (file)
@@ -63,7 +63,9 @@ interface regressor extends predictor {
      * @param int $niterations
      * @param \stored_file $dataset
      * @param string $outputdir
+     * @param  string $trainedmodeldir
      * @return \stdClass
      */
-    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
+    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
+            $outputdir, $trainedmodeldir);
 }
diff --git a/analytics/classes/stats.php b/analytics/classes/stats.php
new file mode 100644 (file)
index 0000000..0caf975
--- /dev/null
@@ -0,0 +1,78 @@
+<?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 the {@link \core_analytics\stats} class.
+ *
+ * @package     core_analytics
+ * @copyright   2019 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Provides stats and meta information about the analytics usage on this site.
+ *
+ * @copyright 2019 David Mudrák <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class stats {
+
+    /**
+     * Return the number of models enabled on this site.
+     *
+     * @return int
+     */
+    public static function enabled_models() : int {
+        return count(manager::get_all_models(true));
+    }
+
+    /**
+     * Return the number of predictions generated by the system.
+     *
+     * @return int
+     */
+    public static function predictions() : int {
+        global $DB;
+
+        return $DB->count_records('analytics_predictions');
+    }
+
+    /**
+     * Return the number of suggested actions executed by users.
+     *
+     * @return int
+     */
+    public static function actions() : int {
+        global $DB;
+
+        return $DB->count_records('analytics_prediction_actions');
+    }
+
+    /**
+     * Return the number of suggested actions flagged as not useful.
+     *
+     * @return int
+     */
+    public static function actions_not_useful() : int {
+        global $DB;
+
+        return $DB->count_records('analytics_prediction_actions', ['actionname' => prediction::ACTION_NOT_USEFUL]);
+    }
+}
index 18582c3..7728648 100644 (file)
@@ -274,7 +274,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
      * test_ml_export_import
      *
      * @param string $predictionsprocessorclass The class name
-     * @dataProvider provider_ml_export_import
+     * @dataProvider provider_ml_processors
      */
     public function test_ml_export_import($predictionsprocessorclass) {
 
@@ -296,6 +296,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $model->update(true, false, '\core\analytics\time_splitting\quarters', get_class($predictionsprocessor));
 
         $model->train();
+        $this->assertTrue($model->trained_locally());
 
         $this->generate_courses(10, ['visible' => 0]);
 
@@ -314,16 +315,18 @@ class core_analytics_prediction_testcase extends advanced_testcase {
             $this->assertEquals($importedmodelresults->predictions[$sampleid]->prediction, $prediction->prediction);
         }
 
+        $this->assertFalse($importmodel->trained_locally());
+
         set_config('enabled_stores', '', 'tool_log');
         get_log_manager(true);
     }
 
     /**
-     * provider_ml_export_import
+     * provider_ml_processors
      *
      * @return array
      */
-    public function provider_ml_export_import() {
+    public function provider_ml_processors() {
         $cases = [
             'case' => [],
         ];
@@ -425,14 +428,14 @@ class core_analytics_prediction_testcase extends advanced_testcase {
     /**
      * Basic test to check that prediction processors work as expected.
      *
-     * @dataProvider provider_ml_test_evaluation
+     * @dataProvider provider_ml_test_evaluation_configuration
      * @param string $modelquality
      * @param int $ncourses
      * @param array $expected
      * @param string $predictionsprocessorclass
      * @return void
      */
-    public function test_ml_evaluation($modelquality, $ncourses, $expected, $predictionsprocessorclass) {
+    public function test_ml_evaluation_configuration($modelquality, $ncourses, $expected, $predictionsprocessorclass) {
         $this->resetAfterTest(true);
         $this->setAdminuser();
         set_config('enabled_stores', 'logstore_standard', 'tool_log');
@@ -440,7 +443,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $sometimesplittings = '\core\analytics\time_splitting\weekly,' .
             '\core\analytics\time_splitting\single_range,' .
             '\core\analytics\time_splitting\quarters';
-        set_config('timesplittings', $sometimesplittings, 'analytics');
+        set_config('defaulttimesplittingsevaluation', $sometimesplittings, 'analytics');
 
         if ($modelquality === 'perfect') {
             $model = $this->add_perfect_model();
@@ -473,6 +476,44 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         get_log_manager(true);
     }
 
+    /**
+     * Tests the evaluation of already trained models.
+     *
+     * @dataProvider provider_ml_processors
+     * @param  string $predictionsprocessorclass
+     * @return null
+     */
+    public function test_ml_evaluation_trained_model($predictionsprocessorclass) {
+        $this->resetAfterTest(true);
+        $this->setAdminuser();
+        set_config('enabled_stores', 'logstore_standard', 'tool_log');
+
+        $model = $this->add_perfect_model();
+
+        // Generate training data.
+        $this->generate_courses(50);
+
+        // We repeat the test for all prediction processors.
+        $predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
+        if ($predictionsprocessor->is_ready() !== true) {
+            $this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
+        }
+
+        $model->update(true, false, '\\core\\analytics\\time_splitting\\quarters', get_class($predictionsprocessor));
+        $model->train();
+
+        $zipfilename = 'model-zip-' . microtime() . '.zip';
+        $zipfilepath = $model->export_model($zipfilename);
+        $importmodel = \core_analytics\model::import_model($zipfilepath);
+
+        $results = $importmodel->evaluate(['mode' => 'trainedmodel']);
+        $this->assertEquals(0, $results['\\core\\analytics\\time_splitting\\quarters']->status);
+        $this->assertEquals(1, $results['\\core\\analytics\\time_splitting\\quarters']->score);
+
+        set_config('enabled_stores', '', 'tool_log');
+        get_log_manager(true);
+    }
+
     /**
      * test_read_indicator_calculations
      *
@@ -547,11 +588,11 @@ class core_analytics_prediction_testcase extends advanced_testcase {
     }
 
     /**
-     * provider_ml_test_evaluation
+     * provider_ml_test_evaluation_configuration
      *
      * @return array
      */
-    public function provider_ml_test_evaluation() {
+    public function provider_ml_test_evaluation_configuration() {
 
         $cases = array(
             'bad' => array(
diff --git a/analytics/tests/stats_test.php b/analytics/tests/stats_test.php
new file mode 100644 (file)
index 0000000..d92f403
--- /dev/null
@@ -0,0 +1,162 @@
+<?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 the {@link analytics_stats_testcase} class.
+ *
+ * @package     core_analytics
+ * @category    test
+ * @copyright   2019 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
+require_once(__DIR__ . '/fixtures/test_target_shortname.php');
+
+/**
+ * Unit tests for the analytics stats.
+ *
+ * @copyright 2019 David Mudrák <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class analytics_stats_testcase extends advanced_testcase {
+
+    /**
+     * Set up the test environment.
+     */
+    public function setUp() {
+
+        $this->setAdminUser();
+    }
+
+    /**
+     * Test the {@link \core_analytics\stats::enabled_models()} implementation.
+     */
+    public function test_enabled_models() {
+
+        $this->resetAfterTest(true);
+
+        // By default, sites have {@link \core\analytics\target\no_teaching} enabled.
+        $this->assertEquals(1, \core_analytics\stats::enabled_models());
+
+        $model = \core_analytics\model::create(
+            \core_analytics\manager::get_target('\core\analytics\target\course_dropout'),
+            [
+                \core_analytics\manager::get_indicator('\core\analytics\indicator\any_write_action'),
+            ]
+        );
+
+        // Purely adding a new model does not make it included in the stats.
+        $this->assertEquals(1, \core_analytics\stats::enabled_models());
+
+        // New models must be enabled to have them counted.
+        $model->enable('\core\analytics\time_splitting\quarters');
+        $this->assertEquals(2, \core_analytics\stats::enabled_models());
+    }
+
+    /**
+     * Test the {@link \core_analytics\stats::predictions()} implementation.
+     */
+    public function test_predictions() {
+
+        $this->resetAfterTest(true);
+
+        $model = \core_analytics\model::create(
+            \core_analytics\manager::get_target('test_target_shortname'),
+            [
+                \core_analytics\manager::get_indicator('test_indicator_fullname'),
+            ]
+        );
+
+        $model->enable('\core\analytics\time_splitting\no_splitting');
+
+        // Train the model.
+        $this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]);
+        $this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]);
+        $model->train();
+
+        // No predictions yet.
+        $this->assertEquals(0, \core_analytics\stats::predictions());
+
+        // Get one new prediction.
+        $this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]);
+        $result = $model->predict();
+
+        $this->assertEquals(1, count($result->predictions));
+        $this->assertEquals(1, \core_analytics\stats::predictions());
+
+        // Nothing changes if there is no new prediction.
+        $result = $model->predict();
+        $this->assertFalse(isset($result->predictions));
+        $this->assertEquals(1, \core_analytics\stats::predictions());
+
+        // Get two more predictions, we have three in total now.
+        $this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]);
+        $this->getDataGenerator()->create_course(['shortname' => 'cc', 'fullname' => 'cc', 'visible' => 0]);
+
+        $result = $model->predict();
+        $this->assertEquals(2, count($result->predictions));
+        $this->assertEquals(3, \core_analytics\stats::predictions());
+    }
+
+    /**
+     * Test the {@link \core_analytics\stats::actions()} and {@link \core_analytics\stats::actions_not_useful()} implementation.
+     */
+    public function test_actions() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $model = \core_analytics\model::create(
+            \core_analytics\manager::get_target('test_target_shortname'),
+            [
+                \core_analytics\manager::get_indicator('test_indicator_fullname'),
+            ]
+        );
+
+        $model->enable('\core\analytics\time_splitting\no_splitting');
+
+        // Train the model.
+        $this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]);
+        $this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]);
+        $model->train();
+
+        // Generate two predictions.
+        $this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]);
+        $this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]);
+        $model->predict();
+
+        list($p1, $p2) = array_values($DB->get_records('analytics_predictions'));
+
+        $p1 = new \core_analytics\prediction($p1, []);
+        $p2 = new \core_analytics\prediction($p2, []);
+
+        // No actions executed at the start.
+        $this->assertEquals(0, \core_analytics\stats::actions());
+        $this->assertEquals(0, \core_analytics\stats::actions_not_useful());
+
+        // The user has acknowledged the first prediction.
+        $p1->action_executed(\core_analytics\prediction::ACTION_FIXED, $model->get_target());
+        $this->assertEquals(1, \core_analytics\stats::actions());
+        $this->assertEquals(0, \core_analytics\stats::actions_not_useful());
+
+        // The user has marked the other prediction as not useful.
+        $p2->action_executed(\core_analytics\prediction::ACTION_NOT_USEFUL, $model->get_target());
+        $this->assertEquals(2, \core_analytics\stats::actions());
+        $this->assertEquals(1, \core_analytics\stats::actions_not_useful());
+    }
+}
index 550a3da..3187365 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in analytics sub system,
 information provided here is intended especially for developers.
 
+=== 3.7 ===
+
+* \core_analytics\regressor::evaluate_regression and \core_analytics\classifier::evaluate_classification
+  have been updated to include a new $trainedmodeldir param. This new param will be used to evaluate the
+  existing trained model.
+
 === 3.5 ===
 
 * There are two new methods for analysers, processes_user_data() and join_sample_user(). You
index 19e314f..5f638ec 100644 (file)
@@ -2282,7 +2282,11 @@ class backup_questions_structure_step extends backup_structure_step {
                               FROM {tag} t
                               JOIN {tag_instance} ti ON ti.tagid = t.id
                               WHERE ti.itemid = ?
-                              AND ti.itemtype = 'question'", array(backup::VAR_PARENTID));
+                              AND ti.itemtype = 'question'
+                              AND ti.component = 'core_question'",
+            [
+                backup::VAR_PARENTID
+            ]);
 
         // don't need to annotate ids nor files
         // (already done by {@link backup_annotate_all_question_files}
index 77260f0..e0b38bd 100644 (file)
@@ -96,6 +96,7 @@ if (!has_any_capability(array(
         'moodle/badges:viewawarded',
         'moodle/badges:createbadge',
         'moodle/badges:awardbadge',
+        'moodle/badges:configurecriteria',
         'moodle/badges:configuremessages',
         'moodle/badges:configuredetails',
         'moodle/badges:deletebadge'), $PAGE->context)) {
index 9a52ef7..c987586 100644 (file)
@@ -580,6 +580,9 @@ class block_base {
     function user_can_addto($page) {
         global $USER;
 
+        // List of formats this block supports.
+        $formats = $this->applicable_formats();
+
         // The blocks in My Moodle are a special case and use a different capability.
         if (!empty($USER->id)
             && $page->context->contextlevel == CONTEXT_USER // Page belongs to a user
@@ -587,7 +590,6 @@ class block_base {
             && $page->pagetype == 'my-index') { // Ensure we are on the My Moodle page
 
             // If the block cannot be displayed on /my it is ok if the myaddinstance capability is not defined.
-            $formats = $this->applicable_formats();
             // Is 'my' explicitly forbidden?
             // If 'all' has not been allowed, has 'my' been explicitly allowed?
             if ((isset($formats['my']) && $formats['my'] == false)
@@ -601,6 +603,12 @@ class block_base {
                        && has_capability('moodle/my:manageblocks', $page->context);
             }
         }
+        // Check if this is a block only used on /my.
+        unset($formats['my']);
+        if (empty($formats)) {
+            // Block can only be added to /my - return false.
+            return false;
+        }
 
         $capability = 'block/' . $this->name() . ':addinstance';
         if ($this->has_add_block_capability($page, $capability)
index d05b432..b7cb15c 100644 (file)
@@ -34,17 +34,5 @@ $capabilities = array(
         ),
 
         'clonepermissionsfrom' => 'moodle/my:manageblocks'
-    ),
-
-    'block/myoverview:addinstance' => array(
-        'riskbitmask' => RISK_SPAM | RISK_XSS,
-
-        'captype' => 'write',
-        'contextlevel' => CONTEXT_BLOCK,
-        'archetypes' => array(
-            'manager' => CAP_ALLOW
-        ),
-
-        'clonepermissionsfrom' => 'moodle/site:manageblocks'
     )
 );
index 910f9fb..3ef06af 100644 (file)
@@ -48,13 +48,12 @@ $string['aria:sortingdropdown'] = 'Sorting drop-down menu';
 $string['card'] = 'Card';
 $string['cards'] = 'Cards';
 $string['courseprogress'] = 'Course progress:';
-$string['complete'] = 'complete';
+$string['completepercent'] = '{$a}% complete';
 $string['favourites'] = 'Starred';
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
 $string['lastaccessed'] = 'Last accessed';
 $string['list'] = 'List';
-$string['myoverview:addinstance'] = 'Add a new course overview block';
 $string['myoverview:myaddinstance'] = 'Add a new course overview block to Dashboard';
 $string['past'] = 'Past';
 $string['pluginname'] = 'Course overview';
@@ -93,4 +92,5 @@ $string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected
 $string['viewcourse'] = 'View course';
 
 // Deprecated since Moodle 3.7.
-$string['nocourses'] = 'No courses';
\ No newline at end of file
+$string['complete'] = 'complete';
+$string['nocourses'] = 'No courses';
index 4e22e76..fb6f86c 100644 (file)
@@ -13,4 +13,5 @@ sortbydates,block_myoverview
 timeline,block_myoverview
 viewcoursename,block_myoverview
 privacy:metadata:overviewlasttab,block_myoverview
-nocourses,block_myoverview
\ No newline at end of file
+nocourses,block_myoverview
+complete,block_myoverview
\ No newline at end of file
index 2567555..574b5e5 100644 (file)
 }}
 
 <div class="m-b-1 mr-1 d-flex align-items-center">
-    <div class="d-none d-md-inline-block mr-1">{{#str}} sortby, core {{/str}}</div>
     <div class="dropdown">
         <button id="sortingdropdown" type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown"  aria-haspopup="true" aria-expanded="false" aria-label="{{#str}} aria:sortingdropdown, block_myoverview {{/str}}">
-            <span data-active-item-text>
+            {{#pix}} t/sort_by {{/pix}}
+            <span class="d-sm-inline-block" data-active-item-text>
                 {{#title}}{{#str}} title, block_myoverview {{/str}}{{/title}}
                 {{#lastaccessed}}{{#str}} lastaccessed, block_myoverview {{/str}}{{/lastaccessed}}
             </span>
index 9bb0a57..02fa1f0 100644 (file)
@@ -30,5 +30,5 @@
 </div>
 <div class="small">
     <span class="sr-only">{{#str}}aria:courseprogress, block_myoverview{{/str}}</span>
-    <strong>{{progress}}%</strong> {{#str}}complete, block_myoverview{{/str}}
+    {{#str}}completepercent, block_myoverview, <strong>{{progress}}</strong>{{/str}}
 </div>
diff --git a/blocks/myoverview/upgrade.txt b/blocks/myoverview/upgrade.txt
new file mode 100644 (file)
index 0000000..e2d27b8
--- /dev/null
@@ -0,0 +1,5 @@
+This file describes API changes in the myoverview block code.
+
+=== 3.7 ===
+
+* The 'block/myoverview:addinstance' capability has been removed. It has never been used in code.
\ No newline at end of file
index 03bb65a..62127b7 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019022600;         // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2018112800;         // Requires this Moodle version.
 $plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
index a06c1b9..5fc4788 100644 (file)
@@ -34,15 +34,5 @@ $capabilities = array(
         ),
 
         'clonepermissionsfrom' => 'moodle/my:manageblocks'
-    ),
-
-    'block/recentlyaccessedcourses:addinstance' => array(
-        'captype' => 'write',
-        'contextlevel' => CONTEXT_BLOCK,
-        'archetypes' => array(
-            'manager' => CAP_ALLOW
-        ),
-
-        'clonepermissionsfrom' => 'moodle/site:manageblocks'
     )
 );
index cdf71be..8a72247 100644 (file)
@@ -22,6 +22,5 @@
  */
 $string['pluginname'] = 'Recently accessed courses';
 $string['privacy:metadata'] = 'The Recently accessed courses block does not store any personal data.';
-$string['recentlyaccessedcourses:addinstance'] = 'Add a new Recently accessed courses block';
 $string['recentlyaccessedcourses:myaddinstance'] = 'Add a new recently accessed courses block to Dashboard';
 $string['nocourses'] = 'No recent courses';
\ No newline at end of file
index 2a32157..1361760 100644 (file)
@@ -34,6 +34,7 @@ Feature: The recently accessed courses block allows users to easily access their
     When I am on "Course 1" course homepage
     And I am on "Course 2" course homepage
     And I follow "Dashboard" in the user menu
+    And I change window size to "large"
     Then I should see "Course 1" in the "Recently accessed courses" "block"
     And I should see "Course 2" in the "Recently accessed courses" "block"
     And I should not see "Course 3" in the "Recently accessed courses" "block"
diff --git a/blocks/recentlyaccessedcourses/upgrade.txt b/blocks/recentlyaccessedcourses/upgrade.txt
new file mode 100644 (file)
index 0000000..2f6b922
--- /dev/null
@@ -0,0 +1,5 @@
+This file describes API changes in the recentlyaccessedcourses block code.
+
+=== 3.7 ===
+
+* The 'block/recentlyaccessedcourses:addinstance' capability has been removed. It has never been used in code.
\ No newline at end of file
index d2dc216..f5d3d96 100644 (file)
@@ -22,6 +22,6 @@
  */
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;            // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019022600;            // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2018112800;            // Requires this Moodle version.
 $plugin->component = 'block_recentlyaccessedcourses'; // Full name of the plugin (used for diagnostics).
index 99ef6db..1f5f1eb 100644 (file)
@@ -29,13 +29,5 @@ $capabilities = array(
                         'user' => CAP_ALLOW
                 ),
                 'clonepermissionsfrom' => 'moodle/my:manageblocks'
-        ),
-        'block/recentlyaccesseditems:addinstance' => array(
-                'captype' => 'write',
-                'contextlevel' => CONTEXT_BLOCK,
-                'archetypes' => array(
-                        'manager' => CAP_ALLOW
-                ),
-                'clonepermissionsfrom' => 'moodle/site:manageblocks'
         )
 );
\ No newline at end of file
index e9f3617..a4d217d 100644 (file)
@@ -28,5 +28,4 @@ $string['privacy:metadata:block_recentlyaccesseditemstablesummary'] = 'The Recen
 $string['privacy:metadata:timeaccess'] = 'The time when the user last accessed the item';
 $string['privacy:metadata:userid'] = 'The ID of the user who accessed the item';
 $string['privacy:recentlyaccesseditemspath'] = 'Recently accessed items';
-$string['recentlyaccesseditems:addinstance'] = 'Add a new recently accessed items block';
 $string['recentlyaccesseditems:myaddinstance'] = 'Add a new recently accessed items block to Dashboard';
\ No newline at end of file
index 5aa383f..4c2de4f 100644 (file)
@@ -35,4 +35,5 @@ Feature: The recently accessed items block allows users to easily access their m
     Given I am on "Course 1" course homepage
     When  I follow "Test forum name"
     And I follow "Dashboard" in the user menu
+    And I change window size to "large"
     Then I should see "Test forum name" in the "Recently accessed items" "block"
\ No newline at end of file
diff --git a/blocks/recentlyaccesseditems/upgrade.txt b/blocks/recentlyaccesseditems/upgrade.txt
new file mode 100644 (file)
index 0000000..c9f8430
--- /dev/null
@@ -0,0 +1,5 @@
+This file describes API changes in the recentlyaccesseditems block code.
+
+=== 3.7 ===
+
+* The 'block/recentlyaccesseditems:addinstance' capability has been removed. It has never been used in code.
\ No newline at end of file
index 1b52c62..b6c5eb1 100644 (file)
@@ -22,6 +22,6 @@
  */
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120301;            // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019022600;            // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2018112800;            // Requires this Moodle version.
 $plugin->component = 'block_recentlyaccesseditems'; // Full name of the plugin (used for diagnostics).
index 2f5e365..7b23252 100644 (file)
@@ -34,15 +34,5 @@ $capabilities = array(
         ),
 
         'clonepermissionsfrom' => 'moodle/my:manageblocks'
-    ),
-
-    'block/starredcourses:addinstance' => array(
-        'captype' => 'write',
-        'contextlevel' => CONTEXT_BLOCK,
-        'archetypes' => array(
-            'manager' => CAP_ALLOW
-        ),
-
-        'clonepermissionsfrom' => 'moodle/site:manageblocks'
-    ),
+    )
 );
index 37d51a6..06706b8 100644 (file)
@@ -24,6 +24,5 @@
 
 $string['pluginname'] = 'Starred courses';
 $string['privacy:metadata'] = 'The starred courses block does not store any personal data.';
-$string['starredcourses:addinstance'] = 'Add a new starred courses block';
 $string['starredcourses:myaddinstance'] = 'Add a new starred courses block to Dashboard';
 $string['nocourses'] = 'No starred courses';
\ No newline at end of file
diff --git a/blocks/starredcourses/upgrade.txt b/blocks/starredcourses/upgrade.txt
new file mode 100644 (file)
index 0000000..4f39536
--- /dev/null
@@ -0,0 +1,5 @@
+This file describes API changes in the starredcourses block code.
+
+=== 3.7 ===
+
+* The 'block/starredcourses:addinstance' capability has been removed. It has never been used in code.
\ No newline at end of file
index ad78412..8ca3131 100644 (file)
@@ -23,6 +23,6 @@
  */
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version = 2018120300;
+$plugin->version = 2019022600;
 $plugin->requires  = 2018112800;
 $plugin->component = 'block_starredcourses';
\ No newline at end of file
index 6bd70a2..2f8e788 100644 (file)
@@ -34,17 +34,5 @@ $capabilities = array(
         ),
 
         'clonepermissionsfrom' => 'moodle/my:manageblocks'
-    ),
-
-    'block/timeline:addinstance' => array(
-        'riskbitmask' => RISK_SPAM | RISK_XSS,
-
-        'captype' => 'write',
-        'contextlevel' => CONTEXT_BLOCK,
-        'archetypes' => array(
-            'manager' => CAP_ALLOW
-        ),
-
-        'clonepermissionsfrom' => 'moodle/site:manageblocks'
     )
 );
index b45d3ff..636463e 100644 (file)
@@ -32,7 +32,6 @@ $string['ariaviewselector'] = 'Sort timeline items';
 $string['ariaviewselectoroption'] = '{$a} sort option';
 $string['duedate'] = 'Due date';
 $string['morecourses'] = 'More courses';
-$string['timeline:addinstance'] = 'Add a new timeline block';
 $string['timeline:myaddinstance'] = 'Add a new timeline block to Dashboard';
 $string['nocoursesinprogress'] = 'No in-progress courses';
 $string['noevents'] = 'No upcoming activities due';
index 938763b..52d92a6 100644 (file)
     {}
 }}
 <div data-region="day-filter" class="dropdown">
-    <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+    <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
+            aria-label="{{#str}} ariadayfilter, block_timeline {{/str}}" aria-controls="menudayfilter">
         {{#pix}} i/duration {{/pix}}
-        <span class="sr-only">
-            {{#str}} ariadayfilter, block_timeline {{/str}}
-            <span data-active-item-text>{{#str}} next30days, block_timeline {{/str}}</span>
+        <span class="sr-only" data-active-item-text>
+            {{#all}} {{#str}} all, core {{/str}} {{/all}}
+            {{#overdue}} {{#str}} overdue, block_timeline {{/str}} {{/overdue}}
+            {{#next7days}} {{#str}}next7days, block_timeline {{/str}} {{/next7days}}
+            {{#next30days}} {{#str}}next30days, block_timeline {{/str}} {{/next30days}}
+            {{#next3months}} {{#str}}next3months, block_timeline {{/str}} {{/next3months}}
+            {{#next6months}} {{#str}}next6months, block_timeline {{/str}} {{/next6months}}
         </span>
     </button>
-    <div role="menu" class="dropdown-menu" data-show-active-item>
+    <div id="menudayfilter" role="menu" class="dropdown-menu" data-show-active-item>
         <a
             class="dropdown-item {{#all}} active {{/all}}"
             href="#"
             data-from="-14"
             data-filtername="all"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} all, core {{/str}}{{/str}}"
+            role="menuitem"
+            {{#all}}aria-current="true"{{/all}}
         >
             {{#str}} all, core {{/str}}
         </a>
             data-to="0"
             data-filtername="overdue"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} overdue, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#overdue}}aria-current="true"{{/overdue}}
         >
             {{#str}} overdue, block_timeline {{/str}}
         </a>
-        <div class="dropdown-divider"></div>
+        <div class="dropdown-divider" role="separator"></div>
         <h6 class="dropdown-header">{{#str}} duedate, block_timeline {{/str}}</h6>
         <a
             class="dropdown-item {{#next7days}} active {{/next7days}}"
@@ -59,6 +68,8 @@
             data-to="7"
             data-filtername="next7days"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#next7days}}aria-current="true"{{/next7days}}
         >
             {{#str}} next7days, block_timeline {{/str}}
         </a>
@@ -69,6 +80,8 @@
             data-to="30"
             data-filtername="next30days"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#next30days}}aria-current="true"{{/next30days}}
         >
             {{#str}} next30days, block_timeline {{/str}}
         </a>
@@ -79,6 +92,8 @@
             data-to="90"
             data-filtername="next3months"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#next3months}}aria-current="true"{{/next3months}}
         >
             {{#str}} next3months, block_timeline {{/str}}
         </a>
             data-to="180"
             data-filtername="next6months"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#next6months}}aria-current="true"{{/next6months}}
         >
             {{#str}} next6months, block_timeline {{/str}}
         </a>
index aa9d14d..106bf75 100644 (file)
     {}
 }}
 <div data-region="view-selector" class="btn-group">
-    <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+    <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
+            aria-label="{{#str}} ariaviewselector, block_timeline{{/str}}" aria-controls="menusortby">
         {{#pix}} t/sort_by {{/pix}}
-        <span class="sr-only">
-            {{#sorttimelinecourses}}<span data-active-item-text>{{/sorttimelinecourses}}{{#str}} ariaviewselector, block_timeline{{/str}}{{#sorttimelinecourses}}</span>{{/sorttimelinecourses}}
-            {{#sorttimelinedates}}<span data-active-item-text>{{/sorttimelinedates}}{{#str}} sortbydates, block_timeline {{/str}}{{#sorttimelinedates}}</span>{{/sorttimelinedates}}
+        <span class="sr-only" data-active-item-text>
+            {{#sorttimelinecourses}}{{#str}} sortbycourses, block_timeline{{/str}}{{/sorttimelinecourses}}
+            {{#sorttimelinedates}}{{#str}} sortbydates, block_timeline {{/str}}{{/sorttimelinedates}}
         </span>
     </button>
-    <div role="menu" class="dropdown-menu dropdown-menu-right list-group hidden" data-show-active-item data-skip-active-class="true" >
+    <div id="menusortby" role="menu" class="dropdown-menu dropdown-menu-right list-group hidden" data-show-active-item data-skip-active-class="true">
         <a
             class="dropdown-item {{#sorttimelinedates}}active{{/sorttimelinedates}}"
             href="#view_dates_{{uniqid}}"
             data-toggle="tab"
             data-filtername="sortbydates"
             aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbydates, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#sorttimelinedates}}aria-current="true"{{/sorttimelinedates}}
         >
             {{#str}} sortbydates, block_timeline {{/str}}
         </a>
@@ -46,6 +49,8 @@
             data-toggle="tab"
             data-filtername="sortbycourses"
             aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbycourses, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#sorttimelinecourses}}aria-current="true"{{/sorttimelinecourses}}
         >
             {{#str}} sortbycourses, block_timeline {{/str}}
         </a>
index d9a8a20..ad50e69 100644 (file)
@@ -36,7 +36,7 @@ Feature: The timeline block allows users to see upcoming courses
 
   Scenario: Next 30 days in course view
     Given I log in as "student1"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     When I click on "Sort by courses" "link" in the "Timeline" "block"
     Then I should see "Course 1" in the "Timeline" "block"
     And I should see "Course 2" in the "Timeline" "block"
@@ -52,9 +52,9 @@ Feature: The timeline block allows users to see upcoming courses
 
   Scenario: All in course view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     And I click on "All" "link" in the "Timeline" "block"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     And I click on "Sort by courses" "link" in the "Timeline" "block"
     When I click on "More courses" "button" in the "Timeline" "block"
     Then I should see "Course 3" in the "Timeline" "block"
@@ -73,9 +73,9 @@ Feature: The timeline block allows users to see upcoming courses
 
   Scenario: Persistent sort filter
     Given I log in as "student1"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     And I click on "Sort by dates" "link" in the "Timeline" "block"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     And I click on "Sort by courses" "link" in the "Timeline" "block"
     And I reload the page
     Then I should see "Course 1" in the "Timeline" "block"
index 29548a0..479aa84 100644 (file)
@@ -33,7 +33,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: Next 7 days in date view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     When I click on "Next 7 days" "link" in the "Timeline" "block"
     Then I should see "Test choice 1 closes" in the "Timeline" "block"
     And I should see "Test feedback 1 closes" in the "Timeline" "block"
@@ -44,7 +44,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: Overdue in date view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     When I click on "Overdue" "link" in the "Timeline" "block"
     Then I should see "Test assign 1 is due" in the "Timeline" "block"
     And I should not see "Test choice 2 closes" in the "Timeline" "block"
@@ -55,7 +55,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: All in date view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     When I click on "All" "link" in the "Timeline" "block"
     Then I should see "Test assign 1 is due" in the "Timeline" "block"
     And I should see "Test feedback 1 closes" in the "Timeline" "block"
@@ -75,7 +75,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: All in date view no next
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     And I click on "All" "link" in the "Timeline" "block"
     And I click on "5" "button" in the "Timeline" "block"
     When I click on "25" "link" in the "Timeline" "block"
@@ -89,7 +89,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: Persistent All in date view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     When I click on "All" "link" in the "Timeline" "block"
     And I reload the page
     Then I should see "Test assign 1 is due" in the "Timeline" "block"
@@ -110,7 +110,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: Persistent Overdue in date view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     When I click on "Overdue" "link" in the "Timeline" "block"
     And I reload the page
     Then I should see "Test assign 1 is due" in the "Timeline" "block"
index c40bbf5..f08bc38 100644 (file)
@@ -35,9 +35,9 @@ Feature: The timeline block allows user persistence of their page limits
 
   Scenario: Toggle the page limit 5 - 25
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     And I click on "All" "link" in the "Timeline" "block"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     And I click on "Sort by dates" "link" in the "Timeline" "block"
     When I click on "5" "button" in the "Timeline" "block"
     And I click on "25" "link"
@@ -48,9 +48,9 @@ Feature: The timeline block allows user persistence of their page limits
 
   Scenario: Toggle the page limit 25 - 5
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     And I click on "All" "link" in the "Timeline" "block"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     And I click on "Sort by dates" "link" in the "Timeline" "block"
     When I click on "5" "button" in the "Timeline" "block"
     And I click on "25" "link"
diff --git a/blocks/timeline/upgrade.txt b/blocks/timeline/upgrade.txt
new file mode 100644 (file)
index 0000000..3331950
--- /dev/null
@@ -0,0 +1,5 @@
+This file describes API changes in the timeline block code.
+
+=== 3.7 ===
+
+* The 'block/timeline:addinstance' capability has been removed. It has never been used in code.
\ No newline at end of file
index 6ac08f7..90c00cf 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019022600;         // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2018112800;         // Requires this Moodle version.
 $plugin->component = 'block_timeline'; // Full name of the plugin (used for diagnostics).
index 6ed2bea..258de45 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /blocks/* - activity modules,
 information provided here is intended especially for developers.
 
+=== 3.7 ===
+* The block:addinstance capability is no longer required if the block can only be added to a dashboard.
+
 === 3.6 ===
 
 * The timeline view from block_myoverview has been split out into block_timeline.
diff --git a/cache/stores/mongodb/MongoDB/BulkWriteResult.php b/cache/stores/mongodb/MongoDB/BulkWriteResult.php
new file mode 100644 (file)
index 0000000..cdf6654
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace MongoDB;
+
+use MongoDB\Driver\WriteResult;
+use MongoDB\Exception\BadMethodCallException;
+
+/**
+ * Result class for a bulk write operation.
+ */
+class BulkWriteResult
+{
+    private $writeResult;
+    private $insertedIds;
+    private $isAcknowledged;
+
+    /**
+     * Constructor.
+     *
+     * @param WriteResult $writeResult
+     * @param mixed[]     $insertedIds
+     */
+    public function __construct(WriteResult $writeResult, array $insertedIds)
+    {
+        $this->writeResult = $writeResult;
+        $this->insertedIds = $insertedIds;
+        $this->isAcknowledged = $writeResult->isAcknowledged();
+    }
+
+    /**
+     * Return the number of documents that were deleted.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return integer
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getDeletedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getDeletedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return the number of documents that were inserted.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return integer
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getInsertedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getInsertedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return a map of the inserted documents' IDs.
+     *
+     * The index of each ID in the map corresponds to each document's position
+     * in the bulk operation. If a document had an ID prior to inserting (i.e.
+     * the driver did not generate an ID), the index will contain its "_id"
+     * field value. Any driver-generated ID will be a MongoDB\BSON\ObjectId
+     * instance.
+     *
+     * @return mixed[]
+     */
+    public function getInsertedIds()
+    {
+        return $this->insertedIds;
+    }
+
+    /**
+     * Return the number of documents that were matched by the filter.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return integer
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getMatchedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getMatchedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return the number of documents that were modified.
+     *
+     * This value is undefined (i.e. null) if the write executed as a legacy
+     * operation instead of command.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return integer|null
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getModifiedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getModifiedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return the number of documents that were upserted.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return integer
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getUpsertedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getUpsertedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return a map of the upserted documents' IDs.
+     *
+     * The index of each ID in the map corresponds to each document's position
+     * in bulk operation. If a document had an ID prior to upserting (i.e. the
+     * server did not need to generate an ID), this will contain its "_id". Any
+     * server-generated ID will be a MongoDB\BSON\ObjectId instance.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return mixed[]
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getUpsertedIds()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getUpsertedIds();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return whether this update was acknowledged by the server.
+     *
+     * If the update was not acknowledged, other fields from the WriteResult
+     * (e.g. matchedCount) will be undefined.
+     *
+     * @return boolean
+     */
+    public function isAcknowledged()
+    {
+        return $this->isAcknowledged;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/ChangeStream.php b/cache/stores/mongodb/MongoDB/ChangeStream.php
new file mode 100644 (file)
index 0000000..98a703f
--- /dev/null
@@ -0,0 +1,228 @@
+<?php
+/*
+ * Copyright 2017 MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace MongoDB;
+
+use MongoDB\BSON\Serializable;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Exception\ConnectionException;
+use MongoDB\Driver\Exception\RuntimeException;
+use MongoDB\Driver\Exception\ServerException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\ResumeTokenException;
+use IteratorIterator;
+use Iterator;
+
+/**
+ * Iterator for a change stream.
+ *
+ * @api
+ * @see \MongoDB\Collection::watch()
+ * @see http://docs.mongodb.org/manual/reference/command/changeStream/
+ */
+class ChangeStream implements Iterator
+{
+    /**
+     * @deprecated 1.4
+     * @todo Remove this in 2.0 (see: PHPLIB-360)
+     */
+    const CURSOR_NOT_FOUND = 43;
+
+    private static $errorCodeCappedPositionLost = 136;
+    private static $errorCodeInterrupted = 11601;
+    private static $errorCodeCursorKilled = 237;
+
+    private $resumeToken;
+    private $resumeCallable;
+    private $csIt;
+    private $key = 0;
+    private $hasAdvanced = false;
+
+    /**
+     * Constructor.
+     *
+     * @internal
+     * @param Cursor $cursor
+     * @param callable $resumeCallable
+     */
+    public function __construct(Cursor $cursor, callable $resumeCallable)
+    {
+        $this->resumeCallable = $resumeCallable;
+        $this->csIt = new IteratorIterator($cursor);
+    }
+
+    /**
+     * @see http://php.net/iterator.current
+     * @return mixed
+     */
+    public function current()
+    {
+        return $this->csIt->current();
+    }
+
+    /**
+     * @return \MongoDB\Driver\CursorId
+     */
+    public function getCursorId()
+    {
+        return $this->csIt->getInnerIterator()->getId();
+    }
+
+    /**
+     * @see http://php.net/iterator.key
+     * @return mixed
+     */
+    public function key()
+    {
+        if ($this->valid()) {
+            return $this->key;
+        }
+        return null;
+    }
+
+    /**
+     * @see http://php.net/iterator.next
+     * @return void
+     */
+    public function next()
+    {
+        try {
+            $this->csIt->next();
+            if ($this->valid()) {
+                if ($this->hasAdvanced) {
+                    $this->key++;
+                }
+                $this->hasAdvanced = true;
+                $this->resumeToken = $this->extractResumeToken($this->csIt->current());
+          &nbs