Merge branch 'MDL-68443-xmldb-path-validation' of https://github.com/brendanheywood...
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 7 May 2020 17:16:37 +0000 (19:16 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 7 May 2020 17:16:37 +0000 (19:16 +0200)
579 files changed:
.eslintignore
.github/FUNDING.yml [new file with mode: 0644]
.stylelintignore
.travis.yml
admin/message.php
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/mobile/tests/api_test.php
admin/tool/recyclebin/tests/behat/basic_functionality.feature
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/actions/check_indexes/check_indexes.class.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 [new file with mode: 0644]
contentbank/amd/build/actions.min.js.map [new file with mode: 0644]
contentbank/amd/src/actions.js [new file with mode: 0644]
contentbank/classes/content.php
contentbank/classes/contentbank.php
contentbank/classes/contenttype.php
contentbank/classes/external/delete_content.php [new file with mode: 0644]
contentbank/classes/external/rename_content.php [new file with mode: 0644]
contentbank/classes/privacy/provider.php
contentbank/contenttype/h5p/classes/content.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/contenttype/h5p/tests/contenttype_h5p_test.php
contentbank/index.php
contentbank/templates/bankcontent.mustache
contentbank/templates/renamecontent.mustache [new file with mode: 0644]
contentbank/templates/toolbar.mustache
contentbank/tests/behat/delete_content.feature [new file with mode: 0644]
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/delete_content_test.php [new file with mode: 0644]
contentbank/tests/external/rename_content_test.php [new file with mode: 0644]
contentbank/tests/generator/lib.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/helper.php
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/classes/api.php
h5p/classes/helper.php
h5p/classes/player.php
h5p/h5plib/v124/lang/en/h5plib_v124.php
h5p/tests/api_test.php
h5p/tests/editor_test.php
h5p/tests/helper_test.php
install/lang/eu/install.php
install/lang/fr/error.php
install/lang/om/install.php [new file with mode: 0644]
install/lang/om/moodle.php [new file with mode: 0644]
install/lang/ps/langconfig.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/ajax/getnavbranch.php
lib/ajax/service.php
lib/amd/build/chart_base.min.js
lib/amd/build/chart_base.min.js.map
lib/amd/build/chart_output_chartjs.min.js
lib/amd/build/chart_output_chartjs.min.js.map
lib/amd/build/chart_series.min.js
lib/amd/build/chart_series.min.js.map
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/chart_base.js
lib/amd/src/chart_output_chartjs.js
lib/amd/src/chart_series.js
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_config_util.php
lib/behat/classes/behat_core_generator.php
lib/behat/lib.php
lib/classes/chart_base.php
lib/classes/chart_series.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/filetypes.php
lib/classes/hub/registration.php
lib/classes/ip_utils.php
lib/classes/plugin_manager.php
lib/classes/plugininfo/contenttype.php
lib/classes/session/database.php
lib/classes/session/handler.php
lib/classes/session/manager.php
lib/classes/session/memcached.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/ddl/database_manager.php
lib/ddl/tests/ddl_test.php
lib/dtl/database_exporter.php
lib/dtl/database_importer.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/externallib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/filestorage/file_system.php
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/ldaplib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/php-jwt/README.md
lib/php-jwt/composer.json
lib/php-jwt/src/BeforeValidException.php
lib/php-jwt/src/ExpiredException.php
lib/php-jwt/src/JWK.php [new file with mode: 0644]
lib/php-jwt/src/JWT.php
lib/php-jwt/src/SignatureInvalidException.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/tcpdf/readme_moodle.txt
lib/tcpdf/tcpdf.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_hooks.php
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/moodlelib_test.php
lib/tests/other/chartjstestpage.php
lib/tests/questionlib_test.php
lib/tests/session_manager_test.php
lib/tests/session_redis_test.php
lib/thirdpartylibs.xml
lib/typo3/class.t3lib_cs.php
lib/typo3/readme_moodle.txt
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/classes/manager.php
message/output/airnotifier/db/install.php
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/airnotifier/lib.php [new file with mode: 0644]
message/output/airnotifier/requestaccesskey.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/services.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/pix/icon.png
mod/h5pactivity/pix/icon.svg
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/db/caches.php [new file with mode: 0644]
mod/lti/edit_form.php
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/mod_form.php
mod/lti/tests/fixtures/test_keyset [new file with mode: 0644]
mod/lti/tests/locallib_test.php
mod/lti/token.php
mod/lti/version.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/report/responses/tests/fixtures/questions00.csv
mod/quiz/tests/attempt_walkthrough_from_csv_test.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/calculated/questiontype.php
question/type/calculated/tests/questiontype_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
question/type/multianswer/questiontype.php
question/type/multianswer/tests/questiontype_test.php
question/type/numerical/db/upgradelib.php
question/type/questiontypebase.php
question/type/random/questiontype.php
question/type/random/tests/questiontype_test.php
question/type/truefalse/questiontype.php
question/type/truefalse/tests/questiontype_test.php
tag/tests/behat/delete_tag.feature
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/contentbank.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/preset/default.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 [moved from user/classes/participants_table.php with 94% similarity]
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/
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644 (file)
index 0000000..adb6942
--- /dev/null
@@ -0,0 +1,2 @@
+# Primary donations pages.
+custom: ["https://moodle.com/donations/", moodle.org]
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 321c133..f60032e 100644 (file)
@@ -2,8 +2,6 @@
 # process (which uses our internal CI system) this file is here for the benefit
 # of community developers git clones - see MDL-51458.
 
-sudo: required
-
 # We currently disable Travis notifications entirely until https://github.com/travis-ci/travis-ci/issues/4976
 # is fixed.
 notifications:
@@ -11,6 +9,8 @@ notifications:
 
 language: php
 
+os: linux
+
 dist: xenial
 
 services:
@@ -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:
@@ -41,18 +41,18 @@ env:
     # Perform an upgrade test too.
     - DB=pgsql    TASK=UPGRADE
 
-matrix:
+jobs:
     # Enable fast finish.
     # This will fail the build if a single job fails (except those in allow_failures).
     # It will not stop the jobs from running.
     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 d004e9e..b5c31d7 100644 (file)
@@ -83,7 +83,7 @@ if (($form = data_submitted()) && confirm_sesskey()) {
                 }
             } else {
                 $newsettings = array();
-                if (array_key_exists($componentprovidersetting, $form)) {
+                if (property_exists($form, $componentprovidersetting)) {
                     // We must be processing loggedin or loggedoff checkboxes.
                     // Store defained comma-separated processors as setting value.
                     // Using array_filter eliminates elements set to 0 above.
index f904352..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();
         }
@@ -813,7 +814,7 @@ class core_admin_renderer extends plugin_renderer_base {
             }
         }
 
-        $updateinfo .= $this->container_start('checkforupdates');
+        $updateinfo .= $this->container_start('checkforupdates mt-1');
         $fetchurl = new moodle_url('/admin/index.php', array('fetchupdates' => 1, 'sesskey' => sesskey(), 'cache' => 0));
         $updateinfo .= $this->single_button($fetchurl, get_string('checkforupdates', 'core_plugin'));
         if ($fetch) {
@@ -902,7 +903,7 @@ class core_admin_renderer extends plugin_renderer_base {
      */
     protected function moodle_available_update_info(\core\update\info $updateinfo) {
 
-        $boxclasses = 'moodleupdateinfo';
+        $boxclasses = 'moodleupdateinfo mb-2';
         $info = array();
 
         if (isset($updateinfo->release)) {
@@ -922,7 +923,8 @@ class core_admin_renderer extends plugin_renderer_base {
         }
 
         if (isset($updateinfo->download)) {
-            $info[] = html_writer::link($updateinfo->download, get_string('download'), array('class' => 'info download'));
+            $info[] = html_writer::link($updateinfo->download, get_string('download'),
+                array('class' => 'info download btn btn-secondary'));
         }
 
         if (isset($updateinfo->url)) {
@@ -930,9 +932,9 @@ class core_admin_renderer extends plugin_renderer_base {
                 array('class' => 'info more'));
         }
 
-        $box  = $this->output->box_start($boxclasses);
-        $box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), '');
-        $box .= $this->output->box_end();
+        $box  = $this->output->container_start($boxclasses);
+        $box .= $this->output->container(implode(html_writer::tag('span', ' | ', array('class' => 'separator')), $info), '');
+        $box .= $this->output->container_end();
 
         return $box;
     }
@@ -1044,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);
@@ -1081,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);
@@ -1091,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')
                     );
                 }
 
