Merge branch 'MDL-68415' of https://github.com/paulholden/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 7 May 2020 16:38:43 +0000 (18:38 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 7 May 2020 16:38:43 +0000 (18:38 +0200)
480 files changed:
.eslintignore
.stylelintignore
.travis.yml
admin/renderer.php
admin/settings/development.php
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/settings.php
admin/tool/task/lang/en/tool_task.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/classes/helper.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/uploadcourse/index.php
admin/tool/uploadcourse/lang/en/tool_uploadcourse.php
admin/tool/uploadcourse/tests/behat/create.feature
admin/tool/uploadcourse/tests/behat/update.feature
admin/tool/uploadcourse/tests/course_test.php
admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv [new file with mode: 0644]
admin/tool/uploadcourse/tests/helper_test.php
admin/tool/xmldb/lang/en/tool_xmldb.php
admin/user/user_bulk_download.php
availability/condition/completion/classes/condition.php
availability/condition/completion/classes/frontend.php
availability/condition/completion/db/caches.php [new file with mode: 0644]
availability/condition/completion/lang/en/availability_completion.php
availability/condition/completion/tests/behat/availability_completion_previous.feature [new file with mode: 0644]
availability/condition/completion/tests/condition_test.php
availability/condition/completion/version.php
availability/tests/fixtures/mock_info_module.php [new file with mode: 0644]
availability/tests/fixtures/mock_info_section.php [new file with mode: 0644]
blocks/recent_activity/renderer.php
blocks/recent_activity/styles.css
blocks/site_main_menu/tests/behat/add_url.feature
cache/classes/helper.php
cache/classes/loaders.php
cache/classes/store.php
cache/tests/cache_test.php
cache/upgrade.txt
calendar/amd/build/crud.min.js
calendar/amd/build/crud.min.js.map
calendar/amd/build/modal_delete.min.js
calendar/amd/build/modal_delete.min.js.map
calendar/amd/src/crud.js
calendar/amd/src/modal_delete.js
composer.json
composer.lock
config-dist.php
contentbank/amd/build/actions.min.js
contentbank/amd/build/actions.min.js.map
contentbank/amd/src/actions.js
contentbank/classes/content.php
contentbank/classes/contentbank.php
contentbank/classes/contenttype.php
contentbank/classes/external/rename_content.php [new file with mode: 0644]
contentbank/classes/privacy/provider.php
contentbank/contenttype/h5p/classes/contenttype.php
contentbank/contenttype/h5p/lang/en/contenttype_h5p.php
contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature
contentbank/contenttype/h5p/tests/behat/manage_content.feature [new file with mode: 0644]
contentbank/contenttype/h5p/tests/content_h5p_test.php
contentbank/templates/renamecontent.mustache [new file with mode: 0644]
contentbank/tests/behat/delete_content.feature
contentbank/tests/behat/events.feature [new file with mode: 0644]
contentbank/tests/content_test.php
contentbank/tests/contentbank_test.php
contentbank/tests/contenttype_test.php
contentbank/tests/external/rename_content_test.php [new file with mode: 0644]
contentbank/tests/privacy_test.php [new file with mode: 0644]
contentbank/upload.php
contentbank/view.php
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/src/activitychooser.js
course/classes/management_renderer.php
course/format/renderer.php
course/format/topics/styles.css [deleted file]
course/format/weeks/styles.css [deleted file]
course/recent.php
course/renderer.php
course/tests/behat/activity_chooser.feature
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js
course/yui/src/dragdrop/js/section.js
customfield/classes/data_controller.php
customfield/classes/field_controller.php
customfield/classes/handler.php
customfield/field/date/classes/field_controller.php
customfield/field/date/tests/plugin_test.php
customfield/field/select/classes/field_controller.php
customfield/field/select/tests/plugin_test.php
customfield/field/textarea/classes/data_controller.php
dataformat/pdf/classes/writer.php
dataformat/upgrade.txt
enrol/database/settings.php
enrol/database/upgrade.txt
filter/mathjaxloader/db/upgrade.php
filter/mathjaxloader/readme_moodle.txt
filter/mathjaxloader/settings.php
filter/mathjaxloader/version.php
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/grading/form/guide/templates/grades/grader/gradingpanel.mustache
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/styles.css
h5p/h5plib/v124/lang/en/h5plib_v124.php
h5p/tests/editor_test.php
install/lang/fr/error.php
install/lang/scn/langconfig.php [new file with mode: 0644]
lang/en/admin.php
lang/en/cache.php
lang/en/contentbank.php
lang/en/course.php
lang/en/enrol.php
lang/en/error.php
lang/en/h5p.php
lang/en/moodle.php
lang/en/repository.php
lang/en/role.php
lib/accesslib.php
lib/amd/build/custom_interaction_events.min.js
lib/amd/build/custom_interaction_events.min.js.map
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-autocomplete.min.js.map
lib/amd/build/local/modal/alert.min.js [new file with mode: 0644]
lib/amd/build/local/modal/alert.min.js.map [new file with mode: 0644]
lib/amd/build/modal.min.js
lib/amd/build/modal.min.js.map
lib/amd/build/modal_cancel.min.js
lib/amd/build/modal_cancel.min.js.map
lib/amd/build/modal_factory.min.js
lib/amd/build/modal_factory.min.js.map
lib/amd/build/modal_registry.min.js
lib/amd/build/modal_registry.min.js.map
lib/amd/build/modal_save_cancel.min.js
lib/amd/build/modal_save_cancel.min.js.map
lib/amd/build/notification.min.js
lib/amd/build/notification.min.js.map
lib/amd/build/prefetch.min.js [new file with mode: 0644]
lib/amd/build/prefetch.min.js.map [new file with mode: 0644]
lib/amd/build/templates.min.js
lib/amd/build/templates.min.js.map
lib/amd/src/custom_interaction_events.js
lib/amd/src/form-autocomplete.js
lib/amd/src/local/modal/alert.js [new file with mode: 0644]
lib/amd/src/modal.js
lib/amd/src/modal_cancel.js
lib/amd/src/modal_factory.js
lib/amd/src/modal_registry.js
lib/amd/src/modal_save_cancel.js
lib/amd/src/notification.js
lib/amd/src/prefetch.js [new file with mode: 0644]
lib/amd/src/templates.js
lib/antivirus/clamav/lang/en/antivirus_clamav.php
lib/behat/classes/behat_core_generator.php
lib/behat/lib.php
lib/classes/component.php
lib/classes/dataformat.php [new file with mode: 0644]
lib/classes/dataformat/base.php
lib/classes/dataformat/spout_base.php
lib/classes/dml/sql_join.php
lib/classes/event/contentbank_content_created.php [new file with mode: 0644]
lib/classes/event/contentbank_content_deleted.php [new file with mode: 0644]
lib/classes/event/contentbank_content_updated.php [new file with mode: 0644]
lib/classes/event/contentbank_content_uploaded.php [new file with mode: 0644]
lib/classes/event/contentbank_content_viewed.php [new file with mode: 0644]
lib/classes/ip_utils.php
lib/classes/plugin_manager.php
lib/classes/session/redis.php
lib/classes/task/completion_daily_task.php
lib/classes/task/manager.php
lib/classes/task/messaging_cleanup_task.php
lib/dataformatlib.php
lib/db/access.php
lib/db/services.php
lib/db/upgrade.php
lib/editor/atto/plugins/accessibilitychecker/tests/behat/accessibilitychecker.feature
lib/editor/atto/plugins/bold/tests/behat/bold.feature
lib/editor/atto/plugins/bold/yui/build/moodle-atto_bold-button/moodle-atto_bold-button-debug.js
lib/editor/atto/plugins/bold/yui/build/moodle-atto_bold-button/moodle-atto_bold-button-min.js
lib/editor/atto/plugins/bold/yui/build/moodle-atto_bold-button/moodle-atto_bold-button.js
lib/editor/atto/plugins/bold/yui/src/button/js/button.js
lib/editor/atto/plugins/image/lang/en/atto_image.php
lib/editor/atto/plugins/image/tests/behat/image.feature
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
lib/editor/atto/plugins/image/yui/src/button/js/button.js
lib/editor/atto/plugins/italic/tests/behat/italic.feature
lib/editor/atto/plugins/italic/yui/build/moodle-atto_italic-button/moodle-atto_italic-button-debug.js
lib/editor/atto/plugins/italic/yui/build/moodle-atto_italic-button/moodle-atto_italic-button-min.js
lib/editor/atto/plugins/italic/yui/build/moodle-atto_italic-button/moodle-atto_italic-button.js
lib/editor/atto/plugins/italic/yui/src/button/js/button.js
lib/editor/atto/plugins/strike/tests/behat/strike.feature
lib/editor/atto/plugins/strike/yui/build/moodle-atto_strike-button/moodle-atto_strike-button-debug.js
lib/editor/atto/plugins/strike/yui/build/moodle-atto_strike-button/moodle-atto_strike-button-min.js
lib/editor/atto/plugins/strike/yui/build/moodle-atto_strike-button/moodle-atto_strike-button.js
lib/editor/atto/plugins/strike/yui/src/button/js/button.js
lib/editor/atto/tests/behat/customtoolbar.feature
lib/editor/atto/tests/behat/disablecontrol.feature
lib/form/checkbox.php
lib/form/course.php
lib/form/dateselector.php
lib/form/datetimeselector.php
lib/form/duration.php
lib/form/tests/behat/autocomplete.feature
lib/form/tests/behat/behat_core_form.php [new file with mode: 0644]
lib/form/tests/behat/fixtures/repeat_defaults_form.php [new file with mode: 0644]
lib/form/tests/behat/repeat_defaults.feature [new file with mode: 0644]
lib/form/tests/course_test.php [new file with mode: 0644]
lib/form/tests/fixtures/autocomplete-disabledif.php [new file with mode: 0644]
lib/formslib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrequirementslib.php
lib/plist/LICENSE [new file with mode: 0644]
lib/plist/README.md [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFBinaryPropertyList.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFPropertyList.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFType.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFTypeDetector.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/IOException.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/PListException.php [new file with mode: 0644]
lib/plist/readme_moodle.txt [new file with mode: 0644]
lib/questionlib.php
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/events.min.js [new file with mode: 0644]
lib/table/amd/build/local/dynamic/events.min.js.map [new file with mode: 0644]
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/build/local/dynamic/selectors.min.js
lib/table/amd/build/local/dynamic/selectors.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/events.js [moved from mod/quiz/accessrule/safebrowser/version.php with 70% similarity]
lib/table/amd/src/local/dynamic/repository.js
lib/table/amd/src/local/dynamic/selectors.js
lib/table/classes/dynamic.php
lib/table/classes/external/dynamic/fetch.php
lib/table/tests/external/dynamic/fetch_test.php
lib/tablelib.php
lib/templates/drawer.mustache
lib/templates/local/modal/alert.mustache [new file with mode: 0644]
lib/templates/single_select.mustache
lib/templates/url_select.mustache
lib/tests/accesslib_test.php
lib/tests/behat/action_modal.feature
lib/tests/behat/alpha_chooser.feature
lib/tests/behat/behat_navigation.php
lib/tests/behat/largeforms.feature
lib/tests/behat/locking.feature
lib/tests/behat/permissionmanager.feature
lib/tests/behat/readonlyform.feature
lib/tests/behat/securelayout.feature
lib/tests/completion_daily_task_test.php [new file with mode: 0644]
lib/tests/dataformat_test.php [new file with mode: 0644]
lib/tests/event/contentbank_content_created_test.php [new file with mode: 0644]
lib/tests/event/contentbank_content_deleted_test.php [new file with mode: 0644]
lib/tests/event/contentbank_content_updated_test.php [new file with mode: 0644]
lib/tests/event/contentbank_content_uploaded_test.php [new file with mode: 0644]
lib/tests/event/contentbank_content_viewed_test.php [new file with mode: 0644]
lib/tests/questionlib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/xapi/classes/local/statement.php
lib/xapi/classes/local/statement/item_attachment.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_context.php [moved from mod/quiz/accessrule/safebrowser/classes/privacy/provider.php with 56% similarity]
lib/xapi/classes/local/statement/item_result.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_score.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_attachment_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_context_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_result_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_score_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement_test.php
media/player/videojs/amd/build/Youtube-lazy.min.js
media/player/videojs/amd/build/Youtube-lazy.min.js.map
media/player/videojs/amd/build/video-lazy.min.js
media/player/videojs/amd/build/video-lazy.min.js.map
media/player/videojs/amd/build/videojs-flash-lazy.min.js
media/player/videojs/amd/build/videojs-flash-lazy.min.js.map
media/player/videojs/amd/src/Youtube-lazy.js
media/player/videojs/amd/src/video-lazy.js
media/player/videojs/amd/src/videojs-flash-lazy.js
media/player/videojs/classes/plugin.php
media/player/videojs/fonts/VideoJS.svg [changed mode: 0644->0755]
media/player/videojs/fonts/VideoJS.ttf [changed mode: 0644->0755]
media/player/videojs/fonts/VideoJS.woff [changed mode: 0644->0755]
media/player/videojs/readme_moodle.txt
media/player/videojs/tests/player_test.php
media/player/videojs/thirdpartylibs.xml
media/player/videojs/videojs/lang/ar.js
media/player/videojs/videojs/lang/ar.json
media/player/videojs/videojs/lang/de.js
media/player/videojs/videojs/lang/de.json
media/player/videojs/videojs/lang/en.js
media/player/videojs/videojs/lang/en.json
media/player/videojs/videojs/lang/fa.js
media/player/videojs/videojs/lang/fa.json
media/player/videojs/videojs/lang/gd.json [changed mode: 0644->0755]
media/player/videojs/videojs/lang/nb.js
media/player/videojs/videojs/lang/nb.json
media/player/videojs/videojs/lang/nn.js
media/player/videojs/videojs/lang/nn.json
message/amd/build/message_drawer.min.js
message/amd/build/message_drawer.min.js.map
message/amd/build/message_drawer_helper.min.js
message/amd/build/message_drawer_helper.min.js.map
message/amd/build/message_popover.min.js
message/amd/build/message_popover.min.js.map
message/amd/build/message_preferences.min.js [new file with mode: 0644]
message/amd/build/message_preferences.min.js.map [new file with mode: 0644]
message/amd/build/message_user_button.min.js
message/amd/build/message_user_button.min.js.map
message/amd/src/message_drawer.js
message/amd/src/message_drawer_helper.js
message/amd/src/message_popover.js
message/amd/src/message_preferences.js [new file with mode: 0644]
message/amd/src/message_user_button.js
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/airnotifier/settings.php
message/output/email/classes/task/send_email_task.php
message/output/email/lang/en/deprecated.txt [new file with mode: 0644]
message/output/email/lang/en/message_email.php
message/output/email/tests/send_email_task_test.php
message/output/lib.php
message/output/popup/db/upgrade.php
message/output/popup/message_output_popup.php
message/output/popup/tests/messaging_cleanup_test.php [new file with mode: 0644]
message/output/popup/version.php
message/templates/message_jumpto.mustache [new file with mode: 0644]
message/templates/message_popover.mustache
message/templates/message_preferences.mustache [new file with mode: 0644]
message/templates/message_preferences_component.mustache [new file with mode: 0644]
message/templates/message_preferences_notification_processor.mustache [new file with mode: 0644]
message/tests/behat/message_preferences.feature [new file with mode: 0644]
message/upgrade.txt
mod/assign/feedback/editpdf/tests/behat/view_previous_annotations.feature
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/tests/behat/edit_previous_feedback.feature
mod/chat/lib.php
mod/feedback/classes/responses_table.php
mod/folder/lib.php
mod/forum/export.php
mod/forum/lib.php
mod/forum/report/summary/templates/bulk_action_menu.mustache
mod/glossary/lib.php
mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php
mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php
mod/h5pactivity/classes/local/attempt.php
mod/h5pactivity/classes/privacy/provider.php
mod/h5pactivity/db/install.xml
mod/h5pactivity/db/upgrade.php
mod/h5pactivity/lang/en/h5pactivity.php
mod/h5pactivity/lib.php
mod/h5pactivity/tests/behat/add_h5pactivity.feature
mod/h5pactivity/tests/behat/sending_attempt.feature
mod/h5pactivity/tests/local/attempt_test.php
mod/h5pactivity/tests/privacy_test.php
mod/h5pactivity/tests/restore_test.php [new file with mode: 0644]
mod/h5pactivity/tests/xapi/handler_test.php
mod/h5pactivity/version.php
mod/lesson/lang/en/lesson.php
mod/lti/lang/en/lti.php
mod/lti/mod_form.php
mod/quiz/accessrule/safebrowser/rule.php [deleted file]
mod/quiz/accessrule/safebrowser/tests/rule_test.php [deleted file]
mod/quiz/accessrule/seb/amd/build/managetemplates.min.js [new file with mode: 0644]
mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map [new file with mode: 0644]
mod/quiz/accessrule/seb/amd/src/managetemplates.js [new file with mode: 0644]
mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php [new file with mode: 0644]
mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/access_manager.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/config_key.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/access_prevented.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_created.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_deleted.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_disabled.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_enabled.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_updated.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/helper.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/hideif_rule.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/link_generator.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/local/form/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/local/table/template_list.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/privacy/provider.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/property_list.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/quiz_settings.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/settings_provider.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/template_controller.php [new file with mode: 0644]
mod/quiz/accessrule/seb/config.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/access.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/caches.php [moved from mod/quiz/accessrule/safebrowser/lang/en/quizaccess_safebrowser.php with 53% similarity]
mod/quiz/accessrule/seb/db/install.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/install.xml [new file with mode: 0644]
mod/quiz/accessrule/seb/db/upgrade.php [moved from enrol/database/settingslib.php with 55% similarity]
mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php [new file with mode: 0644]
mod/quiz/accessrule/seb/lib.php [new file with mode: 0644]
mod/quiz/accessrule/seb/rule.php [new file with mode: 0644]
mod/quiz/accessrule/seb/settings.php [new file with mode: 0644]
mod/quiz/accessrule/seb/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/behat/edit_form.feature [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/generator/behat_quizaccess_seb_generator.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/generator/lib.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/access_manager_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/backup_restore_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/base.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/config_key_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/event_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/helper_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/hideif_rule_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/link_generator_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/privacy_provider_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/property_list_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/quiz_settings_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/rule_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/JSON_unencrypted_mac_001.txt [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/encrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencryptedwithoutoriginator.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted_mac_001.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted_win_223.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/settings_provider_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/template_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/version.php [new file with mode: 0644]
mod/quiz/accessrule/timelimit/lang/en/quizaccess_timelimit.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/lang/en/quiz.php
mod/quiz/locallib.php
mod/quiz/tests/behat/attempt_begin.feature
mod/scorm/lang/en/scorm.php
mod/scorm/locallib.php
mod/survey/lib.php
mod/wiki/lib.php
mod/workshop/lib.php
mod/workshop/tests/behat/file_type_restriction.feature
portfolio/googledocs/lib.php
portfolio/googledocs/upgrade.txt [new file with mode: 0644]
privacy/classes/tests/request/content_writer.php
question/format/gift/format.php
question/format/gift/tests/giftformat_test.php
question/type/ddimageortext/lang/en/qtype_ddimageortext.php
question/type/ddmarker/amd/build/form.min.js
question/type/ddmarker/amd/build/form.min.js.map
question/type/ddmarker/amd/build/question.min.js
question/type/ddmarker/amd/build/question.min.js.map
question/type/ddmarker/amd/build/shapes.min.js
question/type/ddmarker/amd/build/shapes.min.js.map
question/type/ddmarker/amd/src/form.js
question/type/ddmarker/amd/src/question.js
question/type/ddmarker/amd/src/shapes.js
question/type/ddmarker/lang/en/qtype_ddmarker.php
question/type/ddmarker/renderer.php
question/type/ddmarker/styles.css
question/type/ddmarker/tests/behat/behat_qtype_ddmarker.php
question/type/ddmarker/tests/behat/preview.feature
question/type/ddmarker/tests/walkthrough_test.php
tag/tests/behat/delete_tag.feature
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/grade.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/action_redir.php
user/amd/build/participants.min.js
user/amd/build/participants.min.js.map
user/amd/build/repository.min.js [new file with mode: 0644]
user/amd/build/repository.min.js.map [new file with mode: 0644]
user/amd/build/status_field.min.js
user/amd/build/status_field.min.js.map
user/amd/src/participants.js
user/amd/src/repository.js [new file with mode: 0644]
user/amd/src/status_field.js
user/classes/table/participants.php
user/index.php
user/tests/behat/course_preference.feature
user/tests/behat/view_participants.feature
version.php

index 2ef8a38..b9c0b6a 100644 (file)
@@ -65,6 +65,7 @@ lib/php-jwt/
 lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
+lib/plist/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index f00e696..5d9e5c1 100644 (file)
@@ -66,6 +66,7 @@ lib/php-jwt/
 lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
+lib/plist/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index 245d7fc..f60032e 100644 (file)
@@ -18,7 +18,7 @@ services:
 
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
-    - 7.3
+    - 7.4
     - 7.2
 
 addons:
@@ -48,11 +48,11 @@ jobs:
     fast_finish: true
 
     include:
-          # Run mysql only on 7.3 - it's just too slow
-        - php: 7.3
+          # Run mysql only on highest - it's just too slow
+        - php: 7.4
           env: DB=mysqli   TASK=PHPUNIT
-          # Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
-        - php: 7.2
+          # Run grunt/npm install on highest version too ('node' is an alias for the latest node.js version.)
+        - php: 7.4
           env: DB=none     TASK=GRUNT   NVM_VERSION='lts/carbon'
 
 cache:
index 5931a26..45f46a5 100644 (file)
@@ -361,14 +361,15 @@ class core_admin_renderer extends plugin_renderer_base {
         $output = '';
 
         if ($checker->enabled()) {
-            $output .= $this->container_start('checkforupdates');
+            $output .= $this->container_start('checkforupdates mb-4');
             $output .= $this->single_button(
                 new moodle_url($reloadurl, array('fetchupdates' => 1)),
                 get_string('checkforupdates', 'core_plugin')
             );
             if ($timefetched = $checker->get_last_timefetched()) {
                 $timefetched = userdate($timefetched, get_string('strftimedatetime', 'core_langconfig'));
-                $output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched), 'lasttimefetched');
+                $output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched),
+                    'lasttimefetched small text-muted mt-1');
             }
             $output .= $this->container_end();
         }
@@ -1045,7 +1046,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 $displayname = new html_table_cell(
                     $icon.
                     html_writer::span($plugin->displayname, 'pluginname').
-                    html_writer::div($plugin->get_dir(), 'plugindir')
+                    html_writer::div($plugin->get_dir(), 'plugindir text-muted small')
                 );
 
                 $versiondb = new html_table_cell($plugin->versiondb);
@@ -1082,7 +1083,7 @@ class core_admin_renderer extends plugin_renderer_base {
                         break;
                     case core_plugin_manager::PLUGIN_STATUS_NODB:
                     case core_plugin_manager::PLUGIN_STATUS_UPTODATE:
-                        $statusclass .= $dependenciesok ? '' : 'badge-warning';
+                        $statusclass .= $dependenciesok ? 'badge-light' : 'badge-warning';
                         break;
                 }
                 $status = html_writer::span(get_string('status_' . $statuscode, 'core_plugin'), $statusclass);
@@ -1092,7 +1093,7 @@ class core_admin_renderer extends plugin_renderer_base {
                         new moodle_url($this->page->url, array('abortinstall' => $plugin->component)),
                         get_string('cancelinstallone', 'core_plugin'),
                         'post',
-                        array('class' => 'actionbutton cancelinstallone')
+                        array('class' => 'actionbutton cancelinstallone d-block mt-1')
                     );
                 }
 
@@ -1101,7 +1102,7 @@ class core_admin_renderer extends plugin_renderer_base {
                         new moodle_url($this->page->url, array('abortupgrade' => $plugin->component)),
                         get_string('cancelupgradeone', 'core_plugin'),
                         'post',
-                        array('class' => 'actionbutton cancelupgradeone')
+                        array('class' => 'actionbutton cancelupgradeone d-block mt-1')
                     );
                 }
 
@@ -1174,7 +1175,7 @@ class core_admin_renderer extends plugin_renderer_base {
             }
         }
 
-        $out .= $this->output->container_start('actions');
+        $out .= $this->output->container_start('actions mb-2');
 
         $installableupdates = $pluginman->filter_installable($pluginman->available_updates());
         if ($installableupdates) {
@@ -1182,7 +1183,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 new moodle_url($this->page->url, array('installupdatex' => 1)),
                 get_string('updateavailableinstallall', 'core_admin', count($installableupdates)),
                 'post',
-                array('class' => 'singlebutton updateavailableinstallall')
+                array('class' => 'singlebutton updateavailableinstallall mr-1')
             );
         }
 
@@ -1191,7 +1192,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 new moodle_url($this->page->url, array('abortinstallx' => 1)),
                 get_string('cancelinstallall', 'core_plugin', count($installabortable)),
                 'post',
-                array('class' => 'singlebutton cancelinstallall')
+                array('class' => 'singlebutton cancelinstallall mr-1')
             );
         }
 
@@ -1200,15 +1201,17 @@ class core_admin_renderer extends plugin_renderer_base {
                 new moodle_url($this->page->url, array('abortupgradex' => 1)),
                 get_string('cancelupgradeall', 'core_plugin', count($upgradeabortable)),
                 'post',
-                array('class' => 'singlebutton cancelupgradeall')
+                array('class' => 'singlebutton cancelupgradeall mr-1')
             );
         }
 
         $out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 0)),
-            get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge'));
+            get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge badge-light'),
+            'btn btn-link mr-1');
 
         $out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 1)),
-            get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge'));
+            get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge badge-light'),
+            'btn btn-link mr-1');
 
         $out .= $this->output->container_end(); // End of .actions container.
         $out .= $this->output->container_end(); // End of #plugins-check-info container.
@@ -1276,12 +1279,13 @@ class core_admin_renderer extends plugin_renderer_base {
             }
         }
 
-        $out  = $this->output->container_start('plugins-check-dependencies');
+        $out  = $this->output->container_start('plugins-check-dependencies mb-4');
 
         if ($unavailable or $unknown) {
             $out .= $this->output->heading(get_string('misdepsunavail', 'core_plugin'));
             if ($unknown) {
-                $out .= $this->output->notification(get_string('misdepsunknownlist', 'core_plugin', implode(', ', $unknown)));
+                $out .= $this->output->render((new \core\output\notification(get_string('misdepsunknownlist', 'core_plugin',
+                    implode(', ', $unknown))))->set_show_closebutton(false));
             }
             if ($unavailable) {
                 $unavailablelist = array();
@@ -1295,18 +1299,18 @@ class core_admin_renderer extends plugin_renderer_base {
                     }
                     $unavailablelist[] = $unavailablelistitem;
                 }
-                $out .= $this->output->notification(get_string('misdepsunavaillist', 'core_plugin',
-                    implode(', ', $unavailablelist)));
+                $out .= $this->output->render((new \core\output\notification(get_string('misdepsunavaillist', 'core_plugin',
+                    implode(', ', $unavailablelist))))->set_show_closebutton(false));
             }
-            $out .= $this->output->container_start('plugins-check-dependencies-actions');
+            $out .= $this->output->container_start('plugins-check-dependencies-actions mb-4');
             $out .= ' '.html_writer::link(new moodle_url('/admin/tool/installaddon/'),
-                get_string('dependencyuploadmissing', 'core_plugin'));
+                get_string('dependencyuploadmissing', 'core_plugin'), array('class' => 'btn btn-secondary'));
             $out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
         }
 
         if ($available) {
             $out .= $this->output->heading(get_string('misdepsavail', 'core_plugin'));
-            $out .= $this->output->container_start('plugins-check-dependencies-actions');
+            $out .= $this->output->container_start('plugins-check-dependencies-actions mb-2');
 
             $installable = $pluginman->filter_installable($available);
             if ($installable) {
@@ -1314,12 +1318,13 @@ class core_admin_renderer extends plugin_renderer_base {
                     new moodle_url($this->page->url, array('installdepx' => 1)),
                     get_string('dependencyinstallmissing', 'core_plugin', count($installable)),
                     'post',
-                    array('class' => 'singlebutton dependencyinstallmissing')
+                    array('class' => 'singlebutton dependencyinstallmissing d-inline-block mr-1')
                 );
             }
 
             $out .= html_writer::div(html_writer::link(new moodle_url('/admin/tool/installaddon/'),
-                get_string('dependencyuploadmissing', 'core_plugin')), 'dependencyuploadmissing');
+                get_string('dependencyuploadmissing', 'core_plugin'), array('class' => 'btn btn-link')),
+                'dependencyuploadmissing d-inline-block mr-1');
 
             $out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
 
@@ -1360,7 +1365,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 if ($CFG->branch == str_replace('.', '', $moodle->release)) {
                     $supportedmoodles[] = html_writer::span($moodle->release, 'badge badge-success');
                 } else {
-                    $supportedmoodles[] = html_writer::span($moodle->release, 'label');
+                    $supportedmoodles[] = html_writer::span($moodle->release, 'badge badge-light');
                 }
             }
 