@@ -1100,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')
                     );
                 }
 
@@ -1173,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) {
@@ -1181,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')
             );
         }
 
@@ -1190,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')
             );
         }
 
@@ -1199,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.
@@ -1275,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();
@@ -1294,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) {
@@ -1313,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.
 
@@ -1359,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');
                 }
             }
 
@@ -1373,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 = '';
@@ -1384,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),
@@ -1460,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';
@@ -1489,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) {
@@ -1543,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'
             );
@@ -1559,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 4d9a07d..18721a2 100644 (file)
@@ -78,15 +78,7 @@ class tool_mobile_api_testcase extends externallib_advanced_testcase {
         $CFG->debugdisplay = 1;
         set_config('debugauthdb', 1, 'auth_db');
         set_config('debugdb', 1, 'enrol_database');
-        $expectedissues = array('nohttpsformobilewarning', 'invaliduserquotawarning', 'adodbdebugwarning', 'displayerrorswarning',
-            'mobilenotificationsdisabledwarning');
-
-        $processors = get_message_processors();
-        foreach ($processors as $processor => $status) {
-            if ($processor == 'airnotifier' && $status->enabled) {
-                unset($expectedissues['mobilenotificationsdisabledwarning']);
-            }
-        }
+        $expectedissues = array('nohttpsformobilewarning', 'invaliduserquotawarning', 'adodbdebugwarning', 'displayerrorswarning');
 
         $issues = api::get_potential_config_issues();
         $this->assertCount(count($expectedissues), $issues);
index 4d0c7f2..ce2d8fd 100644 (file)
@@ -127,3 +127,36 @@ Feature: Basic recycle bin functionality
     And I press "Yes"
     And I should see "Recycle bin has been emptied"
     And I should see "There are no items in the recycle bin."
+
+  @javascript
+  Scenario: Show recycle bin on category action menu
+    Given I log in as "admin"
+    And I navigate to "Courses >  Manage courses and categories" in site administration
+    And I click on "Actions menu" "link"
+    And I click on "Recycle bin" "link"
+    Then I should see "There are no items in the recycle bin."
+
+  @javascript
+  Scenario: Not show recycle bin empty on category action menu whit autohide enable
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | categorybinenable | 0 | tool_recyclebin |
+    And I navigate to "Courses >  Manage courses and categories" in site administration
+    And I click on "Actions menu" "link"
+    Then I should not see "Recycle bin"
+
+  @javascript
+  Scenario: Show recycle bin not empty on category action menu whit autohide enable
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | autohide | 1 | tool_recyclebin |
+    And I navigate to "Courses >  Manage courses and categories" in site administration
+    And I click on "Actions menu" "link"
+    Then I should not see "Recycle bin"
+    And I click on "delete" action for "Course 2" in management course listing
+    And I press "Delete"
+    And I should see "Deleting C2"
+    And I should see "C2 has been completely deleted"
+    And I press "Continue"
+    When I click on "Actions menu" "link"
+    Then I should see "Recycle bin"
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 ba03bac..b7c175c 100644 (file)
@@ -44,11 +44,13 @@ class check_indexes extends XMLDBCheckAction {
 
         // Get needed strings
         $this->loadStrings(array(
+            'extraindexesfound' => 'tool_xmldb',
             'missing' => 'tool_xmldb',
             'key' => 'tool_xmldb',
             'index' => 'tool_xmldb',
             'missingindexes' => 'tool_xmldb',
-            'nomissingindexesfound' => 'tool_xmldb',
+            'nomissingorextraindexesfound' => 'tool_xmldb',
+            'yesextraindexesfound' => 'tool_xmldb',
             'yesmissingindexesfound' => 'tool_xmldb',
         ));
     }
@@ -58,6 +60,7 @@ class check_indexes extends XMLDBCheckAction {
         $dbman = $DB->get_manager();
 
         $o = '';
+        $dbindexes = $DB->get_indexes($xmldb_table->getName());
         $missing_indexes = array();
 
         // Keys
@@ -88,6 +91,7 @@ class check_indexes extends XMLDBCheckAction {
                     // Check if the index exists in DB
                     if ($dbman->index_exists($xmldb_table, $xmldb_index)) {
                         $o.='<font color="green">' . $this->str['ok'] . '</font>';
+                        $this->remove_index_from_dbindex($dbindexes, $xmldb_index);
                     } else {
                         $o.='<font color="red">' . $this->str['missing'] . '</font>';
                         // Add the missing index to the list
@@ -109,6 +113,7 @@ class check_indexes extends XMLDBCheckAction {
                 // Check if the index exists in DB
                 if ($dbman->index_exists($xmldb_table, $xmldb_index)) {
                     $o.='<font color="green">' . $this->str['ok'] . '</font>';
+                    $this->remove_index_from_dbindex($dbindexes, $xmldb_index);
                 } else {
                     $o.='<font color="red">' . $this->str['missing'] . '</font>';
                     // Add the missing index to the list
@@ -122,6 +127,14 @@ class check_indexes extends XMLDBCheckAction {
             $o.='        </ul>';
         }
 
+        // Hack - skip for table 'search_simpledb_index' as this plugin adds indexes dynamically on install
+        // which are not included in install.xml. See search/engine/simpledb/db/install.php.
+        if ($xmldb_table->getName() != 'search_simpledb_index') {
+            foreach ($dbindexes as $indexname => $index) {
+                $missing_indexes[] = $indexname;
+            }
+        }
+
         return array($o, $missing_indexes);
     }
 
@@ -129,33 +142,56 @@ class check_indexes extends XMLDBCheckAction {
         global $DB;
         $dbman = $DB->get_manager();
 
+        $missingindexes = [];
+        $extraindexes = [];
+
+        foreach ($missing_indexes as $missingindex) {
+            if (is_object($missingindex)) {
+                $missingindexes[] = $missingindex;
+            } else {
+                $extraindexes[] = $missingindex;
+            }
+        }
+
         $s = '';
         $r = '<table class="generaltable boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
         $r.= '  <tr><td class="generalboxcontent">';
         $r.= '    <h2 class="main">' . $this->str['searchresults'] . '</h2>';
-        $r.= '    <p class="centerpara">' . $this->str['missingindexes'] . ': ' . count($missing_indexes) . '</p>';
+        $r .= '    <p class="centerpara">' . $this->str['missingindexes'] . ': ' . count($missingindexes) . '</p>';
+        $r .= '    <p class="centerpara">' . $this->str['extraindexesfound'] . ': ' . count($extraindexes) . '</p>';
         $r.= '  </td></tr>';
         $r.= '  <tr><td class="generalboxcontent">';
 
-        // If we have found missing indexes inform about them
-        if (count($missing_indexes)) {
-            $r.= '    <p class="centerpara">' . $this->str['yesmissingindexesfound'] . '</p>';
-            $r.= '        <ul>';
-            foreach ($missing_indexes as $obj) {
-                $xmldb_table = $obj->table;
-                $xmldb_index = $obj->index;
-                $sqlarr = $dbman->generator->getAddIndexSQL($xmldb_table, $xmldb_index);
-                $r.= '            <li>' . $this->str['table'] . ': ' . $xmldb_table->getName() . '. ' .
-                                          $this->str['index'] . ': ' . $xmldb_index->readableInfo() . '</li>';
-                $sqlarr = $dbman->generator->getEndedStatements($sqlarr);
-                $s.= '<code>' . str_replace("\n", '<br />', implode('<br />', $sqlarr)) . '</code><br />';
+        // If we have found missing indexes or extra indexes inform the user about them.
+        if (!empty($missingindexes) || !empty($extraindexes)) {
+            if ($missingindexes) {
+                $r.= '    <p class="centerpara">' . $this->str['yesmissingindexesfound'] . '</p>';
+                $r.= '        <ul>';
+                foreach ($missingindexes as $obj) {
+                    $xmldb_table = $obj->table;
+                    $xmldb_index = $obj->index;
+                    $sqlarr = $dbman->generator->getAddIndexSQL($xmldb_table, $xmldb_index);
+                    $r.= '            <li>' . $this->str['table'] . ': ' . $xmldb_table->getName() . '. ' .
+                                              $this->str['index'] . ': ' . $xmldb_index->readableInfo() . '</li>';
+                    $sqlarr = $dbman->generator->getEndedStatements($sqlarr);
+                    $s.= '<code>' . str_replace("\n", '<br />', implode('<br />', $sqlarr)) . '</code><br />';
 
+                }
+                $r.= '        </ul>';
+                // Add the SQL statements (all together)
+                $r.= '<hr />' . $s;
+            }
+            if ($extraindexes) {
+                $r .= '<p class="centerpara">' . $this->str['yesextraindexesfound'] . '</p>';
+                $r .= '<ul>';
+                foreach ($extraindexes as $ei) {
+                    $r .= '<li>' . $ei . '</li>';
+                }
+                $r .= '</ul>';
+                $r .= '<hr />';
             }
-            $r.= '        </ul>';
-            // Add the SQL statements (all together)
-            $r.= '<hr />' . $s;
         } else {
-            $r.= '    <p class="centerpara">' . $this->str['nomissingindexesfound'] . '</p>';
+            $r .= '<p class="centerpara">' . $this->str['nomissingorextraindexesfound'] . '</p>';
         }
         $r.= '  </td></tr>';
         $r.= '  <tr><td class="generalboxcontent">';
@@ -166,4 +202,18 @@ class check_indexes extends XMLDBCheckAction {
 
         return $r;
     }
+
+    /**
+     * Removes an index from the array $dbindexes if it is found.
+     *
+     * @param array $dbindexes
+     * @param xmldb_index $index
+     */
+    private function remove_index_from_dbindex(array &$dbindexes, xmldb_index $index) {
+        foreach ($dbindexes as $key => $dbindex) {
+            if ($dbindex['columns'] == $index->getFields()) {
+                unset($dbindexes[$key]);
+            }
+        }
+    }
 }
index c465ed1..e879835 100644 (file)
@@ -99,6 +99,7 @@ $string['edit_xml_file'] = 'Edit XML file';
 $string['enumvaluesincorrect'] = 'Incorrect values for enum field';
 $string['expected'] = 'Expected';
 $string['extensionrequired'] = 'Sorry - the PHP extension \'{$a}\' is required for this action. Please install the extension if you want to use this feature.';
+$string['extraindexesfound'] = 'Extra indexes found';
 $string['field'] = 'Field';
 $string['fieldnameempty'] = 'Name field empty';
 $string['fields'] = 'Fields';
@@ -157,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['nomissingindexesfound'] = 'No missing 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';
@@ -215,6 +216,7 @@ $string['wronglengthforenum'] = 'Incorrect length for enum field';
 $string['wrongnumberofreffields'] = 'Wrong number of reference fields';
 $string['wrongreservedwords'] = 'Currently used reserved words<br />(note that table names aren\'t important if using $CFG->prefix)';
 $string['wrongoraclesemantics'] = 'Wrong Oracle BYTE semantics found';
+$string['yesextraindexesfound'] = 'The following additional indexes were found.';
 $string['yesmissingindexesfound'] = '<p>Some missing indexes have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to create all of them. Remember to backup your data first!</p>
 <p>After doing that, it\'s highly recommended to execute this utility again to check that no more missing indexes are found.</p>';
 $string['yeswrongdefaultsfound'] = '<p>Some inconsistent defaults have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to fix them all. Remember to backup your data first!</p>
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.
  &nb