@@ -1374,7 +1379,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 }
                 $info = html_writer::div(
                     get_string('requiredby', 'core_plugin', implode(', ', $requriedby)),
-                    'requiredby'
+                    'requiredby mb-1'
                 );
             } else {
                 $info = '';
@@ -1385,35 +1390,36 @@ class core_admin_renderer extends plugin_renderer_base {
             $info .= html_writer::div(
                 html_writer::link('https://moodle.org/plugins/view.php?plugin='.$plugin->component,
                     get_string('misdepinfoplugin', 'core_plugin')),
-                'misdepinfoplugin'
+                'misdepinfoplugin d-inline-block mr-3 mb-1'
             );
 
             $info .= html_writer::div(
                 html_writer::link('https://moodle.org/plugins/pluginversion.php?id='.$plugin->version->id,
                     get_string('misdepinfoversion', 'core_plugin')),
-                'misdepinfoversion'
+                'misdepinfoversion d-inline-block mr-3 mb-1'
             );
 
-            $info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')), 'misdepdownload');
+            $info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')),
+                'misdepdownload d-inline-block mr-3 mb-1');
 
             if ($pluginman->is_remote_plugin_installable($plugin->component, $plugin->version->version, $reason)) {
                 $info .= $this->output->single_button(
                     new moodle_url($this->page->url, array('installdep' => $plugin->component)),
                     get_string('dependencyinstall', 'core_plugin'),
                     'post',
-                    array('class' => 'singlebutton dependencyinstall')
+                    array('class' => 'singlebutton dependencyinstall mr-3 mb-1')
                 );
             } else {
                 $reasonhelp = $this->info_remote_plugin_not_installable($reason);
                 if ($reasonhelp) {
-                    $info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall');
+                    $info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall d-inline-block mr-3 mb-1');
                 }
             }
 
             $info .= $this->output->container_end(); // End of .actions container.
 
             $table->data[] = array(
-                html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component'),
+                html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component text-muted small'),
                 $plugin->version->release,
                 $plugin->version->version,
                 implode(' ', $supportedmoodles),
@@ -1461,7 +1467,7 @@ class core_admin_renderer extends plugin_renderer_base {
         foreach ($requirements as $reqname => $reqinfo) {
             if ($reqname === 'core') {
                 if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
-                    $class = 'requires-ok';
+                    $class = 'requires-ok text-muted';
                     $label = '';
                 } else {
                     $class = 'requires-failed';
@@ -1490,7 +1496,7 @@ class core_admin_renderer extends plugin_renderer_base {
 
                 if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
                     $label = '';
-                    $class = 'requires-ok';
+                    $class = 'requires-ok text-muted';
 
                 } else if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_MISSING) {
                     if ($reqinfo->availability == $pluginman::REQUIREMENT_AVAILABLE) {
@@ -1544,13 +1550,14 @@ class core_admin_renderer extends plugin_renderer_base {
             return '';
         }
 
-        $out = html_writer::tag('ul', implode("\n", $requires));
+        $out = html_writer::tag('ul', implode("\n", $requires), array('class' => 'm-0'));
 
         if ($displayuploadlink) {
             $out .= html_writer::div(
                 html_writer::link(
                     new moodle_url('/admin/tool/installaddon/'),
-                    get_string('dependencyuploadmissing', 'core_plugin')
+                    get_string('dependencyuploadmissing', 'core_plugin'),
+                    array('class' => 'btn btn-secondary btn-sm m-1')
                 ),
                 'dependencyuploadmissing'
             );
@@ -1560,7 +1567,8 @@ class core_admin_renderer extends plugin_renderer_base {
             $out .= html_writer::div(
                 html_writer::link(
                     new moodle_url($this->page->url, array('sesskey' => sesskey(), 'fetchupdates' => 1)),
-                    get_string('checkforupdates', 'core_plugin')
+                    get_string('checkforupdates', 'core_plugin'),
+                    array('class' => 'btn btn-secondary btn-sm m-1')
                 ),
                 'checkforupdates'
             );
index f8a5ceb..46206fc 100644 (file)
@@ -10,7 +10,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp = new admin_settingpage('experimentalsettings', new lang_string('experimentalsettings', 'admin'));
     //TODO: Re-enable cc-import once re-implemented in 2.0.x
     //$temp->add(new admin_setting_configcheckbox('enableimsccimport', new lang_string('enable_cc_import', 'imscc'), new lang_string('enable_cc_import_description', 'imscc'), 0));
-    $temp->add(new admin_setting_configcheckbox('enablesafebrowserintegration', new lang_string('enablesafebrowserintegration', 'admin'), new lang_string('configenablesafebrowserintegration', 'admin'), 0));
 
     $temp->add(new admin_setting_configcheckbox('dndallowtextandlinks', new lang_string('dndallowtextandlinks', 'admin'), new lang_string('configdndallowtextandlinks', 'admin'), 0));
 
index 8daab15..fd0b465 100644 (file)
@@ -24,7 +24,7 @@
 
 $string['classname'] = 'Class name';
 $string['component'] = 'Component';
-$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:587. If a port isn\'t specified, the default port for the type of mail server will be used.';
+$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:993. If a port isn\'t specified, the default port for the type of mail server will be used.';
 $string['defaultexpiration'] = 'Default address expiry period';
 $string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
 $string['description'] = 'Description';
index c6160d3..1136bd0 100644 (file)
@@ -49,6 +49,8 @@ class api {
     const LOGIN_VIA_EMBEDDED_BROWSER = 3;
     /** @var int seconds an auto-login key will expire. */
     const LOGIN_KEY_TTL = 60;
+    /** @var string URL of the Moodle Apps Portal */
+    const MOODLE_APPS_PORTAL_URL = 'https://apps.moodle.com';
 
     /**
      * Returns a list of Moodle plugins supporting the mobile app.
index 092f7da..ccc6867 100644 (file)
@@ -91,6 +91,7 @@ $string['mobilecssurl'] = 'CSS';
 $string['mobilefeatures'] = 'Mobile features';
 $string['mobilenotificationsdisabledwarning'] = 'Mobile notifications are not enabled. They should be enabled in Notification settings.';
 $string['mobilesettings'] = 'Mobile settings';
+$string['moodleappsportalfeatureswarning'] = 'Please note that some features may be restricted depending on your Moodle app subscription. For details, visit the <a href="{$a}" target="_blank">Moodle Apps Portal</a>.';
 $string['oauth2identityproviders'] = 'OAuth 2 identity providers';
 $string['offlineuse'] = 'Offline use';
 $string['pluginname'] = 'Moodle app tools';
index a9cdbcf..05ed4aa 100644 (file)
@@ -50,6 +50,12 @@ if ($hassiteconfig) {
 
     // Show only mobile settings if the mobile service is enabled.
     if (!empty($CFG->enablemobilewebservice)) {
+        // General notification about limited features due to app restrictions.
+        $notify = new \core\output\notification(
+            get_string('moodleappsportalfeatureswarning', 'tool_mobile', tool_mobile\api::MOODLE_APPS_PORTAL_URL),
+            \core\output\notification::NOTIFY_WARNING);
+        $featuresnotice = $OUTPUT->render($notify);
+
         // Type of login.
         $temp = new admin_settingpage('mobileauthentication', new lang_string('mobileauthentication', 'tool_mobile'));
         $options = array(
@@ -74,6 +80,8 @@ if ($hassiteconfig) {
         // Appearance related settings.
         $temp = new admin_settingpage('mobileappearance', new lang_string('mobileappearance', 'tool_mobile'));
 
+        $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesappearance', '', $featuresnotice));
+
         $temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'tool_mobile'),
                     new lang_string('configmobilecssurl', 'tool_mobile'), '', PARAM_URL));
 
@@ -106,6 +114,8 @@ if ($hassiteconfig) {
         // Features related settings.
         $temp = new admin_settingpage('mobilefeatures', new lang_string('mobilefeatures', 'tool_mobile'));
 
+        $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeatures', '', $featuresnotice));
+
         $temp->add(new admin_setting_heading('tool_mobile/logout',
                     new lang_string('logout'), ''));
 
index 9ccd292..299603f 100644 (file)
  */
 
 $string['asap'] = 'ASAP';
-$string['adhocempty'] = 'Adhoc task queue is empty';
-$string['adhocqueuesize'] = 'Adhoc task queue has {$a} tasks';
+$string['adhocempty'] = 'Ad hoc task queue is empty';
+$string['adhocqueuesize'] = 'Ad hoc task queue has {$a} tasks';
 $string['adhocqueueold'] = 'Oldest task is {$a->age} which is more than {$a->max}';
 $string['backtoscheduledtasks'] = 'Back to scheduled tasks';
 $string['blocking'] = 'Blocking';
 $string['cannotfindthepathtothecli'] = 'Cannot find the path to the PHP CLI executable so task execution aborted. Set the \'Path to PHP CLI\' setting in Site administration / Server / System paths.';
-$string['checkadhocqueue'] = 'Adhoc task queue';
+$string['checkadhocqueue'] = 'Ad hoc task queue';
 $string['checkcronrunning'] = 'Cron running';
 $string['checkmaxfaildelay'] = 'Tasks max fail delay';
 $string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail delay for task \'{$a}\'? After clearing the delay, the task will run according to its normal schedule.';
@@ -58,7 +58,7 @@ $string['runpattern'] = 'Run pattern';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
-$string['taskfailures'] = 'There are {$a} task(s) failing';
+$string['taskfailures'] = '{$a} task(s) failing';
 $string['tasklogs'] = 'Task logs';
 $string['tasknofailures'] = 'There are no tasks failing';
 $string['taskscheduleday'] = 'Day';
index 8ae7a8d..88cd888 100644 (file)
@@ -284,6 +284,15 @@ class tool_uploadcourse_course {
         return $this->errors;
     }
 
+    /**
+     * Return array of valid fields for default values
+     *
+     * @return array
+     */
+    protected function get_valid_fields() {
+        return array_merge(self::$validfields, \tool_uploadcourse_helper::get_custom_course_field_names());
+    }
+
     /**
      * Assemble the course data based on defaults.
      *
@@ -293,7 +302,7 @@ class tool_uploadcourse_course {
      * @return array
      */
     protected function get_final_create_data($data) {
-        foreach (self::$validfields as $field) {
+        foreach ($this->get_valid_fields() as $field) {
             if (!isset($data[$field]) && isset($this->defaults[$field])) {
                 $data[$field] = $this->defaults[$field];
             }
@@ -316,9 +325,9 @@ class tool_uploadcourse_course {
         global $DB;
         $newdata = array();
         $existingdata = $DB->get_record('course', array('shortname' => $this->shortname));
-        foreach (self::$validfields as $field) {
+        foreach ($this->get_valid_fields() as $field) {
             if ($missingonly) {
-                if (!is_null($existingdata->$field) and $existingdata->$field !== '') {
+                if (isset($existingdata->$field) and $existingdata->$field !== '') {
                     continue;
                 }
             }
@@ -699,6 +708,27 @@ class tool_uploadcourse_course {
             $coursedata[$rolekey] = $rolename;
         }
 
+        // Custom fields. If the course already exists and mode isn't set to force creation, we can use its context.
+        if ($exists && $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL) {
+            $context = context_course::instance($coursedata['id']);
+        } else {
+            // The category ID is taken from the defaults if it exists, otherwise from course data.
+            $context = context_coursecat::instance($this->defaults['category'] ?? $coursedata['category']);
+        }
+        $customfielddata = tool_uploadcourse_helper::get_custom_course_field_data($this->rawdata, $this->defaults, $context,
+            $errors);
+        if (!empty($errors)) {
+            foreach ($errors as $key => $message) {
+                $this->error($key, $message);
+            }
+
+            return false;
+        }
+
+        foreach ($customfielddata as $name => $value) {
+            $coursedata[$name] = $value;
+        }
+
         // Some validation.
         if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) {
             $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse'));
index 2325c9c..1011c37 100644 (file)
@@ -337,6 +337,103 @@ class tool_uploadcourse_helper {
         return $rolenames;
     }
 
+    /**
+     * Return array of all custom course fields indexed by their shortname
+     *
+     * @return \core_customfield\field_controller[]
+     */
+    public static function get_custom_course_fields(): array {
+        $result = [];
+
+        $fields = \core_course\customfield\course_handler::create()->get_fields();
+        foreach ($fields as $field) {
+            $result[$field->get('shortname')] = $field;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Return array of custom field element names
+     *
+     * @return string[]
+     */
+    public static function get_custom_course_field_names(): array {
+        $result = [];
+
+        $fields = self::get_custom_course_fields();
+        foreach ($fields as $field) {
+            $controller = \core_customfield\data_controller::create(0, null, $field);
+            $result[] = $controller->get_form_element_name();
+        }
+
+        return $result;
+    }
+
+    /**
+     * Return any elements from passed $data whose key matches one of the custom course fields defined for the site
+     *
+     * @param array $data
+     * @param array $defaults
+     * @param context $context
+     * @param array $errors Will be populated with any errors
+     * @return array
+     */
+    public static function get_custom_course_field_data(array $data, array $defaults, context $context,
+            array &$errors = []): array {
+
+        $fields = self::get_custom_course_fields();
+        $result = [];
+
+        $canchangelockedfields = guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context);
+
+        foreach ($data as $name => $originalvalue) {
+            if (preg_match('/^customfield_(?<name>.*)?$/', $name, $matches)
+                    && isset($fields[$matches['name']])) {
+
+                $fieldname = $matches['name'];
+                $field = $fields[$fieldname];
+
+                // Skip field if it's locked and user doesn't have capability to change locked fields.
+                if ($field->get_configdata_property('locked') && !$canchangelockedfields) {
+                    continue;
+                }
+
+                // Create field data controller.
+                $controller = \core_customfield\data_controller::create(0, null, $field);
+                $controller->set('id', 1);
+
+                $defaultvalue = $defaults["customfield_{$fieldname}"] ?? $controller->get_default_value();
+                $value = (empty($originalvalue) ? $defaultvalue : $field->parse_value($originalvalue));
+
+                // If we initially had a value, but now don't, then reset it to the default.
+                if (!empty($originalvalue) && empty($value)) {
+                    $value = $defaultvalue;
+                }
+
+                // Validate data with controller.
+                $fieldformdata = [$controller->get_form_element_name() => $value];
+                $validationerrors = $controller->instance_form_validation($fieldformdata, []);
+                if (count($validationerrors) > 0) {
+                    $errors['customfieldinvalid'] = new lang_string('customfieldinvalid', 'tool_uploadcourse',
+                        $field->get_formatted_name());
+
+                    continue;
+                }
+
+                $controller->set($controller->datafield(), $value);
+
+                // Pass an empty object to the data controller, which will transform it to a correct name/value pair.
+                $instance = new stdClass();
+                $controller->instance_form_before_set_data($instance);
+
+                $result = array_merge($result, (array) $instance);
+            }
+        }
+
+        return $result;
+    }
+
     /**
      * Helper to increment an ID number.
      *
@@ -493,5 +590,4 @@ class tool_uploadcourse_helper {
         }
         return $id;
     }
-
-}
+}
\ No newline at end of file
index 58c39d4..8854abb 100644 (file)
@@ -173,6 +173,10 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             $mform->addHelpButton('defaults[enablecompletion]', 'enablecompletion', 'completion');
         }
 
+        // Add custom fields to the form.
+        $handler = \core_course\customfield\course_handler::create();
+        $handler->instance_form_definition($mform, 0, 'defaultvaluescustomfieldcategory', 'tool_uploadcourse');
+
         // Hidden fields.
         $mform->addElement('hidden', 'importid');
         $mform->setType('importid', PARAM_INT);
@@ -182,6 +186,10 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
 
         $this->add_action_buttons(true, get_string('uploadcourses', 'tool_uploadcourse'));
 
+        // Prepare custom fields data.
+        $data = (object) $data;
+        $handler->instance_form_before_set_data($data);
+
         $this->set_data($data);
     }
 
@@ -219,6 +227,9 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             $enddate = $format->get_default_course_enddate($mform, array('startdate' => 'defaults[startdate]'));
             $mform->setDefault('defaults[enddate]', $enddate);
         }
+
+        // Tweak the form with values provided by custom fields in use.
+        \core_course\customfield\course_handler::create()->instance_form_definition_after_data($mform);
     }
 
     /**
@@ -237,6 +248,9 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             $errors['defaults[enddate]'] = get_string($errorcode, 'error');
         }
 
+        // Custom fields validation.
+        array_merge($errors, \core_course\customfield\course_handler::create()->instance_form_validation($data, $files));
+
         return $errors;
     }
 }
index 69f4410..8c2ccfd 100644 (file)
@@ -78,6 +78,12 @@ if ($form2data = $mform2->is_cancelled()) {
     $options = (array) $form2data->options;
     $defaults = (array) $form2data->defaults;
 
+    // Custom field defaults.
+    $customfields = tool_uploadcourse_helper::get_custom_course_field_names();
+    foreach ($customfields as $customfield) {
+        $defaults[$customfield] = $form2data->{$customfield};
+    }
+
     // Restorefile deserves its own logic because formslib does not really appreciate
     // when the name of a filepicker is an array...
     $options['restorefile'] = '';
index 9a0e2c6..b44b4f3 100644 (file)
@@ -75,6 +75,7 @@ $string['csvdelimiter_help'] = 'CSV delimiter of the CSV file.';
 $string['csvfileerror'] = 'There is something wrong with the format of the CSV file. Please check the number of headings and columns match, and that the delimiter and file encoding are correct: {$a}';
 $string['csvline'] = 'Line';
 $string['defaultvalues'] = 'Default course values';
+$string['defaultvaluescustomfieldcategory'] = 'Default values for \'{$a}\'';
 $string['encoding'] = 'Encoding';
 $string['encoding_help'] = 'Encoding of the CSV file.';
 $string['errorwhilerestoringcourse'] = 'Error while restoring the course';
@@ -102,6 +103,7 @@ $string['mode_help'] = 'This allows you to specify if courses can be created and
 $string['nochanges'] = 'No changes';
 $string['pluginname'] = 'Course upload';
 $string['preview'] = 'Preview';
+$string['customfieldinvalid'] = 'Custom field \'{$a}\' is empty or contains invalid data';
 $string['reset'] = 'Reset course after upload';
 $string['reset_help'] = 'Whether to reset the course after creating/updating it.';
 $string['result'] = 'Result';
index 9fbfa01..a17fc2f 100644 (file)
@@ -42,3 +42,66 @@ Feature: An admin can create courses using a CSV file
     And I should see "Course 1"
     And I should see "Course 2"
     And I should see "Course 3"
+
+  @javascript
+  Scenario: Creation of new courses with custom fields
+    Given the following "custom field categories" exist:
+      | name  | component   | area   | itemid |
+      | Other | core_course | course | 0      |
+    And the following "custom fields" exist:
+      | name    | category | type     | shortname | configdata            |
+      | Field 1 | Other    | checkbox | checkbox  |                       |
+      | Field 2 | Other    | date     | date      |                       |
+      | Field 3 | Other    | select   | select    | {"options":"a\nb\nc"} |
+      | Field 4 | Other    | text     | text      |                       |
+      | Field 5 | Other    | textarea | textarea  |                       |
+    When I upload "admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Create new courses only, skip existing ones"
+    And I click on "Preview" "button"
+    And I click on "Upload courses" "button"
+    Then I should see "Course created"
+    And I should see "Courses created: 1"
+    And I am on site homepage
+    And I should see "Course fields 1"
+    And I should see "Field 1: Yes"
+    And I should see "Field 2: Tuesday, 1 October 2019, 2:00"
+    And I should see "Field 3: b"
+    And I should see "Field 4: Hello"
+    And I should see "Field 5: Goodbye"
+
+  @javascript
+  Scenario: Creation of new courses with custom fields using defaults
+    Given the following "custom field categories" exist:
+      | name  | component   | area   | itemid |
+      | Other | core_course | course | 0      |
+    And the following "custom fields" exist:
+      | name    | category | type     | shortname | configdata                                          |
+      | Field 1 | Other    | checkbox | checkbox  | {"checkbydefault":1}                                |
+      | Field 2 | Other    | date     | date      | {"includetime":0}                                   |
+      | Field 3 | Other    | select   | select    | {"options":"a\nb\nc","defaultvalue":"b"}            |
+      | Field 4 | Other    | text     | text      | {"defaultvalue":"Hello"}                            |
+      | Field 5 | Other    | textarea | textarea  | {"defaultvalue":"Some text","defaultvalueformat":1} |
+    When I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Create all, increment shortname if needed"
+    And I click on "Preview" "button"
+    And I expand all fieldsets
+    And the field "Field 1" matches value "1"
+    And the field "Field 3" matches value "b"
+    And the field "Field 4" matches value "Hello"
+    And the field "Field 5" matches value "Some text"
+    # We have to enable the date field manually.
+    And I set the following fields to these values:
+      | customfield_date[enabled] | 1    |
+      | customfield_date[day]     | 1    |
+      | customfield_date[month]   | June |
+      | customfield_date[year]    | 2020 |
+    And I click on "Upload courses" "button"
+    Then I should see "Course created"
+    And I should see "Courses created: 3"
+    And I am on site homepage
+    And I should see "Course 1"
+    And I should see "Field 1: Yes"
+    And I should see "Field 2: 1 June 2020"
+    And I should see "Field 3: b"
+    And I should see "Field 4: Hello"
+    And I should see "Field 5: Some text"
\ No newline at end of file
index dbdbef5..dbca1f4 100644 (file)
@@ -7,7 +7,8 @@ Feature: An admin can update courses using a CSV file
   Background:
     Given the following "courses" exist:
       | fullname | shortname | category |
-      | Some random name | C1 | 0 |
+      | Some random name | C1  | 0 |
+      | Another course   | CF1 | 0 |
     And I log in as "admin"
     And I navigate to "Courses > Upload courses" in site administration
 
@@ -28,3 +29,31 @@ Feature: An admin can update courses using a CSV file
     And I should see "Course 1"
     And I should not see "Course 2"
     And I should not see "Course 3"
+
+  @javascript
+  Scenario: Updating a course with custom fields
+    Given the following "custom field categories" exist:
+      | name  | component   | area   | itemid |
+      | Other | core_course | course | 0      |
+    And the following "custom fields" exist:
+      | name    | category | type     | shortname | configdata            |
+      | Field 1 | Other    | checkbox | checkbox  |                       |
+      | Field 2 | Other    | date     | date      |                       |
+      | Field 3 | Other    | select   | select    | {"options":"a\nb\nc"} |
+      | Field 4 | Other    | text     | text      |                       |
+      | Field 5 | Other    | textarea | textarea  |                       |
+    When I upload "admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv" file to "File" filemanager
+    And I set the following fields to these values:
+      | Upload mode | Only update existing courses |
+      | Update mode | Update with CSV data only    |
+    And I click on "Preview" "button"
+    And I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I should see "Courses updated: 1"
+    And I am on site homepage
+    And I should see "Course fields 1"
+    And I should see "Field 1: Yes"
+    And I should see "Field 2: Tuesday, 1 October 2019, 2:00"
+    And I should see "Field 3: b"
+    And I should see "Field 4: Hello"
+    And I should see "Field 5: Goodbye"
\ No newline at end of file
index 0add18e..7d811cb 100644 (file)
@@ -1081,6 +1081,136 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertEquals(strtotime('12th July 2013'), $enroldata['manual']->enrolenddate);
     }
 
+    /**
+     * Test upload processing of course custom fields
+     */
+    public function test_custom_fields_data() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom fields.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'date', 'mydatefield');
+        $this->create_custom_field($category, 'text', 'mytextfield');
+        $this->create_custom_field($category, 'textarea', 'mytextareafield');
+
+        // Perform upload.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_mydatefield' => '2020-04-01 16:00',
+            'customfield_mytextfield' => 'Hello',
+            'customfield_mytextareafield' => 'Is it me you\'re looking for?',
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertTrue($uploader->prepare());
+        $uploader->proceed();
+
+        // Confirm presence of course custom fields.
+        $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+        $this->assertEquals('Wednesday, 1 April 2020, 4:00 PM', $data->mydatefield, '', 0.0, 10, false, true);
+        $this->assertEquals($dataupload['customfield_mytextfield'], $data->mytextfield);
+        $this->assertContains($dataupload['customfield_mytextareafield'], $data->mytextareafield);
+    }
+
+    /**
+     * Test upload processing of course custom field that is required but empty
+     */
+    public function test_custom_fields_data_required() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom field.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'select', 'myselect', ['required' => true, 'options' => "Cat\nDog"]);
+
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_myselect' => null,
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertFalse($uploader->prepare());
+        $this->assertArrayHasKey('customfieldinvalid', $uploader->get_errors());
+
+        // Try again with a default value.
+        $defaults = [
+            'customfield_myselect' => 2, // Our second option: Dog.
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload, $defaults);
+        $this->assertTrue($uploader->prepare());
+        $uploader->proceed();
+
+        // Confirm presence of course custom fields.
+        $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+        $this->assertEquals('Dog', $data->myselect);
+    }
+
+    /**
+     * Test upload processing of course custom field with an invalid select option
+     */
+    public function test_custom_fields_data_invalid_select_option() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom field.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'select', 'myselect',
+            ['required' => true, 'options' => "Cat\nDog", 'defaultvalue' => 'Cat']);
+
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_myselect' => 'Fish', // No, invalid.
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertTrue($uploader->prepare());
+        $uploader->proceed();
+
+        // Confirm presence of course custom fields.
+        $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+        $this->assertEquals('Cat', $data->myselect);
+    }
+
+    /**
+     * Test upload processing of course custom field with an out of range date
+     */
+    public function test_custom_fields_data_invalid_date() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom field.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'date', 'mydate',
+            ['mindate' => strtotime('2020-04-01'), 'maxdate' => '2020-04-30']);
+
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_mydate' => '2020-05-06', // Out of range.
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertFalse($uploader->prepare());
+        $this->assertArrayHasKey('customfieldinvalid', $uploader->get_errors());
+    }
+
     public function test_idnumber_problems() {
         $this->resetAfterTest(true);
 
@@ -1224,7 +1354,34 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
         $this->assertFalse($co->prepare());
         $this->assertArrayHasKey('cannotrenameshortnamealreadyinuse', $co->get_errors());
+    }
 
+    /**
+     * Get custom field plugin generator
+     *
+     * @return core_customfield_generator
+     */
+    protected function get_customfield_generator() : core_customfield_generator {
+        return $this->getDataGenerator()->get_plugin_generator('core_customfield');
     }
 
-}
+    /**
+     * Helper method to create custom course field
+     *
+     * @param \core_customfield\category_controller $category
+     * @param string $type
+     * @param string $shortname
+     * @param array $configdata
+     * @return \core_customfield\field_controller
+     */
+    protected function create_custom_field(\core_customfield\category_controller $category, string $type, string $shortname,
+            array $configdata = []) : \core_customfield\field_controller {
+
+        return $this->get_customfield_generator()->create_field([
+            'categoryid' => $category->get('id'),
+            'type' => $type,
+            'shortname' => $shortname,
+            'configdata' => $configdata,
+        ]);
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv b/admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv
new file mode 100644 (file)
index 0000000..f949599
--- /dev/null
@@ -0,0 +1,2 @@
+shortname,fullname,summary,category,customfield_checkbox,customfield_date,customfield_select,customfield_text,customfield_textarea
+CF1,Course fields 1,Testing course fields,1,1,2019-10-01 14:00,b,Hello,Goodbye
\ No newline at end of file
index 6768507..feff463 100644 (file)
@@ -250,6 +250,81 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertArrayHasKey('invalidroles', $errors);
     }
 
+    /**
+     * Test custom field data processing
+     */
+    public function test_get_custom_course_field_data() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create all the fields!
+        $category = $this->get_customfield_generator()->create_category();
+
+        $checkboxfield = $this->create_custom_field($category, 'checkbox', 'mycheckbox');
+        $datefield = $this->create_custom_field($category, 'date', 'mydate');
+        $selectfield = $this->create_custom_field($category, 'select', 'myselect', ['options' => "Red\nGreen\nBlue"]);
+        $textfield = $this->create_custom_field($category, 'text', 'mytext', ['locked' => 1]);
+        $textareafield = $this->create_custom_field($category, 'textarea', 'mytextarea');
+
+        $fields = tool_uploadcourse_helper::get_custom_course_fields();
+        $this->assertCount(5, $fields);
+
+        $this->assertArrayHasKey($checkboxfield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_checkbox\field_controller::class, $fields[$checkboxfield->get('shortname')]);
+
+        $this->assertArrayHasKey($datefield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_date\field_controller::class, $fields[$datefield->get('shortname')]);
+
+        $this->assertArrayHasKey($selectfield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_select\field_controller::class, $fields[$selectfield->get('shortname')]);
+
+        $this->assertArrayHasKey($textfield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_text\field_controller::class, $fields[$textfield->get('shortname')]);
+
+        $this->assertArrayHasKey($textareafield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_textarea\field_controller::class, $fields[$textareafield->get('shortname')]);
+
+        $data = [
+            'customfield_mycheckbox' => '1',
+            'customfield_mydate' => '2019-10-01',
+            'customfield_myselect' => 'Green',
+            'customfield_mytext' => 'Hello',
+            'customfield_myunknownfield' => 'Goodbye',
+        ];
+
+        $expected = [
+            'customfield_mycheckbox' => '1',
+            'customfield_mydate' => strtotime('2019-10-01'),
+            'customfield_myselect' => 2,
+            'customfield_mytext' => 'Hello',
+        ];
+
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_and_enrol($course, 'manager');
+        $this->setUser($user);
+
+        $context = context_course::instance($course->id);
+
+        $this->assertEquals($expected, tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context));
+
+        // Now add our custom textarea field (separately because the value of it's 'itemid' element is unknown).
+        $data['customfield_mytextarea'] = 'Something';
+        $fields = tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context);
+        $this->assertArrayHasKey('customfield_mytextarea_editor', $fields);
+        $this->assertArrayHasKey('text', $fields['customfield_mytextarea_editor']);
+        $this->assertEquals('Something', $fields['customfield_mytextarea_editor']['text']);
+
+        // Now prohibit the capability to change locked fields for the manager role.
+        $managerrole = $DB->get_record('role', ['shortname' => 'manager']);
+        role_change_permission($managerrole->id, $context, 'moodle/course:changelockedcustomfields', CAP_PROHIBIT);
+
+        // The locked 'mytext' custom field should not be returned.
+        $fields = tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context);
+        $this->assertCount(4, $fields);
+        $this->assertArrayNotHasKey('customfield_mytext', $fields);
+    }
+
     public function test_increment_idnumber() {
         $this->resetAfterTest(true);
 
@@ -394,4 +469,33 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path));
         $this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path));
     }
-}
+
+    /**
+     * Get custom field plugin generator
+     *
+     * @return core_customfield_generator
+     */
+    protected function get_customfield_generator() : core_customfield_generator {
+        return $this->getDataGenerator()->get_plugin_generator('core_customfield');
+    }
+
+    /**
+     * Helper method to create custom course field
+     *
+     * @param \core_customfield\category_controller $category
+     * @param string $type
+     * @param string $shortname
+     * @param array $configdata
+     * @return \core_customfield\field_controller
+     */
+    protected function create_custom_field(\core_customfield\category_controller $category, string $type, string $shortname,
+            array $configdata = []) : \core_customfield\field_controller {
+
+        return $this->get_customfield_generator()->create_field([
+            'categoryid' => $category->get('id'),
+            'type' => $type,
+            'shortname' => $shortname,
+            'configdata' => $configdata,
+        ]);
+    }
+}
\ No newline at end of file
index 77ababc..e879835 100644 (file)
@@ -158,7 +158,7 @@ $string['newtablefrommysql'] = 'New table from MySQL';
 $string['new_table_from_mysql'] = 'New table from MySQL';
 $string['nofieldsspecified'] = 'No fields specified';
 $string['nomasterprimaryuniquefound'] = 'The column(s) that your foreign key references must be included in a primary or unique KEY in the referenced table. Note that the column being in a UNIQUE INDEX is not good enough.';
-$string['nomissingorextraindexesfound'] = 'No missing or extra indexes have been found, your DB doesn\'t need further actions.';
+$string['nomissingorextraindexesfound'] = 'No missing or extra indexes have been found, so no further action is required.';
 $string['noreffieldsspecified'] = 'No reference fields specified';
 $string['noreftablespecified'] = 'Specified reference table not found';
 $string['noviolatedforeignkeysfound'] = 'No violated foreign keys found';
index babede0..4e5e8d3 100644 (file)
@@ -25,7 +25,6 @@
 define('NO_OUTPUT_BUFFERING', true);
 require_once('../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/dataformatlib.php');
 require_once($CFG->dirroot.'/user/profile/lib.php');
 
 $dataformat = optional_param('dataformat', '', PARAM_ALPHA);
@@ -69,9 +68,9 @@ if ($dataformat) {
     $downloadusers = new ArrayObject($SESSION->bulk_users);
     $iterator = $downloadusers->getIterator();
 
-    download_as_dataformat($filename, $dataformat, $fields, $iterator, function($userid) use ($extrafields, $fields) {
+    \core\dataformat::download_data($filename, $dataformat, $fields, $iterator, function($userid) use ($extrafields, $fields) {
         global $DB;
-        $row = array();
+
         if (!$user = $DB->get_record('user', array('id' => $userid))) {
             return null;
         }
index 11f86c8..991af64 100644 (file)
 
 namespace availability_completion;
 
+use cache;
+use core_availability\info;
+use core_availability\info_module;
+use core_availability\info_section;
+use stdClass;
+
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->libdir . '/completionlib.php');
@@ -36,14 +42,27 @@ require_once($CFG->libdir . '/completionlib.php');
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class condition extends \core_availability\condition {
+
+    /** @var int previous module cm value used to calculate relative completions */
+    public const OPTION_PREVIOUS = -1;
+
     /** @var int ID of module that this depends on */
     protected $cmid;
 
+    /** @var array IDs of the current module and section */
+    protected $selfids;
+
     /** @var int Expected completion type (one of the COMPLETE_xx constants) */
     protected $expectedcompletion;
 
+    /** @var array Array of previous cmids used to calculate relative completions */
+    protected $modfastprevious = [];
+
+    /** @var array Array of cmids previous to each course section */
+    protected $sectionfastprevious = [];
+
     /** @var array Array of modules used in these conditions for course */
-    protected static $modsusedincondition = array();
+    protected static $modsusedincondition = [];
 
     /**
      * Constructor.
@@ -58,20 +77,27 @@ class condition extends \core_availability\condition {
         } else {
             throw new \coding_exception('Missing or invalid ->cm for completion condition');
         }
-
         // Get expected completion.
         if (isset($structure->e) && in_array($structure->e,
-                array(COMPLETION_COMPLETE, COMPLETION_INCOMPLETE,
-                        COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL))) {
+                [COMPLETION_COMPLETE, COMPLETION_INCOMPLETE,
+                COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL])) {
             $this->expectedcompletion = $structure->e;
         } else {
             throw new \coding_exception('Missing or invalid ->e for completion condition');
         }
     }
 
-    public function save() {
-        return (object)array('type' => 'completion',
-                'cm' => $this->cmid, 'e' => $this->expectedcompletion);
+    /**
+     * Saves tree data back to a structure object.
+     *
+     * @return stdClass Structure object (ready to be made into JSON format)
+     */
+    public function save(): stdClass {
+        return (object) [
+            'type' => 'completion',
+            'cm' => $this->cmid,
+            'e' => $this->expectedcompletion,
+        ];
     }
 
     /**
@@ -84,22 +110,41 @@ class condition extends \core_availability\condition {
      * @param int $expectedcompletion Expected completion value (COMPLETION_xx)
      * @return stdClass Object representing condition
      */
-    public static function get_json($cmid, $expectedcompletion) {
-        return (object)array('type' => 'completion', 'cm' => (int)$cmid,
-                'e' => (int)$expectedcompletion);
+    public static function get_json(int $cmid, int $expectedcompletion): stdClass {
+        return (object) [
+            'type' => 'completion',
+            'cm' => (int)$cmid,
+            'e' => (int)$expectedcompletion,
+        ];
     }
 
-    public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
+    /**
+     * Determines whether a particular item is currently available
+     * according to this availability condition.
+     *
+     * @see \core_availability\tree_node\update_after_restore
+     *
+     * @param bool $not Set true if we are inverting the condition
+     * @param info $info Item we're checking
+     * @param bool $grabthelot Performance hint: if true, caches information
+     *   required for all course-modules, to make the front page and similar
+     *   pages work more quickly (works only for current user)
+     * @param int $userid User ID to check availability for
+     * @return bool True if available
+     */
+    public function is_available($not, info $info, $grabthelot, $userid): bool {
+        list($selfcmid, $selfsectionid) = $this->get_selfids($info);
+        $cmid = $this->get_cmid($info->get_course(), $selfcmid, $selfsectionid);
         $modinfo = $info->get_modinfo();
         $completion = new \completion_info($modinfo->get_course());
-        if (!array_key_exists($this->cmid, $modinfo->cms)) {
+        if (!array_key_exists($cmid, $modinfo->cms) || $modinfo->cms[$cmid]->deletioninprogress) {
             // If the cmid cannot be found, always return false regardless
             // of the condition or $not state. (Will be displayed in the
             // information message.)
             $allow = false;
         } else {
             // The completion system caches its own data so no caching needed here.
-            $completiondata = $completion->get_data((object)array('id' => $this->cmid),
+            $completiondata = $completion->get_data((object)['id' => $cmid],
                     $grabthelot, $userid, $modinfo);
 
             $allow = true;
@@ -128,6 +173,134 @@ class condition extends \core_availability\condition {
         return $allow;
     }
 
+    /**
+     * Return current item IDs (cmid and sectionid).
+     *
+     * @param info $info
+     * @return int[] with [0] => cmid/null, [1] => sectionid/null
+     */
+    public function get_selfids(info $info): array {
+        if (isset($this->selfids)) {
+            return $this->selfids;
+        }
+        if ($info instanceof info_module) {
+            $cminfo = $info->get_course_module();
+            if (!empty($cminfo->id)) {
+                $this->selfids = [$cminfo->id, null];
+                return $this->selfids;
+            }
+        }
+        if ($info instanceof info_section) {
+            $section = $info->get_section();
+            if (!empty($section->id)) {
+                $this->selfids = [null, $section->id];
+                return $this->selfids;
+            }
+
+        }
+        return [null, null];
+    }
+
+    /**
+     * Get the cmid referenced in the access restriction.
+     *
+     * @param stdClass $course course object
+     * @param int|null $selfcmid current course-module ID or null
+     * @param int|null $selfsectionid current course-section ID or null
+     * @return int|null cmid or null if no referenced cm is found
+     */
+    public function get_cmid(stdClass $course, ?int $selfcmid, ?int $selfsectionid): ?int {
+        if ($this->cmid > 0) {
+            return $this->cmid;
+        }
+        // If it's a relative completion, load fast browsing.
+        if ($this->cmid == self::OPTION_PREVIOUS) {
+            $prevcmid = $this->get_previous_cmid($course, $selfcmid, $selfsectionid);
+            if ($prevcmid) {
+                return $prevcmid;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the previous CM ID of an specific course-module or course-section.
+     *
+     * @param stdClass $course course object
+     * @param int|null $selfcmid course-module ID or null
+     * @param int|null $selfsectionid course-section ID or null
+     * @return int|null
+     */
+    private function get_previous_cmid(stdClass $course, ?int $selfcmid, ?int $selfsectionid): ?int {
+        $this->load_course_structure($course);
+        if (isset($this->modfastprevious[$selfcmid])) {
+            return $this->modfastprevious[$selfcmid];
+        }
+        if (isset($this->sectionfastprevious[$selfsectionid])) {
+            return $this->sectionfastprevious[$selfsectionid];
+        }
+        return null;
+    }
+
+    /**
+     * Loads static information about a course elements previous activities.
+     *
+     * Populates two variables:
+     *   - $this->sectionprevious[] course-module previous to a cmid
+     *   - $this->sectionfastprevious[] course-section previous to a cmid
+     *
+     * @param stdClass $course course object
+     */
+    private function load_course_structure(stdClass $course): void {
+        // If already loaded we don't need to do anything.
+        if (empty($this->modfastprevious)) {
+            $previouscache = cache::make('availability_completion', 'previous_cache');
+            $this->modfastprevious = $previouscache->get("mod_{$course->id}");
+            $this->sectionfastprevious = $previouscache->get("sec_{$course->id}");
+        }
+
+        if (!empty($this->modfastprevious)) {
+            return;
+        }
+
+        if (empty($this->modfastprevious)) {
+            $this->modfastprevious = [];
+            $sectionprevious = [];
+
+            $modinfo = get_fast_modinfo($course);
+            $lastcmid = 0;
+            foreach ($modinfo->cms as $othercm) {
+                if ($othercm->deletioninprogress) {
+                    continue;
+                }
+                // Save first cm of every section.
+                if (!isset($sectionprevious[$othercm->section])) {
+                    $sectionprevious[$othercm->section] = $lastcmid;
+                }
+                // Load previous to all cms with completion.
+                if ($othercm->completion == COMPLETION_TRACKING_NONE) {
+                    continue;
+                }
+                if ($lastcmid) {
+                    $this->modfastprevious[$othercm->id] = $lastcmid;
+                }
+                $lastcmid = $othercm->id;
+            }
+            // Fill empty sections index.
+            $isections = array_reverse($modinfo->get_section_info_all());
+            foreach ($isections as $section) {
+                if (isset($sectionprevious[$section->id])) {
+                    $lastcmid = $sectionprevious[$section->id];
+                } else {
+                    $sectionprevious[$section->id] = $lastcmid;
+                }
+            }
+            $this->sectionfastprevious = $sectionprevious;
+            $previouscache->set("mod_{$course->id}", $this->modfastprevious);
+            $previouscache->set("sec_{$course->id}", $this->sectionfastprevious);
+        }
+    }
+
     /**
      * Returns a more readable keyword corresponding to a completion state.
      *
@@ -136,7 +309,7 @@ class condition extends \core_availability\condition {
      * @param int $completionstate COMPLETION_xx constant
      * @return string Readable keyword
      */
-    protected static function get_lang_string_keyword($completionstate) {
+    protected static function get_lang_string_keyword(int $completionstate): string {
         switch($completionstate) {
             case COMPLETION_INCOMPLETE:
                 return 'incomplete';
@@ -151,38 +324,69 @@ class condition extends \core_availability\condition {
         }
     }
 
-    public function get_description($full, $not, \core_availability\info $info) {
-        // Get name for module.
-        $modinfo = $info->get_modinfo();
-        if (!array_key_exists($this->cmid, $modinfo->cms)) {
-            $modname = get_string('missing', 'availability_completion');
+    /**
+     * Obtains a string describing this restriction (whether or not
+     * it actually applies).
+     *
+     * @param bool $full Set true if this is the 'full information' view
+     * @param bool $not Set true if we are inverting the condition
+     * @param info $info Item we're checking
+     * @return string Information string (for admin) about all restrictions on
+     *   this item
+     */
+    public function get_description($full, $not, info $info): string {
+        global $USER;
+        $str = 'requires_';
+        $course = $info->get_course();
+        list($selfcmid, $selfsectionid) = $this->get_selfids($info);
+        $modname = '';
+        // On ajax duplicate get_fast_modinfo is called before $PAGE->set_context
+        // so we cannot use $PAGE->user_is_editing().
+        $coursecontext = \context_course::instance($course->id);
+        $editing = !empty($USER->editing) && has_capability('moodle/course:manageactivities', $coursecontext);
+        if ($this->cmid == self::OPTION_PREVIOUS && $editing) {
+            // Previous activity name could be inconsistent when editing due to partial page loadings.
+            $str .= 'previous_';
         } else {
-            $modname = '<AVAILABILITY_CMNAME_' . $modinfo->cms[$this->cmid]->id . '/>';
+            // Get name for module.
+            $cmid = $this->get_cmid($course, $selfcmid, $selfsectionid);
+            $modinfo = $info->get_modinfo();
+            if (!array_key_exists($cmid, $modinfo->cms) || $modinfo->cms[$cmid]->deletioninprogress) {
+                $modname = get_string('missing', 'availability_completion');
+            } else {
+                $modname = '<AVAILABILITY_CMNAME_' . $modinfo->cms[$cmid]->id . '/>';
+            }
         }
 
-        // Work out which lang string to use.
+        // Work out which lang string to use depending on required completion status.
         if ($not) {
             // Convert NOT strings to use the equivalent where possible.
             switch ($this->expectedcompletion) {
                 case COMPLETION_INCOMPLETE:
-                    $str = 'requires_' . self::get_lang_string_keyword(COMPLETION_COMPLETE);
+                    $str .= self::get_lang_string_keyword(COMPLETION_COMPLETE);
                     break;
                 case COMPLETION_COMPLETE:
-                    $str = 'requires_' . self::get_lang_string_keyword(COMPLETION_INCOMPLETE);
+                    $str .= self::get_lang_string_keyword(COMPLETION_INCOMPLETE);
                     break;
                 default:
                     // The other two cases do not have direct opposites.
-                    $str = 'requires_not_' . self::get_lang_string_keyword($this->expectedcompletion);
+                    $str .= 'not_' . self::get_lang_string_keyword($this->expectedcompletion);
                     break;
             }
         } else {
-            $str = 'requires_' . self::get_lang_string_keyword($this->expectedcompletion);
+            $str .= self::get_lang_string_keyword($this->expectedcompletion);
         }
 
         return get_string($str, 'availability_completion', $modname);
     }
 
-    protected function get_debug_string() {
+    /**
+     * Obtains a representation of the options of this condition as a string,
+     * for debugging.
+     *
+     * @return string Text representation of parameters
+     */
+    protected function get_debug_string(): string {
         switch ($this->expectedcompletion) {
             case COMPLETION_COMPLETE :
                 $type = 'COMPLETE';
@@ -199,18 +403,38 @@ class condition extends \core_availability\condition {
             default:
                 throw new \coding_exception('Unexpected expected completion');
         }
-        return 'cm' . $this->cmid . ' ' . $type;
+        $cm = $this->cmid;
+        if ($this->cmid == self::OPTION_PREVIOUS) {
+            $cm = 'opprevious';
+        }
+        return 'cm' . $cm . ' ' . $type;
     }
 
-    public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
+    /**
+     * Updates this node after restore, returning true if anything changed.
+     *
+     * @see \core_availability\tree_node\update_after_restore
+     *
+     * @param string $restoreid Restore ID
+     * @param int $courseid ID of target course
+     * @param \base_logger $logger Logger for any warnings
+     * @param string $name Name of this item (for use in warning messages)
+     * @return bool True if there was any change
+     */
+    public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name): bool {
         global $DB;
+        $res = false;
+        // If we depend on the previous activity, no translation is needed.
+        if ($this->cmid == self::OPTION_PREVIOUS) {
+            return $res;
+        }
         $rec = \restore_dbops::get_backup_ids_record($restoreid, 'course_module', $this->cmid);
         if (!$rec || !$rec->newitemid) {
             // If we are on the same course (e.g. duplicate) then we can just
             // use the existing one.
             if ($DB->record_exists('course_modules',
-                    array('id' => $this->cmid, 'course' => $courseid))) {
-                return false;
+                    ['id' => $this->cmid, 'course' => $courseid])) {
+                return $res;
             }
             // Otherwise it's a warning.
             $this->cmid = 0;
@@ -231,13 +455,13 @@ class condition extends \core_availability\condition {
      * @param int $cmid Course-module id
      * @return bool True if this is used in a condition, false otherwise
      */
-    public static function completion_value_used($course, $cmid) {
+    public static function completion_value_used($course, $cmid): bool {
         // Have we already worked out a list of required completion values
         // for this course? If so just use that.
         if (!array_key_exists($course->id, self::$modsusedincondition)) {
             // We don't have data for this course, build it.
             $modinfo = get_fast_modinfo($course);
-            self::$modsusedincondition[$course->id] = array();
+            self::$modsusedincondition[$course->id] = [];
 
             // Activities.
             foreach ($modinfo->cms as $othercm) {
@@ -247,7 +471,10 @@ class condition extends \core_availability\condition {
                 $ci = new \core_availability\info_module($othercm);
                 $tree = $ci->get_availability_tree();
                 foreach ($tree->get_all_children('availability_completion\condition') as $cond) {
-                    self::$modsusedincondition[$course->id][$cond->cmid] = true;
+                    $condcmid = $cond->get_cmid($course, $othercm->id, null);
+                    if (!empty($condcmid)) {
+                        self::$modsusedincondition[$course->id][$condcmid] = true;
+                    }
                 }
             }
 
@@ -259,7 +486,10 @@ class condition extends \core_availability\condition {
                 $ci = new \core_availability\info_section($section);
                 $tree = $ci->get_availability_tree();
                 foreach ($tree->get_all_children('availability_completion\condition') as $cond) {
-                    self::$modsusedincondition[$course->id][$cond->cmid] = true;
+                    $condcmid = $cond->get_cmid($course, null, $section->id);
+                    if (!empty($condcmid)) {
+                        self::$modsusedincondition[$course->id][$condcmid] = true;
+                    }
                 }
             }
         }
@@ -270,7 +500,7 @@ class condition extends \core_availability\condition {
      * Wipes the static cache of modules used in a condition (for unit testing).
      */
     public static function wipe_static_cache() {
-        self::$modsusedincondition = array();
+        self::$modsusedincondition = [];
     }
 
     public function update_dependency_id($table, $oldid, $newid) {
index 7427328..c646685 100644 (file)
@@ -37,7 +37,7 @@ class frontend extends \core_availability\frontend {
     /**
      * @var array Cached init parameters
      */
-    protected $cacheparams = array();
+    protected $cacheparams = [];
 
     /**
      * @var string IDs of course, cm, and section for cache (if any)
@@ -45,8 +45,8 @@ class frontend extends \core_availability\frontend {
     protected $cachekey = '';
 
     protected function get_javascript_strings() {
-        return array('option_complete', 'option_fail', 'option_incomplete', 'option_pass',
-                'label_cm', 'label_completion');
+        return ['option_complete', 'option_fail', 'option_incomplete', 'option_pass',
+                        'label_cm', 'label_completion'];
     }
 
     protected function get_javascript_init_params($course, \cm_info $cm = null,
@@ -59,20 +59,29 @@ class frontend extends \core_availability\frontend {
             // Get list of activities on course which have completion values,
             // to fill the dropdown.
             $context = \context_course::instance($course->id);
-            $cms = array();
+            $cms = [];
             $modinfo = get_fast_modinfo($course);
+            $previouscm = false;
             foreach ($modinfo->cms as $id => $othercm) {
                 // Add each course-module if it has completion turned on and is not
                 // the one currently being edited.
                 if ($othercm->completion && (empty($cm) || $cm->id != $id) && !$othercm->deletioninprogress) {
-                    $cms[] = (object)array('id' => $id,
-                        'name' => format_string($othercm->name, true, array('context' => $context)),
-                        'completiongradeitemnumber' => $othercm->completiongradeitemnumber);
+                    $cms[] = (object)['id' => $id,
+                        'name' => format_string($othercm->name, true, ['context' => $context]),
+                        'completiongradeitemnumber' => $othercm->completiongradeitemnumber];
+                }
+                if (count($cms) && (empty($cm) || $cm->id == $id)) {
+                    $previouscm = true;
                 }
             }
-
+            if ($previouscm) {
+                $previous = (object)['id' => \availability_completion\condition::OPTION_PREVIOUS,
+                        'name' => get_string('option_previous', 'availability_completion'),
+                        'completiongradeitemnumber' => \availability_completion\condition::OPTION_PREVIOUS];
+                array_unshift($cms, $previous);
+            }
             $this->cachekey = $cachekey;
-            $this->cacheinitparams = array($cms);
+            $this->cacheinitparams = [$cms];
         }
         return $this->cacheinitparams;
     }
diff --git a/availability/condition/completion/db/caches.php b/availability/condition/completion/db/caches.php
new file mode 100644 (file)
index 0000000..e365533
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Defined caches used internally by the plugin.
+ *
+ * @package     availability_completion
+ * @category    cache
+ * @copyright   2020 Ferran Recio <ferran@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$definitions = [
+    'previous_cache' => [
+        'mode' => cache_store::MODE_REQUEST,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'staticacceleration' => true
+    ],
+];
index f3a8693..4d406da 100644 (file)
@@ -22,6 +22,7 @@
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['cachedef_previous_cache'] = 'Previous activity dependency information';
 $string['description'] = 'Require students to complete (or not complete) another activity.';
 $string['error_selectcmid'] = 'You must select an activity for the completion condition.';
 $string['error_selectcmidpassfail'] = 'You must select an activity with "Require grade" completion condition set.';
@@ -32,6 +33,7 @@ $string['option_complete'] = 'must be marked complete';
 $string['option_fail'] = 'must be complete with fail grade';
 $string['option_incomplete'] = 'must not be marked complete';
 $string['option_pass'] = 'must be complete with pass grade';
+$string['option_previous'] = 'Previous activity with completion';
 $string['pluginname'] = 'Restriction by activity completion';
 $string['requires_incomplete'] = 'The activity <strong>{$a}</strong> is incomplete';
 $string['requires_complete'] = 'The activity <strong>{$a}</strong> is marked complete';
@@ -39,5 +41,11 @@ $string['requires_complete_pass'] = 'The activity <strong>{$a}</strong> is compl
 $string['requires_complete_fail'] = 'The activity <strong>{$a}</strong> is complete and failed';
 $string['requires_not_complete_pass'] = 'The activity <strong>{$a}</strong> is not complete and passed';
 $string['requires_not_complete_fail'] = 'The activity <strong>{$a}</strong> is not complete and failed';
+$string['requires_previous_incomplete'] = 'The <strong>previous activity with completion</strong> is incomplete';
+$string['requires_previous_complete'] = 'The <strong>previous activity with completion</strong> is marked complete';
+$string['requires_previous_complete_pass'] = 'The <strong>previous activity with completion</strong> is complete and passed';
+$string['requires_previous_complete_fail'] = 'The <strong>previous activity with completion</strong> is complete and failed';
+$string['requires_previous_not_complete_pass'] = 'The <strong>previous activity with completion</strong> is not complete and passed';
+$string['requires_previous_not_complete_fail'] = 'The <strong>previous activity with completion</strong> is not complete and failed';
 $string['title'] = 'Activity completion';
 $string['privacy:metadata'] = 'The Restriction by activity completion plugin does not store any personal data.';
diff --git a/availability/condition/completion/tests/behat/availability_completion_previous.feature b/availability/condition/completion/tests/behat/availability_completion_previous.feature
new file mode 100644 (file)
index 0000000..ae21266
--- /dev/null
@@ -0,0 +1,200 @@
+@availability @availability_completion
+Feature: Confirm that availability_completion works with previous activity setting
+  In order to control student access to activities
+  As a teacher
+  I need to set completion conditions which prevent student access
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format | enablecompletion | numsections |
+      | Course 1 | C1        | topics | 1                | 5           |
+    And the following "users" exist:
+      | username |
+      | teacher1 |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    Given the following "activities" exist:
+      | activity | name           | intro              | course | idnumber | groupmode | completion | section |
+      | page     | Page1          | Page 1 description | C1     | page1    | 1         | 1          | 1       |
+      | page     | Page Ignored 1 | Page Ignored       | C1     | pagei1   | 1         | 0          | 1       |
+      | page     | Page2          | Page 2 description | C1     | page2    | 1         | 1          | 3       |
+      | page     | Page3          | Page 3 description | C1     | page3    | 1         | 1          | 4       |
+
+  @javascript
+  Scenario: Test condition with previous activity on an activity
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Set Page3 restriction to Previous Activity with completion.
+    When I open "Page3" actions menu
+    And I click on "Edit settings" "link" in the "Page3" activity
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save and return to course"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+    # Remove Page 2 and check Page3 depends now on Page1.
+    When I turn editing mode on
+    And I delete "Page2" activity
+    And I turn editing mode off
+    Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+  @javascript
+  Scenario: Test previous activity availability when duplicate an activity
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Set Page3 restriction to Previous Activity with completion.
+    When I open "Page3" actions menu
+    And I click on "Edit settings" "link" in the "Page3" activity
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save and return to course"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+    # Duplicate Page3.
+    When I turn editing mode on
+    And I duplicate "Page3" activity
+    And I turn editing mode off
+    Then I should see "Not available unless: The activity Page3 is marked complete" in the "region-main" "region"
+
+  @javascript
+  Scenario: Test previous activity availability when modify completion tacking
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Set Page3 restriction to Previous Activity with completion.
+    When I open "Page3" actions menu
+    And I click on "Edit settings" "link" in the "Page3" activity
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save and return to course"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+    # Test if I disable completion tracking on Page2 section 5 depends on Page2.
+    When I turn editing mode on
+    When I open "Page2" actions menu
+    And I click on "Edit settings" "link" in the "Page2" activity
+    And I set the following fields to these values:
+      | Completion tracking | Do not indicate activity completion |
+    And I press "Save and return to course"
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+  @javascript
+  Scenario: Test condition with previous activity on a section
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Set section 4 restriction to Previous Activity with completion.
+    When I edit the section "4"
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save changes"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+    # Remove Page 2 and check Section 4 depends now on Page1.
+    When I turn editing mode on
+    And I delete "Page2" activity
+    And I turn editing mode off
+    Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+  @javascript
+  Scenario: Test condition with previous activity on the first activity of the course
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Try to set Page1 restriction to Previous Activity with completion.
+    When I open "Page1" actions menu
+    And I click on "Edit settings" "link" in the "Page1" activity
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    Then the "Activity or resource" select box should not contain "Previous activity with completion"
+
+    # Set Page2 restriction to Previous Activity with completion and delete Page1.
+    When I am on "Course 1" course homepage
+    When I open "Page2" actions menu
+    And I click on "Edit settings" "link" in the "Page2" activity
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save and return to course"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    # Delete Page 1 and check than Page2 now depends on a missing activity (no previous activity found).
+    When I am on "Course 1" course homepage
+    And I delete "Page1" activity
+    And I turn editing mode off
+    Then I should see "Not available unless: The activity (Missing activity)" in the "region-main" "region"
+
+  @javascript
+  Scenario: Test previous activities on empty sections
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Set section 2 restriction to Previous Activity with completion.
+    When I edit the section "2"
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save changes"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+    # Set section 5 restriction to Previous Activity with completion.
+    When I turn editing mode on
+    And I edit the section "5"
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save changes"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page3 is marked complete" in the "region-main" "region"
+
+    # Test if I disable completion tracking on Page3 section 5 depends on Page2.
+    When I turn editing mode on
+    And I open "Page3" actions menu
+    And I click on "Edit settings" "link" in the "Page3" activity
+    And I set the following fields to these values:
+      | Completion tracking | Do not indicate activity completion |
+    And I press "Save and return to course"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
index c64be08..b1a0040 100644 (file)
@@ -37,13 +37,23 @@ require_once($CFG->libdir . '/completionlib.php');
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class availability_completion_condition_testcase extends advanced_testcase {
+
     /**
-     * Load required classes.
+     * Setup to ensure that fixtures are loaded.
      */
-    public function setUp() {
-        // Load the mock info class so that it can be used.
+    public static function setupBeforeClass(): void {
         global $CFG;
+        // Load the mock info class so that it can be used.
         require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
+        require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_module.php');
+        require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_section.php');
+    }
+
+    /**
+     * Load required classes.
+     */
+    public function setUp() {
+        availability_completion\condition::wipe_static_cache();
     }
 
     /**
@@ -59,17 +69,27 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $CFG->enablecompletion = true;
         $CFG->enableavailability = true;
         $generator = $this->getDataGenerator();
-        $course = $generator->create_course(array('enablecompletion' => 1));
+        $course = $generator->create_course(['enablecompletion' => 1]);
         $page = $generator->get_plugin_generator('mod_page')->create_instance(
-                array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+                ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
+        $selfpage = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
 
         $modinfo = get_fast_modinfo($course);
         $cm = $modinfo->get_cm($page->cmid);
         $info = new \core_availability\mock_info($course, $USER->id);
 
-        $structure = (object)array('op' => '|', 'show' => true, 'c' => array(
-                (object)array('type' => 'completion', 'cm' => (int)$cm->id,
-                'e' => COMPLETION_COMPLETE)));
+        $structure = (object)[
+            'op' => '|',
+            'show' => true,
+            'c' => [
+                (object)[
+                    'type' => 'completion',
+                    'cm' => (int)$cm->id,
+                    'e' => COMPLETION_COMPLETE
+                ]
+            ]
+        ];
         $tree = new \core_availability\tree($structure);
 
         // Initial check (user has not completed activity).
@@ -142,13 +162,19 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $structure->e = COMPLETION_INCOMPLETE;
         $cond = new condition($structure);
         $this->assertEquals('{completion:cm42 INCOMPLETE}', (string)$cond);
+
+        // Successful contruct with previous activity.
+        $structure->cm = condition::OPTION_PREVIOUS;
+        $cond = new condition($structure);
+        $this->assertEquals('{completion:cmopprevious INCOMPLETE}', (string)$cond);
+
     }
 
     /**
      * Tests the save() function.
      */
     public function test_save() {
-        $structure = (object)array('cm' => 42, 'e' => COMPLETION_COMPLETE);
+        $structure = (object)['cm' => 42, 'e' => COMPLETION_COMPLETE];
         $cond = new condition($structure);
         $structure->type = 'completion';
         $this->assertEquals($structure, $cond->save());
@@ -166,24 +192,24 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $CFG->enablecompletion = true;
         $CFG->enableavailability = true;
         $generator = $this->getDataGenerator();
-        $course = $generator->create_course(array('enablecompletion' => 1));
+        $course = $generator->create_course(['enablecompletion' => 1]);
         $user = $generator->create_user();
         $generator->enrol_user($user->id, $course->id);
         $this->setUser($user);
 
         // Create a Page with manual completion for basic checks.
         $page = $generator->get_plugin_generator('mod_page')->create_instance(
-                array('course' => $course->id, 'name' => 'Page!',
-                'completion' => COMPLETION_TRACKING_MANUAL));
+                ['course' => $course->id, 'name' => 'Page!',
+                'completion' => COMPLETION_TRACKING_MANUAL]);
 
         // Create an assignment - we need to have something that can be graded
         // so as to test the PASS/FAIL states. Set it up to be completed based
         // on its grade item.
-        $assignrow = $this->getDataGenerator()->create_module('assign', array(
-                'course' => $course->id, 'name' => 'Assign!',
-                'completion' => COMPLETION_TRACKING_AUTOMATIC));
+        $assignrow = $this->getDataGenerator()->create_module('assign', [
+                        'course' => $course->id, 'name' => 'Assign!',
+                        'completion' => COMPLETION_TRACKING_AUTOMATIC]);
         $DB->set_field('course_modules', 'completiongradeitemnumber', 0,
-                array('id' => $assignrow->cmid));
+                ['id' => $assignrow->cmid]);
         $assign = new assign(context_module::instance($assignrow->cmid), false, false);
 
         // Get basic details.
@@ -193,8 +219,9 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $info = new \core_availability\mock_info($course, $user->id);
 
         // COMPLETE state (false), positive and NOT.
-        $cond = new condition((object)array(
-                'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE));
+        $cond = new condition((object)[
+            'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
         $information = $cond->get_description(false, false, $info);
         $information = \core_availability\info::format_info($information, $course);
@@ -202,8 +229,9 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 
         // INCOMPLETE state (true).
-        $cond = new condition((object)array(
-                'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE));
+        $cond = new condition((object)[
+            'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE
+        ]);
         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
         $information = $cond->get_description(false, true, $info);
@@ -215,8 +243,9 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $completion->update_state($pagecm, COMPLETION_COMPLETE);
 
         // COMPLETE state (true).
-        $cond = new condition((object)array(
-                'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE));
+        $cond = new condition((object)[
+            'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE
+        ]);
         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
         $information = $cond->get_description(false, true, $info);
@@ -224,8 +253,9 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $this->assertRegExp('~Page!.*is incomplete~', $information);
 
         // INCOMPLETE state (false).
-        $cond = new condition((object)array(
-                'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE));
+        $cond = new condition((object)[
+            'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
         $information = $cond->get_description(false, false, $info);
         $information = \core_availability\info::format_info($information, $course);
@@ -235,32 +265,36 @@ class availability_completion_condition_testcase extends advanced_testcase {
 
         // We are going to need the grade item so that we can get pass/fails.
         $gradeitem = $assign->get_grade_item();
-        grade_object::set_properties($gradeitem, array('gradepass' => 50.0));
+        grade_object::set_properties($gradeitem, ['gradepass' => 50.0]);
         $gradeitem->update();
 
         // With no grade, it should return true for INCOMPLETE and false for
         // the other three.
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
+        ]);
         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 
         // Check $information for COMPLETE_PASS and _FAIL as we haven't yet.
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
         $information = $cond->get_description(false, false, $info);
         $information = \core_availability\info::format_info($information, $course);
         $this->assertRegExp('~Assign!.*is complete and passed~', $information);
         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
         $information = $cond->get_description(false, false, $info);
         $information = \core_availability\info::format_info($information, $course);
@@ -270,26 +304,30 @@ class availability_completion_condition_testcase extends advanced_testcase {
         // Change the grade to be complete and failed.
         self::set_grade($assignrow, $user->id, 40);
 
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
+        ]);
         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
         $information = $cond->get_description(false, false, $info);
         $information = \core_availability\info::format_info($information, $course);
         $this->assertRegExp('~Assign!.*is complete and passed~', $information);
         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
+        ]);
         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
         $information = $cond->get_description(false, true, $info);
@@ -299,26 +337,30 @@ class availability_completion_condition_testcase extends advanced_testcase {
         // Now change it to pass.
         self::set_grade($assignrow, $user->id, 60);
 
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
 
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
+        ]);
         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
 
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
+        $cond = new condition((object)[
+                        'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
+                    ]);
         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
         $information = $cond->get_description(false, true, $info);
         $information = \core_availability\info::format_info($information, $course);
         $this->assertRegExp('~Assign!.*is not complete and passed~', $information);
 
-        $cond = new condition((object)array(
-                'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
+        $cond = new condition((object)[
+            'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
         $information = $cond->get_description(false, false, $info);
         $information = \core_availability\info::format_info($information, $course);
@@ -327,59 +369,390 @@ class availability_completion_condition_testcase extends advanced_testcase {
 
         // Simulate deletion of an activity by using an invalid cmid. These
         // conditions always fail, regardless of NOT flag or INCOMPLETE.
-        $cond = new condition((object)array(
-                'cm' => ($assigncm->id + 100), 'e' => COMPLETION_COMPLETE));
+        $cond = new condition((object)[
+            'cm' => ($assigncm->id + 100), 'e' => COMPLETION_COMPLETE
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
         $information = $cond->get_description(false, false, $info);
         $information = \core_availability\info::format_info($information, $course);
         $this->assertRegExp('~(Missing activity).*is marked complete~', $information);
         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
-        $cond = new condition((object)array(
-                'cm' => ($assigncm->id + 100), 'e' => COMPLETION_INCOMPLETE));
+        $cond = new condition((object)[
+            'cm' => ($assigncm->id + 100), 'e' => COMPLETION_INCOMPLETE
+        ]);
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
     }
 
+    /**
+     * Tests the is_available and get_description functions for previous activity option.
+     *
+     * @dataProvider test_previous_activity_data
+     * @param int $grade the current assign grade (0 for none)
+     * @param int $condition true for complete, false for incomplete
+     * @param string $mark activity to mark as complete
+     * @param string $activity activity name to test
+     * @param bool $result if it must be available or not
+     * @param bool $resultnot if it must be available when the condition is inverted
+     * @param string $description the availabiklity text to check
+     */
+    public function test_previous_activity(int $grade, int $condition, string $mark, string $activity,
+            bool $result, bool $resultnot, string $description): void {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/assign/locallib.php');
+        $this->resetAfterTest();
+
+        // Create course with completion turned on.
+        $CFG->enablecompletion = true;
+        $CFG->enableavailability = true;
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course(['enablecompletion' => 1]);
+        $user = $generator->create_user();
+        $generator->enrol_user($user->id, $course->id);
+        $this->setUser($user);
+
+        // Page 1 (manual completion).
+        $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course->id, 'name' => 'Page1!',
+                'completion' => COMPLETION_TRACKING_MANUAL]);
+
+        // Page 2 (manual completion).
+        $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course->id, 'name' => 'Page2!',
+                'completion' => COMPLETION_TRACKING_MANUAL]);
+
+        // Page ignored (no completion).
+        $pagenocompletion = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course->id, 'name' => 'Page ignored!']);
+
+        // Create an assignment - we need to have something that can be graded
+        // so as to test the PASS/FAIL states. Set it up to be completed based
+        // on its grade item.
+        $assignrow = $this->getDataGenerator()->create_module('assign', [
+            'course' => $course->id, 'name' => 'Assign!',
+            'completion' => COMPLETION_TRACKING_AUTOMATIC
+        ]);
+        $DB->set_field('course_modules', 'completiongradeitemnumber', 0,
+                ['id' => $assignrow->cmid]);
+        $assign = new assign(context_module::instance($assignrow->cmid), false, false);
+
+        // Page 3 (manual completion).
+        $page3 = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course->id, 'name' => 'Page3!',
+                'completion' => COMPLETION_TRACKING_MANUAL]);
+
+        // Get basic details.
+        $activities = [];
+        $modinfo = get_fast_modinfo($course);
+        $activities['page1'] = $modinfo->get_cm($page1->cmid);
+        $activities['page2'] = $modinfo->get_cm($page2->cmid);
+        $activities['assign'] = $assign->get_course_module();
+        $activities['page3'] = $modinfo->get_cm($page3->cmid);
+        $prevvalue = condition::OPTION_PREVIOUS;
+
+        // Setup gradings and completion.
+        if ($grade) {
+            $gradeitem = $assign->get_grade_item();
+            grade_object::set_properties($gradeitem, ['gradepass' => 50.0]);
+            $gradeitem->update();
+            self::set_grade($assignrow, $user->id, $grade);
+        }
+        if ($mark) {
+            $completion = new completion_info($course);
+            $completion->update_state($activities[$mark], COMPLETION_COMPLETE);
+        }
+
+        // Set opprevious WITH non existent previous activity.
+        $info = new \core_availability\mock_info_module($user->id, $activities[$activity]);
+        $cond = new condition((object)[
+            'cm' => (int)$prevvalue, 'e' => $condition
+        ]);
+
+        // Do the checks.
+        $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id));
+        $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id));
+        $information = $cond->get_description(false, false, $info);
+        $information = \core_availability\info::format_info($information, $course);
+        $this->assertRegExp($description, $information);
+    }
+
+    public function test_previous_activity_data(): array {
+        // Assign grade, condition, activity to complete, activity to test, result, resultnot, description.
+        return [
+            'Missing previous activity complete' => [
+                0, COMPLETION_COMPLETE, '', 'page1', false, false, '~Missing activity.*is marked complete~'
+            ],
+            'Missing previous activity incomplete' => [
+                0, COMPLETION_INCOMPLETE, '', 'page1', false, false, '~Missing activity.*is incomplete~'
+            ],
+            'Previous complete condition with previous activity incompleted' => [
+                0, COMPLETION_COMPLETE, '', 'page2', false, true, '~Page1!.*is marked complete~'
+            ],
+            'Previous incomplete condition with previous activity incompleted' => [
+                0, COMPLETION_INCOMPLETE, '', 'page2', true, false, '~Page1!.*is incomplete~'
+            ],
+            'Previous complete condition with previous activity completed' => [
+                0, COMPLETION_COMPLETE, 'page1', 'page2', true, false, '~Page1!.*is marked complete~'
+            ],
+            'Previous incomplete condition with previous activity completed' => [
+                0, COMPLETION_INCOMPLETE, 'page1', 'page2', false, true, '~Page1!.*is incomplete~'
+            ],
+            // Depenging on page pass fail (pages are not gradable).
+            'Previous complete pass condition with previous no gradable activity incompleted' => [
+                0, COMPLETION_COMPLETE_PASS, '', 'page2', false, true, '~Page1!.*is complete and passed~'
+            ],
+            'Previous complete fail condition with previous no gradable activity incompleted' => [
+                0, COMPLETION_COMPLETE_FAIL, '', 'page2', false, true, '~Page1!.*is complete and failed~'
+            ],
+            'Previous complete pass condition with previous no gradable activity completed' => [
+                0, COMPLETION_COMPLETE_PASS, 'page1', 'page2', false, true, '~Page1!.*is complete and passed~'
+            ],
+            'Previous complete fail condition with previous no gradable activity completed' => [
+                0, COMPLETION_COMPLETE_FAIL, 'page1', 'page2', false, true, '~Page1!.*is complete and failed~'
+            ],
+            // There's an page without completion between page2 ans assign.
+            'Previous complete condition with sibling activity incompleted' => [
+                0, COMPLETION_COMPLETE, '', 'assign', false, true, '~Page2!.*is marked complete~'
+            ],
+            'Previous incomplete condition with sibling activity incompleted' => [
+                0, COMPLETION_INCOMPLETE, '', 'assign', true, false, '~Page2!.*is incomplete~'
+            ],
+            'Previous complete condition with sibling activity completed' => [
+                0, COMPLETION_COMPLETE, 'page2', 'assign', true, false, '~Page2!.*is marked complete~'
+            ],
+            'Previous incomplete condition with sibling activity completed' => [
+                0, COMPLETION_INCOMPLETE, 'page2', 'assign', false, true, '~Page2!.*is incomplete~'
+            ],
+            // Depending on assign without grade.
+            'Previous complete condition with previous without grade' => [
+                0, COMPLETION_COMPLETE, '', 'page3', false, true, '~Assign!.*is marked complete~'
+            ],
+            'Previous incomplete condition with previous without grade' => [
+                0, COMPLETION_INCOMPLETE, '', 'page3', true, false, '~Assign!.*is incomplete~'
+            ],
+            'Previous complete pass condition with previous without grade' => [
+                0, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
+            ],
+            'Previous complete fail condition with previous without grade' => [
+                0, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~'
+            ],
+            // Depending on assign with grade.
+            'Previous complete condition with previous fail grade' => [
+                40, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
+            ],
+            'Previous incomplete condition with previous fail grade' => [
+                40, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
+            ],
+            'Previous complete pass condition with previous fail grade' => [
+                40, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
+            ],
+            'Previous complete fail condition with previous fail grade' => [
+                40, COMPLETION_COMPLETE_FAIL, '', 'page3', true, false, '~Assign!.*is complete and failed~'
+            ],
+            'Previous complete condition with previous pass grade' => [
+                60, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
+            ],
+            'Previous incomplete condition with previous pass grade' => [
+                60, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
+            ],
+            'Previous complete pass condition with previous pass grade' => [
+                60, COMPLETION_COMPLETE_PASS, '', 'page3', true, false, '~Assign!.*is complete and passed~'
+            ],
+            'Previous complete fail condition with previous pass grade' => [
+                60, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~'
+            ],
+        ];
+    }
+
+    /**
+     * Tests the is_available and get_description functions for
+     * previous activity option in course sections.
+     *
+     * @dataProvider test_section_previous_activity_data
+     * @param int $condition condition value
+     * @param bool $mark if Page 1 must be mark as completed
+     * @param string $section section to add the availability
+     * @param bool $result expected result
+     * @param bool $resultnot expected negated result
+     * @param string $description description to match
+     */
+    public function test_section_previous_activity(int $condition, bool $mark, string $section,
+                bool $result, bool $resultnot, string $description): void {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/assign/locallib.php');
+        $this->resetAfterTest();
+
+        // Create course with completion turned on.
+        $CFG->enablecompletion = true;
+        $CFG->enableavailability = true;
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course(
+                ['numsections' => 4, 'enablecompletion' => 1],
+                ['createsections' => true]);
+        $user = $generator->create_user();
+        $generator->enrol_user($user->id, $course->id);
+        $this->setUser($user);
+
+        // Section 1 - page1 (manual completion).
+        $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course->id, 'name' => 'Page1!', 'section' => 1,
+                'completion' => COMPLETION_TRACKING_MANUAL]);
+
+        // Section 1 - page ignored 1 (no completion).
+        $pagenocompletion1 = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course, 'name' => 'Page ignored!', 'section' => 1]);
+
+        // Section 2 - page ignored 2 (no completion).
+        $pagenocompletion2 = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course, 'name' => 'Page ignored!', 'section' => 2]);
+
+        // Section 3 - page2 (manual completion).
+        $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course->id, 'name' => 'Page2!', 'section' => 3,
+                'completion' => COMPLETION_TRACKING_MANUAL]);
+
+        // Section 4 is empty.
+
+        // Get basic details.
+        get_fast_modinfo(0, 0, true);
+        $modinfo = get_fast_modinfo($course);
+        $sections['section1'] = $modinfo->get_section_info(1);
+        $sections['section2'] = $modinfo->get_section_info(2);
+        $sections['section3'] = $modinfo->get_section_info(3);
+        $sections['section4'] = $modinfo->get_section_info(4);
+        $page1cm = $modinfo->get_cm($page1->cmid);
+        $prevvalue = condition::OPTION_PREVIOUS;
+
+        if ($mark) {
+            // Mark page1 complete.
+            $completion = new completion_info($course);
+            $completion->update_state($page1cm, COMPLETION_COMPLETE);
+        }
+
+        $info = new \core_availability\mock_info_section($user->id, $sections[$section]);
+        $cond = new condition((object)[
+            'cm' => (int)$prevvalue, 'e' => $condition
+        ]);
+        $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id));
+        $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id));
+        $information = $cond->get_description(false, false, $info);
+        $information = \core_availability\info::format_info($information, $course);
+        $this->assertRegExp($description, $information);
+
+    }
+
+    public function test_section_previous_activity_data(): array {
+        return [
+            // Condition, Activity completion, section to test, result, resultnot, description.
+            'Completion complete Section with no previous activity' => [
+                COMPLETION_COMPLETE, false, 'section1', false, false, '~Missing activity.*is marked complete~'
+            ],
+            'Completion incomplete Section with no previous activity' => [
+                COMPLETION_INCOMPLETE, false, 'section1', false, false, '~Missing activity.*is incomplete~'
+            ],
+            // Section 2 depending on section 1 -> Page 1 (no grading).
+            'Completion complete Section with previous activity incompleted' => [
+                COMPLETION_COMPLETE, false, 'section2', false, true, '~Page1!.*is marked complete~'
+            ],
+            'Completion incomplete Section with previous activity incompleted' => [
+                COMPLETION_INCOMPLETE, false, 'section2', true, false, '~Page1!.*is incomplete~'
+            ],
+            'Completion complete Section with previous activity completed' => [
+                COMPLETION_COMPLETE, true, 'section2', true, false, '~Page1!.*is marked complete~'
+            ],
+            'Completion incomplete Section with previous activity completed' => [
+                COMPLETION_INCOMPLETE, true, 'section2', false, true, '~Page1!.*is incomplete~'
+            ],
+            // Section 3 depending on section 1 -> Page 1 (no grading).
+            'Completion complete Section ignoring empty sections and activity incompleted' => [
+                COMPLETION_COMPLETE, false, 'section3', false, true, '~Page1!.*is marked complete~'
+            ],
+            'Completion incomplete Section ignoring empty sections and activity incompleted' => [
+                COMPLETION_INCOMPLETE, false, 'section3', true, false, '~Page1!.*is incomplete~'
+            ],
+            'Completion complete Section ignoring empty sections and activity completed' => [
+                COMPLETION_COMPLETE, true, 'section3', true, false, '~Page1!.*is marked complete~'
+            ],
+            'Completion incomplete Section ignoring empty sections and activity completed' => [
+                COMPLETION_INCOMPLETE, true, 'section3', false, true, '~Page1!.*is incomplete~'
+            ],
+            // Section 4 depending on section 3 -> Page 2 (no grading).
+            'Completion complete Last section with previous activity incompleted' => [
+                COMPLETION_COMPLETE, false, 'section4', false, true, '~Page2!.*is marked complete~'
+            ],
+            'Completion incomplete Last section with previous activity incompleted' => [
+                COMPLETION_INCOMPLETE, false, 'section4', true, false, '~Page2!.*is incomplete~'
+            ],
+            'Completion complete Last section with previous activity completed' => [
+                COMPLETION_COMPLETE, true, 'section4', false, true, '~Page2!.*is marked complete~'
+            ],
+            'Completion incomplete Last section with previous activity completed' => [
+                COMPLETION_INCOMPLETE, true, 'section4', true, false, '~Page2!.*is incomplete~'
+            ],
+        ];
+    }
+
     /**
      * Tests completion_value_used static function.
      */
     public function test_completion_value_used() {
         global $CFG, $DB;
         $this->resetAfterTest();
+        $prevvalue = condition::OPTION_PREVIOUS;
 
         // Create course with completion turned on and some sections.
         $CFG->enablecompletion = true;
         $CFG->enableavailability = true;
         $generator = $this->getDataGenerator();
         $course = $generator->create_course(
-                array('numsections' => 1, 'enablecompletion' => 1),
-                array('createsections' => true));
-        availability_completion\condition::wipe_static_cache();
+                ['numsections' => 1, 'enablecompletion' => 1],
+                ['createsections' => true]);
 
-        // Create three pages with manual completion.
+        // Create six pages with manual completion.
         $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
-                array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+                ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
         $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
-                array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+                ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
         $page3 = $generator->get_plugin_generator('mod_page')->create_instance(
-                array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+                ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
+        $page4 = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
+        $page5 = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
+        $page6 = $generator->get_plugin_generator('mod_page')->create_instance(
+                ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
 
         // Set up page3 to depend on page1, and section1 to depend on page2.
         $DB->set_field('course_modules', 'availability',
                 '{"op":"|","show":true,"c":[' .
                 '{"type":"completion","e":1,"cm":' . $page1->cmid . '}]}',
-                array('id' => $page3->cmid));
+                ['id' => $page3->cmid]);
         $DB->set_field('course_sections', 'availability',
                 '{"op":"|","show":true,"c":[' .
                 '{"type":"completion","e":1,"cm":' . $page2->cmid . '}]}',
-                array('course' => $course->id, 'section' => 1));
+                ['course' => $course->id, 'section' => 1]);
+        // Set up page5 and page6 to depend on previous activity.
+        $DB->set_field('course_modules', 'availability',
+                '{"op":"|","show":true,"c":[' .
+                '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}',
+                ['id' => $page5->cmid]);
+        $DB->set_field('course_modules', 'availability',
+                '{"op":"|","show":true,"c":[' .
+                '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}',
+                ['id' => $page6->cmid]);
 
-        // Now check: nothing depends on page3 but something does on the others.
+        // Check 1: nothing depends on page3 and page6 but something does on the others.
         $this->assertTrue(availability_completion\condition::completion_value_used(
                 $course, $page1->cmid));
         $this->assertTrue(availability_completion\condition::completion_value_used(
                 $course, $page2->cmid));
         $this->assertFalse(availability_completion\condition::completion_value_used(
                 $course, $page3->cmid));
+        $this->assertTrue(availability_completion\condition::completion_value_used(
+                $course, $page4->cmid));
+        $this->assertTrue(availability_completion\condition::completion_value_used(
+                $course, $page5->cmid));
+        $this->assertFalse(availability_completion\condition::completion_value_used(
+                $course, $page6->cmid));
     }
 
     /**
@@ -390,9 +763,10 @@ class availability_completion_condition_testcase extends advanced_testcase {
      * @param float $grade Grade
      */
     protected static function set_grade($assignrow, $userid, $grade) {
-        $grades = array();
-        $grades[$userid] = (object)array(
-                'rawgrade' => $grade, 'userid' => $userid);
+        $grades = [];
+        $grades[$userid] = (object)[
+            'rawgrade' => $grade, 'userid' => $userid
+        ];
         $assignrow->cmidnumber = null;
         assign_grade_item_update($assignrow, $grades);
     }
@@ -401,12 +775,32 @@ class availability_completion_condition_testcase extends advanced_testcase {
      * Tests the update_dependency_id() function.
      */
     public function test_update_dependency_id() {
-        $cond = new condition((object)array(
-                'cm' => 123, 'e' => COMPLETION_COMPLETE));
-        $this->assertFalse($cond->update_dependency_id('frogs', 123, 456));
+        $cond = new condition((object)[
+            'cm' => 42, 'e' => COMPLETION_COMPLETE, 'selfid' => 43
+        ]);
+        $this->assertFalse($cond->update_dependency_id('frogs', 42, 540));
         $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
-        $this->assertTrue($cond->update_dependency_id('course_modules', 123, 456));
+        $this->assertTrue($cond->update_dependency_id('course_modules', 42, 456));
         $after = $cond->save();
         $this->assertEquals(456, $after->cm);
+
+        // Test selfid updating.
+        $cond = new condition((object)[
+            'cm' => 42, 'e' => COMPLETION_COMPLETE
+        ]);
+        $this->assertFalse($cond->update_dependency_id('frogs', 43, 540));
+        $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
+        $after = $cond->save();
+        $this->assertEquals(42, $after->cm);
+
+        // Test on previous activity.
+        $cond = new condition((object)[
+            'cm' => condition::OPTION_PREVIOUS,
+            'e' => COMPLETION_COMPLETE
+        ]);
+        $this->assertFalse($cond->update_dependency_id('frogs', 43, 80));
+        $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
+        $after = $cond->save();
+        $this->assertEquals(condition::OPTION_PREVIOUS, $after->cm);
     }
 }
index 71d6281..4e1a55f 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2019111800;
+$plugin->version = 2020032600;
 $plugin->requires = 2019111200;
 $plugin->component = 'availability_completion';
diff --git a/availability/tests/fixtures/mock_info_module.php b/availability/tests/fixtures/mock_info_module.php
new file mode 100644 (file)
index 0000000..47f75cc
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * For use in unit tests that require an info module which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_availability;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * For use in unit tests that require an info module which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mock_info_module extends info_module {
+    /** @var int User id for modinfo */
+    protected $userid;
+
+    /** @var \cm_info Activity. */
+    protected $cm;
+
+    /**
+     * Constructs with item details.
+     *
+     * @param int $userid Userid for modinfo (if used)
+     * @param \cm_info $cm Course-module object
+     */
+    public function __construct($userid = 0, \cm_info $cm = null) {
+        parent::__construct($cm);
+        $this->userid = $userid;
+        $this->cm = $cm;
+    }
+
+    /**
+     * Just returns a mock name.
+     *
+     * @return string Name of item
+     */
+    protected function get_thing_name() {
+        return 'Mock Module';
+    }
+
+    /**
+     * Returns the current context.
+     *
+     * @return \context Context for this item
+     */
+    public function get_context() {
+        return \context_course::instance($this->get_course()->id);
+    }
+
+    /**
+     * Returns the cappability used to ignore access restrictions.
+     *
+     * @return string Name of capability used to view hidden items of this type
+     */
+    protected function get_view_hidden_capability() {
+        return 'moodle/course:ignoreavailabilityrestrictions';
+    }
+
+    /**
+     * Mocks don't need to save anything into DB.
+     *
+     * @param string $availability New JSON value
+     */
+    protected function set_in_database($availability) {
+    }
+
+    /**
+     * Obtains the modinfo associated with this availability information.
+     *
+     * Note: This field is available ONLY for use by conditions when calculating
+     * availability or information.
+     *
+     * @return \course_modinfo Modinfo
+     * @throws \coding_exception If called at incorrect times
+     */
+    public function get_modinfo() {
+        // Allow modinfo usage outside is_available etc., so we can use this
+        // to directly call into condition is_available.
+        if (!$this->userid) {
+            throw new \coding_exception('Need to set mock_info userid');
+        }
+        return get_fast_modinfo($this->course, $this->userid);
+    }
+
+    /**
+     * Override course-module info.
+     * @param \cm_info $cm
+     */
+    public function set_cm (\cm_info $cm) {
+        $this->cm = $cm;
+    }
+}
diff --git a/availability/tests/fixtures/mock_info_section.php b/availability/tests/fixtures/mock_info_section.php
new file mode 100644 (file)
index 0000000..15fba45
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/>.
+
+/**
+ * For use in unit tests that require an info section which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_availability;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * For use in unit tests that require an info section which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mock_info_section extends info_section {
+    /** @var int User id for modinfo */
+    protected $userid;
+
+    /** @var \section_info Section. */
+    protected $section;
+
+    /**
+     * Constructs with item details.
+     *
+     * @param int $userid Userid for modinfo (if used)
+     * @param \section_info $section Section object
+     */
+    public function __construct($userid = 0, \section_info $section = null) {
+        parent::__construct($section);
+        $this->userid = $userid;
+        $this->section = $section;
+    }
+
+    /**
+     * Just returns a mock name.
+     *
+     * @return string Name of item
+     */
+    protected function get_thing_name() {
+        return 'Mock Section';
+    }
+
+    /**
+     * Returns the current context.
+     *
+     * @return \context Context for this item
+     */
+    public function get_context() {
+        return \context_course::instance($this->get_course()->id);
+    }
+
+    /**
+     * Returns the cappability used to ignore access restrictions.
+     *
+     * @return string Name of capability used to view hidden items of this type
+     */
+    protected function get_view_hidden_capability() {
+        return 'moodle/course:ignoreavailabilityrestrictions';
+    }
+
+    /**
+     * Mocks don't need to save anything into DB.
+     *
+     * @param string $availability New JSON value
+     */
+    protected function set_in_database($availability) {
+    }
+
+    /**
+     * Obtains the modinfo associated with this availability information.
+     *
+     * Note: This field is available ONLY for use by conditions when calculating
+     * availability or information.
+     *
+     * @return \course_modinfo Modinfo
+     * @throws \coding_exception If called at incorrect times
+     */
+    public function get_modinfo() {
+        // Allow modinfo usage outside is_available etc., so we can use this
+        // to directly call into condition is_available.
+        if (!$this->userid) {
+            throw new \coding_exception('Need to set mock_info userid');
+        }
+        return get_fast_modinfo($this->course, $this->userid);
+    }
+
+    /**
+     * Override section info.
+     *
+     * @param \section_info $section
+     */
+    public function set_section (\section_info $section) {
+        $this->section = $section;
+    }
+}
index 2e96eeb..c5f86bb 100644 (file)
@@ -53,7 +53,7 @@ class block_recent_activity_renderer extends plugin_renderer_base {
         $output .= html_writer::tag('div',
                 html_writer::link(new moodle_url('/course/recent.php', array('id' => $course->id)),
                     get_string('recentactivityreport')),
-                array('class' => 'activityhead'));
+                array('class' => 'activityhead mb-3'));
 
         $content = false;
 
@@ -79,7 +79,7 @@ class block_recent_activity_renderer extends plugin_renderer_base {
         // Next, have there been any modifications to the course structure?
         if (!empty($structuralchanges)) {
             $content = true;
-            $output .= $this->heading(get_string("courseupdates").':', 3);
+            $output .= $this->heading(get_string("courseupdates") . ':', 6);
             foreach ($structuralchanges as $changeinfo => $change) {
                 $output .= $this->structural_change($change);
             }
index 3d1d9e3..f1e2c2a 100644 (file)
@@ -1,8 +1,3 @@
-.block_recent_activity .activitydate,
-.block_recent_activity .activityhead {
-    text-align: center;
-}
-
 .block_recent_activity .unlist li {
     margin-bottom: 1em;
 }
index 1b2a7df..ac42715 100644 (file)
@@ -16,4 +16,4 @@ Feature: Add URL to main menu block
       | External URL | http://www.google.com |
       | id_display | In pop-up |
     Then "google" "link" should exist in the "Main menu" "block"
-    And "Add an activity or resource" "button" should exist in the "Main menu" "block"
+    And "Add an activity" "button" should exist in the "Main menu" "block"
index 2776339..dc4821b 100644 (file)
@@ -361,20 +361,23 @@ class cache_helper {
     /**
      * Ensure that the stats array is ready to collect information for the given store and definition.
      * @param string $store
+     * @param string $storeclass
      * @param string $definition A string that identifies the definition.
      * @param int $mode One of cache_store::MODE_*. Since 2.9.
      */
-    protected static function ensure_ready_for_stats($store, $definition, $mode = cache_store::MODE_APPLICATION) {
+    protected static function ensure_ready_for_stats($store, $storeclass, $definition, $mode = cache_store::MODE_APPLICATION) {
         // This function is performance-sensitive, so exit as quickly as possible
         // if we do not need to do anything.
         if (isset(self::$stats[$definition]['stores'][$store])) {
             return;
         }
+
         if (!array_key_exists($definition, self::$stats)) {
             self::$stats[$definition] = array(
                 'mode' => $mode,
                 'stores' => array(
                     $store => array(
+                        'class' => $storeclass,
                         'hits' => 0,
                         'misses' => 0,
                         'sets' => 0,
@@ -383,6 +386,7 @@ class cache_helper {
             );
         } else if (!array_key_exists($store, self::$stats[$definition]['stores'])) {
             self::$stats[$definition]['stores'][$store] = array(
+                'class' => $storeclass,
                 'hits' => 0,
                 'misses' => 0,
                 'sets' => 0,
@@ -418,15 +422,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param cache_definition $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $hits The number of hits to record (by default 1)
      */
     public static function record_cache_hit($store, $definition, $hits = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['hits'] += $hits;
     }
 
@@ -436,15 +447,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param string $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $misses The number of misses to record (by default 1)
      */
     public static function record_cache_miss($store, $definition, $misses = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['misses'] += $misses;
     }
 
@@ -454,15 +472,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param string $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $sets The number of sets to record (by default 1)
      */
     public static function record_cache_set($store, $definition, $sets = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['sets'] += $sets;
     }
 
index 8cd7914..6236cb0 100644 (file)
@@ -414,7 +414,7 @@ class cache implements cache_loader {
         $setaftervalidation = false;
         if ($result === false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss($this->storetype, $this->definition);
+                cache_helper::record_cache_miss($this->store, $this->definition);
             }
             if ($this->loader !== false) {
                 // We must pass the original (unparsed) key to the next loader in the chain.
@@ -426,7 +426,7 @@ class cache implements cache_loader {
             }
             $setaftervalidation = ($result !== false);
         } else if ($this->perfdebug) {
-            cache_helper::record_cache_hit($this->storetype, $this->definition);
+            cache_helper::record_cache_hit($this->store, $this->definition);
         }
         // 5. Validate strictness.
         if ($strictness === MUST_EXIST && $result === false) {
@@ -580,8 +580,8 @@ class cache implements cache_loader {
                     $hits++;
                 }
             }
-            cache_helper::record_cache_hit($this->storetype, $this->definition, $hits);
-            cache_helper::record_cache_miss($this->storetype, $this->definition, $misses);
+            cache_helper::record_cache_hit($this->store, $this->definition, $hits);
+            cache_helper::record_cache_miss($this->store, $this->definition, $misses);
         }
 
         // Return the result. Phew!
@@ -607,7 +607,7 @@ class cache implements cache_loader {
      */
     public function set($key, $data) {
         if ($this->perfdebug) {
-            cache_helper::record_cache_set($this->storetype, $this->definition);
+            cache_helper::record_cache_set($this->store, $this->definition);
         }
         if ($this->loader !== false) {
             // We have a loader available set it there as well.
@@ -762,7 +762,7 @@ class cache implements cache_loader {
         }
         $successfullyset = $this->store->set_many($data);
         if ($this->perfdebug && $successfullyset) {
-            cache_helper::record_cache_set($this->storetype, $this->definition, $successfullyset);
+            cache_helper::record_cache_set($this->store, $this->definition, $successfullyset);
         }
         return $successfullyset;
     }
@@ -1112,7 +1112,7 @@ class cache implements cache_loader {
         }
         if ($result !== false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_hit('** static acceleration **', $this->definition);
+                cache_helper::record_cache_hit(cache_store::STATIC_ACCEL, $this->definition);
             }
             if ($this->staticaccelerationsize > 1 && $this->staticaccelerationcount > 1) {
                 // Check to see if this is the last item on the static acceleration keys array.
@@ -1126,7 +1126,7 @@ class cache implements cache_loader {
             return $result;
         } else {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss('** static acceleration **', $this->definition);
+                cache_helper::record_cache_miss(cache_store::STATIC_ACCEL, $this->definition);
             }
             return false;
         }
@@ -1830,7 +1830,7 @@ class cache_session extends cache {
         // 4. Load if from the loader/datasource if we don't already have it.
         if ($result === false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss($this->storetype, $this->get_definition());
+                cache_helper::record_cache_miss($this->get_store(), $this->get_definition());
             }
             if ($this->get_loader() !== false) {
                 // We must pass the original (unparsed) key to the next loader in the chain.
@@ -1845,7 +1845,7 @@ class cache_session extends cache {
                 $this->set($key, $result);
             }
         } else if ($this->perfdebug) {
-            cache_helper::record_cache_hit($this->storetype, $this->get_definition());
+            cache_helper::record_cache_hit($this->get_store(), $this->get_definition());
         }
         // 5. Validate strictness.
         if ($strictness === MUST_EXIST && $result === false) {
@@ -1889,7 +1889,7 @@ class cache_session extends cache {
             $loader->set($key, $data);
         }
         if ($this->perfdebug) {
-            cache_helper::record_cache_set($this->storetype, $this->get_definition());
+            cache_helper::record_cache_set($this->get_store(), $this->get_definition());
         }
         if (is_object($data) && $data instanceof cacheable_object) {
             $data = new cache_cached_object($data);
@@ -2019,8 +2019,8 @@ class cache_session extends cache {
                     $hits++;
                 }
             }
-            cache_helper::record_cache_hit($this->storetype, $this->get_definition(), $hits);
-            cache_helper::record_cache_miss($this->storetype, $this->get_definition(), $misses);
+            cache_helper::record_cache_hit($this->get_store(), $this->get_definition(), $hits);
+            cache_helper::record_cache_miss($this->get_store(), $this->get_definition(), $misses);
         }
         return $return;
 
@@ -2097,7 +2097,7 @@ class cache_session extends cache {
         }
         $successfullyset = $this->get_store()->set_many($data);
         if ($this->perfdebug && $successfullyset) {
-            cache_helper::record_cache_set($this->storetype, $this->get_definition(), $successfullyset);
+            cache_helper::record_cache_set($this->store, $this->get_definition(), $successfullyset);
         }
         return $successfullyset;
     }
index 4fcb03f..a2cfe3e 100644 (file)
@@ -144,6 +144,10 @@ abstract class cache_store implements cache_store_interface {
      * Request caches. Static caches really.
      */
     const MODE_REQUEST = 4;
+    /**
+     * Static caches.
+     */
+    const STATIC_ACCEL = '** static accel. **';
 
     /**
      * Constructs an instance of the cache store.
index 9c0f1d9..0e6b203 100644 (file)
@@ -2092,15 +2092,15 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertFalse($request->get('missMe'));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(1, $endstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(1, $endstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2116,24 +2116,24 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertTrue($request->set('setMe4', 4));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2149,24 +2149,24 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertEquals($request->get('setMe4'), 4);
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2176,24 +2176,24 @@ class core_cache_testcase extends advanced_testcase {
         $request->get_many(array('setMe1', 'setMe2', 'setMe3', 'setMe4'));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
     }
 
     public function test_static_cache() {
@@ -2225,8 +2225,8 @@ class core_cache_testcase extends advanced_testcase {
 
         // Check that the static acceleration worked, even on empty arrays and the number 0.
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['** static acceleration **']['misses']);
-        $this->assertEquals(3, $endstats[$applicationid]['stores']['** static acceleration **']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['** static accel. **']['misses']);
+        $this->assertEquals(3, $endstats[$applicationid]['stores']['** static accel. **']['hits']);
     }
 
     public function test_performance_debug_off() {
index eaf6344..076bd53 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /cache/stores/* - cache store plugins.
 Information provided here is intended especially for developers.
 
+=== 3.9 ===
+* The record_cache_hit/miss/set methods now take a cache_store instead of a cache_definition object
+
 === 3.8 ===
 * The Redis cache store can now make use of the Zstandard compression algorithm (see MDL-66428).
 
index d44cd94..31d3420 100644 (file)
Binary files a/calendar/amd/build/crud.min.js and b/calendar/amd/build/crud.min.js differ
index c6e0e09..499e7b6 100644 (file)
Binary files a/calendar/amd/build/crud.min.js.map and b/calendar/amd/build/crud.min.js.map differ
index 184a283..77dbc02 100644 (file)
Binary files a/calendar/amd/build/modal_delete.min.js and b/calendar/amd/build/modal_delete.min.js differ
index 4a4c185..090c326 100644 (file)
Binary files a/calendar/amd/build/modal_delete.min.js.map and b/calendar/amd/build/modal_delete.min.js.map differ
index 7853a67..c18cb56 100644 (file)
@@ -63,6 +63,7 @@ function(
      * @return {Promise}
      */
     function confirmDeletion(eventId, eventTitle, eventCount) {
+        var pendingPromise = new Pending('core_calendar/crud:confirmDeletion');
         var deleteStrings = [
             {
                 key: 'deleteevent',
@@ -139,6 +140,11 @@ function(
 
             return deleteModal;
         })
+        .then(function(modal) {
+            pendingPromise.resolve();
+
+            return modal;
+        })
         .catch(Notification.exception);
 
         return finalPromise;
index 9541dec..e457761 100644 (file)
@@ -55,6 +55,8 @@ function(
      */
     var ModalDelete = function(root) {
         Modal.call(this, root);
+
+        this.setRemoveOnClose(true);
     };
 
     ModalDelete.TYPE = 'core_calendar-modal_delete';
index 9a3f1d0..3b08693 100644 (file)
@@ -13,7 +13,7 @@
     "require-dev": {
         "phpunit/phpunit": "7.5.*",
         "phpunit/dbunit": "4.0.*",
-        "moodlehq/behat-extension": "3.39.2",
+        "moodlehq/behat-extension": "3.39.3",
         "mikey179/vfsstream": "^1.6",
         "instaclick/php-webdriver": "dev-local as 1.x-dev"
     }
index 7fd4234..11dbff6 100644 (file)
@@ -4,42 +4,44 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "a2329ee2d14a351b74f99322f42722da",
+    "content-hash": "b1953ceec577434625a7aee12f650daa",
     "packages": [],
     "packages-dev": [
         {
             "name": "behat/behat",
-            "version": "v3.5.0",
+            "version": "v3.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Behat.git",
-                "reference": "e4bce688be0c2029dc1700e46058d86428c63cab"
+                "reference": "9bfe195b4745c32e068af03fa4df9558b4916d30"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Behat/zipball/e4bce688be0c2029dc1700e46058d86428c63cab",
-                "reference": "e4bce688be0c2029dc1700e46058d86428c63cab",
+                "url": "https://api.github.com/repos/Behat/Behat/zipball/9bfe195b4745c32e068af03fa4df9558b4916d30",
+                "reference": "9bfe195b4745c32e068af03fa4df9558b4916d30",
                 "shasum": ""
             },
             "require": {
-                "behat/gherkin": "^4.5.1",
+                "behat/gherkin": "^4.6.0",
                 "behat/transliterator": "^1.2",
                 "container-interop/container-interop": "^1.2",
                 "ext-mbstring": "*",
                 "php": ">=5.3.3",
                 "psr/container": "^1.0",
-                "symfony/class-loader": "~2.1||~3.0",
-                "symfony/config": "~2.3||~3.0||~4.0",
-                "symfony/console": "~2.7.40||^2.8.33||~3.3.15||^3.4.3||^4.0.3",
-                "symfony/dependency-injection": "~2.1||~3.0||~4.0",
-                "symfony/event-dispatcher": "~2.1||~3.0||~4.0",
-                "symfony/translation": "~2.3||~3.0||~4.0",
-                "symfony/yaml": "~2.1||~3.0||~4.0"
+                "symfony/config": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/console": "^2.7.51 || ^2.8.33 || ^3.3.15 || ^3.4.3 || ^4.0.3 || ^5.0",
+                "symfony/dependency-injection": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/event-dispatcher": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/translation": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/yaml": "^2.7.51 || ^3.0 || ^4.0 || ^5.0"
             },
             "require-dev": {
                 "herrera-io/box": "~1.6.1",
-                "phpunit/phpunit": "^4.8.36|^6.3",
-                "symfony/process": "~2.5|~3.0|~4.0"
+                "phpunit/phpunit": "^4.8.36 || ^6.3",
+                "symfony/process": "~2.5 || ^3.0 || ^4.0 || ^5.0"
+            },
+            "suggest": {
+                "ext-dom": "Needed to output test results in JUnit format."
             },
             "bin": [
                 "bin/behat"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.5.x-dev"
+                    "dev-master": "3.6.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Behat\\Behat": "src/",
-                    "Behat\\Testwork": "src/"
+                "psr-4": {
+                    "Behat\\Behat\\": "src/Behat/Behat/",
+                    "Behat\\Testwork\\": "src/Behat/Testwork/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -83,7 +85,7 @@
                 "symfony",
                 "testing"
             ],
-            "time": "2018-08-10T18:56:51+00:00"
+            "time": "2020-02-06T09:54:48+00:00"
         },
         {
             "name": "behat/gherkin",
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.5.2",
+            "version": "6.5.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82"
+                "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82",
-                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aab4ebd862aa7d04f01a4b51849d657db56d882e",
+                "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "guzzlehttp/promises": "^1.0",
                 "guzzlehttp/psr7": "^1.6.1",
-                "php": ">=5.5"
+                "php": ">=5.5",
+                "symfony/polyfill-intl-idn": "^1.11"
             },
             "require-dev": {
                 "ext-curl": "*",
                 "psr/log": "^1.1"
             },
             "suggest": {
-                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
                 "psr/log": "Required for using the Log middleware"
             },
             "type": "library",
                 "rest",
                 "web service"
             ],
-            "time": "2019-12-23T11:57:10+00:00"
+            "time": "2020-04-18T10:38:46+00:00"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.39.2",
+            "version": "v3.39.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "7a2df2124ba8a85ccf21e517d18c78f932bdbbce"
+                "reference": "d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/7a2df2124ba8a85ccf21e517d18c78f932bdbbce",
-                "reference": "7a2df2124ba8a85ccf21e517d18c78f932bdbbce",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b",
+                "reference": "d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b",
                 "shasum": ""
             },
             "require": {
-                "behat/behat": "3.5.*",
+                "behat/behat": "3.6.*",
                 "behat/mink": "~1.8",
-                "behat/mink-extension": "~2.2",
+                "behat/mink-extension": "~2.3",
                 "behat/mink-goutte-driver": "~1.2",
-                "behat/mink-selenium2-driver": "~1.3",
+                "behat/mink-selenium2-driver": "~1.4",
                 "php": ">=7.2.0",
-                "symfony/process": "2.8.*"
+                "symfony/process": "^4.0 || ^5.0"
             },
             "type": "library",
             "autoload": {
                 "Behat",
                 "moodle"
             ],
-            "time": "2020-04-09T16:06:14+00:00"
+            "time": "2020-04-20T09:32:44+00:00"
         },
         {
             "name": "myclabs/deep-copy",
             "time": "2017-02-14T16:28:37+00:00"
         },
         {
-            "name": "psr/http-message",
-            "version": "1.0.1",
+            "name": "psr/event-dispatcher",
+            "version": "1.0.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/php-fig/http-message.git",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+                "url": "https://github.com/php-fig/event-dispatcher.git",
+                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.0"
+                "php": ">=7.2.0"
             },
             "type": "library",
             "extra": {
             },
             "autoload": {
                 "psr-4": {
-                    "Psr\\Http\\Message\\": "src/"
+                    "Psr\\EventDispatcher\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "homepage": "http://www.php-fig.org/"
                 }
             ],
-            "description": "Common interface for HTTP messages",
-            "homepage": "https://github.com/php-fig/http-message",
+            "description": "Standard interfaces for event handling.",
             "keywords": [
-                "http",
-                "http-message",
+                "events",
                 "psr",
-                "psr-7",
-                "request",
-                "response"
+                "psr-14"
             ],
-            "time": "2016-08-06T14:39:51+00:00"
+            "time": "2019-01-08T18:20:26+00:00"
         },
         {
-            "name": "psr/log",
-            "version": "1.1.3",
+            "name": "psr/http-message",
+            "version": "1.0.1",
             "source": {
                 "type": "git",
-                "url": "https://github.com/php-fig/log.git",
-                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
-                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1.x-dev"
+                    "dev-master": "1.0.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Psr\\Log\\": "Psr/Log/"
+                    "Psr\\Http\\Message\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "homepage": "http://www.php-fig.org/"
                 }
             ],
-            "description": "Common interface for logging libraries",
-            "homepage": "https://github.com/php-fig/log",
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
             "keywords": [
-                "log",
+                "http",
+                "h