Merge branch 'MDL-68631' of https://github.com/jonof/moodle
authorJake Dallimore <jake@moodle.com>
Wed, 13 May 2020 01:24:30 +0000 (09:24 +0800)
committerJake Dallimore <jake@moodle.com>
Wed, 13 May 2020 01:24:30 +0000 (09:24 +0800)
540 files changed:
.gherkin-lintrc
.nvmrc
Gruntfile.js
admin/cli/adhoc_task.php [new file with mode: 0644]
admin/cli/scheduled_task.php [new file with mode: 0644]
admin/renderer.php
admin/settings/appearance.php
admin/settings/h5p.php
admin/tests/behat/filter_users.feature
admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js
admin/tool/lp/amd/build/menubar.min.js
admin/tool/lp/amd/build/menubar.min.js.map
admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-min.js
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/db/services.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/lib.php
admin/tool/mobile/settings.php
admin/tool/mobile/tests/api_test.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/version.php
admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-min.js
admin/tool/policy/styles.css
admin/tool/task/classes/run_from_cli.php
admin/tool/task/cli/adhoc_task.php
admin/tool/task/cli/schedule_task.php
admin/tool/task/renderer.php
admin/tool/task/schedule_task.php
admin/tool/uploadcourse/tests/behat/create.feature
admin/tool/uploadcourse/tests/behat/update.feature
admin/tool/uploaduser/tests/behat/upload_users.feature
admin/tool/usertours/amd/build/tour.min.js
admin/tool/usertours/amd/build/tour.min.js.map
admin/tool/xmldb/actions/view_xml/view_xml.class.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/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js
availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js
availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js
availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js
availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js
availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js
availability/tests/fixtures/mock_info_module.php [new file with mode: 0644]
availability/tests/fixtures/mock_info_section.php [new file with mode: 0644]
availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js
backup/util/ui/tests/behat/restore_moodle2_courses_settings.feature
backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-min.js
backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-min.js
badges/tests/behat/award_badge_groups.feature
blocks/navigation/amd/build/ajax_response_renderer.min.js
blocks/navigation/amd/build/ajax_response_renderer.min.js.map
blocks/recent_activity/renderer.php
blocks/recent_activity/styles.css
blocks/recentlyaccesseditems/tests/behat/block_recentlyaccesseditems_dashboard.feature
blocks/settings/styles.css
calendar/amd/build/view_manager.min.js
calendar/amd/build/view_manager.min.js.map
contentbank/classes/content.php
contentbank/classes/contentbank.php
contentbank/classes/contenttype.php
contentbank/contenttype/h5p/classes/contenttype.php
contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature
contentbank/tests/behat/access_permissions.feature
contentbank/tests/behat/events.feature [new file with mode: 0644]
contentbank/tests/contentbank_test.php
contentbank/upload.php
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/src/activitychooser.js
course/format/renderer.php
course/format/singleactivity/tests/behat/edit_format_course.feature
course/format/topics/styles.css [deleted file]
course/format/weeks/styles.css [deleted file]
course/recent.php
course/renderer.php
course/tests/behat/category_resort.feature
course/tests/behat/course_category_management_listing.feature
course/tests/behat/course_resort.feature
course/tests/behat/create_delete_course.feature
course/tests/behat/edit_settings.feature
course/yui/build/moodle-course-categoryexpander/moodle-course-categoryexpander-min.js
course/yui/build/moodle-course-coursebase/moodle-course-coursebase-min.js
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/build/moodle-course-formatchooser/moodle-course-formatchooser-min.js
course/yui/build/moodle-course-management/moodle-course-management-debug.js
course/yui/build/moodle-course-management/moodle-course-management-min.js
course/yui/build/moodle-course-management/moodle-course-management.js
course/yui/build/moodle-course-util-base/moodle-course-util-base-min.js
course/yui/build/moodle-course-util-cm/moodle-course-util-cm-min.js
course/yui/build/moodle-course-util-section/moodle-course-util-section-min.js
course/yui/src/dragdrop/js/section.js
course/yui/src/management/build.json
course/yui/src/management/js/category.js
course/yui/src/management/js/console.js
course/yui/src/management/js/course.js
course/yui/src/management/js/dd.js
course/yui/src/management/js/item.js
course/yui/src/management/js/shared.js [new file with mode: 0644]
customfield/field/date/tests/behat/field.feature
customfield/tests/behat/unique_field.feature
enrol/database/settings.php
enrol/database/upgrade.txt
enrol/guest/tests/behat/guest_access.feature
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js
filter/mathjaxloader/db/upgrade.php
filter/mathjaxloader/readme_moodle.txt
filter/mathjaxloader/settings.php
filter/mathjaxloader/version.php
filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-min.js
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/grading/form/guide/templates/grades/grader/gradingpanel.mustache
grade/grading/form/rubric/tests/behat/grade_calculation.feature
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js
grade/report/grader/yui/src/gradereporttable/js/floatingheaders.js
grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-min.js
grade/tests/behat/grade_override_letter.feature
group/tests/behat/create_groups.feature
group/tests/behat/group_description.feature
h5p/classes/core.php
h5p/classes/editor_ajax.php
h5p/classes/editor_framework.php
h5p/classes/helper.php
h5p/classes/local/library/autoloader.php
h5p/tests/editor_ajax_test.php
h5p/tests/editor_framework_test.php
h5p/tests/generator/lib.php
h5p/tests/h5p_core_test.php
install/lang/en_us_wp/langconfig.php [new file with mode: 0644]
install/lang/nl_wp/langconfig.php [new file with mode: 0644]
install/lang/pt_br_wp/langconfig.php [new file with mode: 0644]
lang/en/admin.php
lang/en/cache.php
lang/en/contentbank.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/adapter.min.js
lib/amd/build/adapter.min.js.map
lib/amd/build/chart_base.min.js
lib/amd/build/chart_base.min.js.map
lib/amd/build/chart_series.min.js
lib/amd/build/chart_series.min.js.map
lib/amd/build/chartjs-lazy.min.js
lib/amd/build/chartjs-lazy.min.js.map
lib/amd/build/drawer.min.js
lib/amd/build/drawer.min.js.map
lib/amd/build/emoji/auto_complete.min.js
lib/amd/build/emoji/auto_complete.min.js.map
lib/amd/build/emoji/picker.min.js
lib/amd/build/emoji/picker.min.js.map
lib/amd/build/local/modal/alert.min.js
lib/amd/build/local/modal/alert.min.js.map
lib/amd/build/loglevel.min.js
lib/amd/build/loglevel.min.js.map
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_save_cancel.min.js
lib/amd/build/modal_save_cancel.min.js.map
lib/amd/build/mustache.min.js
lib/amd/build/mustache.min.js.map
lib/amd/build/notification.min.js
lib/amd/build/notification.min.js.map
lib/amd/build/popper.min.js
lib/amd/build/popper.min.js.map
lib/amd/build/prefetch.min.js
lib/amd/build/prefetch.min.js.map
lib/amd/build/sortable_list.min.js
lib/amd/build/sortable_list.min.js.map
lib/amd/build/str.min.js
lib/amd/build/str.min.js.map
lib/amd/build/templates.min.js
lib/amd/build/templates.min.js.map
lib/amd/build/toast.min.js
lib/amd/build/toast.min.js.map
lib/amd/build/truncate.min.js
lib/amd/build/truncate.min.js.map
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/hub/registration.php
lib/classes/qrcode.php [moved from enrol/database/settingslib.php with 56% similarity]
lib/classes/task/h5p_get_content_types_task.php
lib/classes/task/manager.php
lib/cronlib.php
lib/db/caches.php
lib/deprecatedlib.php
lib/editor/atto/plugins/accessibilitychecker/tests/behat/accessibilitychecker.feature
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js
lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button-min.js
lib/editor/atto/plugins/align/yui/build/moodle-atto_align-button/moodle-atto_align-button-min.js
lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-min.js
lib/editor/atto/plugins/bold/yui/build/moodle-atto_bold-button/moodle-atto_bold-button-min.js
lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-min.js
lib/editor/atto/plugins/clear/yui/build/moodle-atto_clear-button/moodle-atto_clear-button-min.js
lib/editor/atto/plugins/collapse/yui/build/moodle-atto_collapse-button/moodle-atto_collapse-button-min.js
lib/editor/atto/plugins/emojipicker/yui/build/moodle-atto_emojipicker-button/moodle-atto_emojipicker-button-min.js
lib/editor/atto/plugins/emoticon/yui/build/moodle-atto_emoticon-button/moodle-atto_emoticon-button-min.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js
lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-min.js
lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-min.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-min.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-min.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.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/indent/yui/build/moodle-atto_indent-button/moodle-atto_indent-button-min.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/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js
lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-button/moodle-atto_managefiles-button-min.js
lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-usedfiles/moodle-atto_managefiles-usedfiles-min.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js
lib/editor/atto/plugins/noautolink/yui/build/moodle-atto_noautolink-button/moodle-atto_noautolink-button-min.js
lib/editor/atto/plugins/orderedlist/yui/build/moodle-atto_orderedlist-button/moodle-atto_orderedlist-button-min.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button-min.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js
lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-min.js
lib/editor/atto/plugins/strike/yui/build/moodle-atto_strike-button/moodle-atto_strike-button-min.js
lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-min.js
lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-min.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js
lib/editor/atto/plugins/title/yui/build/moodle-atto_title-button/moodle-atto_title-button-min.js
lib/editor/atto/plugins/underline/yui/build/moodle-atto_underline-button/moodle-atto_underline-button-min.js
lib/editor/atto/plugins/undo/yui/build/moodle-atto_undo-button/moodle-atto_undo-button-min.js
lib/editor/atto/plugins/unorderedlist/yui/build/moodle-atto_unorderedlist-button/moodle-atto_unorderedlist-button-min.js
lib/editor/atto/tests/behat/customtoolbar.feature
lib/editor/atto/tests/behat/disablecontrol.feature
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/build/moodle-editor_atto-menu/moodle-editor_atto-menu-min.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js
lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy-min.js
lib/editor/atto/yui/src/editor/js/autosave.js
lib/editor/atto/yui/src/editor/js/clean.js
lib/editor/atto/yui/src/editor/js/editor-plugin-buttons.js
lib/editor/atto/yui/src/editor/js/selection.js
lib/editor/atto/yui/src/editor/js/styling.js
lib/editor/atto/yui/src/editor/js/toolbar-keyboardnav.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-debug.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-min.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector.js
lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask-min.js
lib/form/yui/build/moodle-form-shortforms/moodle-form-shortforms-min.js
lib/form/yui/src/dateselector/build.json
lib/form/yui/src/dateselector/js/calendar.js
lib/form/yui/src/dateselector/js/dateselector.js
lib/form/yui/src/dateselector/js/moodlecalendar.js
lib/form/yui/src/dateselector/js/shared.js [new file with mode: 0644]
lib/grade/grade_category.php
lib/grade/grade_grade.php
lib/grade/grade_item.php
lib/moodlelib.php
lib/outputrenderers.php
lib/questionlib.php
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/repository.js
lib/table/classes/external/dynamic/fetch.php
lib/tablelib.php
lib/tests/accesslib_test.php
lib/tests/adminlib_test.php [new file with mode: 0644]
lib/tests/behat/securelayout.feature
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/qrcode_test.php [new file with mode: 0644]
lib/tests/questionlib_test.php
lib/upgrade.txt
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-min.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js
lib/yui/build/moodle-core-event/moodle-core-event-min.js
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js
lib/yui/build/moodle-core-handlebars/moodle-core-handlebars-min.js
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-min.js
lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-min.js
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-debug.js
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-min.js
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js
lib/yui/build/moodle-core-notification/moodle-core-notification-min.js
lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-min.js
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-min.js
lib/yui/build/moodle-core-widget-focusafterclose/moodle-core-widget-focusafterclose-min.js
lib/yui/src/blocks/build.json
lib/yui/src/blocks/js/blockregion.js
lib/yui/src/blocks/js/blocks.js
lib/yui/src/blocks/js/manager.js
lib/yui/src/blocks/js/shared.js [new file with mode: 0644]
lib/yui/src/languninstallconfirm/js/languninstallconfirm.js
lib/yui/src/notification/js/ajaxexception.js
lib/yui/src/notification/js/alert.js
lib/yui/src/notification/js/confirm.js
lib/yui/src/notification/js/dialogue.js
lib/yui/src/notification/js/exception.js
lib/yui/src/notification/js/info.js
lib/yui/src/notification/js/shared.js
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/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/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_router.min.js
message/amd/build/message_drawer_router.min.js.map
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation.min.js.map
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_conversation_patcher.min.js.map
message/amd/build/message_drawer_view_search.min.js
message/amd/build/message_drawer_view_search.min.js.map
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/airnotifier/yui/build/moodle-message_airnotifier-toolboxes/moodle-message_airnotifier-toolboxes-min.js
message/output/popup/amd/build/notification_area_control_area.min.js
message/output/popup/amd/build/notification_area_control_area.min.js.map
message/tests/behat/message_manage_notification_preferences.feature
message/tests/behat/message_preferences.feature
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotation.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationhighlight.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationline.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationoval.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationpen.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationrectangle.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationstamp.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/feedback/editpdf/yui/src/editor/js/commentsearch.js
mod/assign/feedback/editpdf/yui/src/editor/js/drawable.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/quickcommentlist.js
mod/assign/lib.php
mod/assign/tests/behat/hide_grader.feature
mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-min.js
mod/chat/lib.php
mod/data/tests/behat/add_entries.feature
mod/folder/lib.php
mod/forum/amd/build/discussion_nested_v2.min.js
mod/forum/amd/build/discussion_nested_v2.min.js.map
mod/forum/amd/build/grades/expandconversation.min.js
mod/forum/amd/build/grades/expandconversation.min.js.map
mod/forum/amd/build/grades/grader.min.js
mod/forum/amd/build/grades/grader.min.js.map
mod/forum/amd/build/local/grades/grader.min.js
mod/forum/amd/build/local/grades/grader.min.js.map
mod/forum/amd/build/local/grades/local/grader/user_picker.min.js
mod/forum/amd/build/local/grades/local/grader/user_picker.min.js.map
mod/forum/amd/build/local/layout/fullscreen.min.js
mod/forum/amd/build/local/layout/fullscreen.min.js.map
mod/forum/lib.php
mod/forum/tests/behat/add_forum_inline.feature
mod/forum/tests/behat/favourite_discussion.feature
mod/forum/tests/behat/inpage_reply.feature
mod/glossary/lib.php
mod/h5pactivity/backup/moodle2/backup_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/tests/local/attempt_test.php
mod/h5pactivity/tests/restore_test.php
mod/h5pactivity/tests/xapi/handler_test.php
mod/h5pactivity/version.php
mod/lesson/tests/behat/duplicate_lesson_page.feature
mod/lesson/tests/behat/import_fillintheblank_question.feature
mod/lesson/tests/behat/lesson_student_dashboard.feature
mod/lesson/tests/behat/teacher_grade_essays.feature
mod/quiz/accessrule/seb/classes/quiz_settings.php
mod/quiz/accessrule/seb/classes/settings_provider.php
mod/quiz/accessrule/seb/tests/behat/edit_form.feature
mod/quiz/accessrule/seb/tests/phpunit/backup_restore_test.php
mod/quiz/accessrule/seb/tests/phpunit/quiz_settings_test.php
mod/quiz/accessrule/seb/tests/phpunit/settings_provider_test.php
mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js
mod/quiz/yui/build/moodle-mod_quiz-modform/moodle-mod_quiz-modform-min.js
mod/quiz/yui/build/moodle-mod_quiz-questionchooser/moodle-mod_quiz-questionchooser-min.js
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js
mod/quiz/yui/build/moodle-mod_quiz-util-base/moodle-mod_quiz-util-base-min.js
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-min.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js
mod/quiz/yui/src/dragdrop/js/resource.js
mod/quiz/yui/src/toolboxes/js/resource.js
mod/quiz/yui/src/toolboxes/js/section.js
mod/resource/tests/behat/display_resource.feature
mod/scorm/lang/en/scorm.php
mod/scorm/locallib.php
mod/scorm/tests/behat/completion_condition_require_status.feature
mod/scorm/tests/behat/multisco_review_mode.feature
mod/survey/lib.php
mod/wiki/lib.php
mod/wiki/tests/behat/reset_wiki_comments_tags_files.feature
mod/workshop/lib.php
npm-shrinkwrap.json
package.json
pix/i/contentbank.png [new file with mode: 0644]
pix/i/contentbank.svg [new file with mode: 0644]
portfolio/googledocs/lib.php
portfolio/googledocs/upgrade.txt [new file with mode: 0644]
question/format/aiken/tests/behat/aiken_import.feature
question/format/gift/format.php
question/format/gift/tests/giftformat_test.php
question/type/ddimageortext/amd/build/question.min.js
question/type/ddimageortext/amd/build/question.min.js.map
question/type/ddmarker/amd/build/question.min.js
question/type/ddmarker/amd/build/question.min.js.map
question/type/ddmarker/styles.css
question/type/ddwtos/amd/build/ddwtos.min.js
question/type/ddwtos/amd/build/ddwtos.min.js.map
question/yui/build/moodle-question-chooser/moodle-question-chooser-min.js
question/yui/build/moodle-question-preview/moodle-question-preview-min.js
question/yui/build/moodle-question-searchform/moodle-question-searchform-min.js
report/configlog/tests/behat/view_report.feature
report/eventlist/yui/build/moodle-report_eventlist-eventfilter/moodle-report_eventlist-eventfilter-min.js
report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-min.js
repository/filepicker.js
repository/tests/behat/edit_file.feature [new file with mode: 0644]
repository/tests/behat/select_file.feature [new file with mode: 0644]
theme/boost/amd/build/carousel.min.js
theme/boost/amd/build/carousel.min.js.map
theme/boost/amd/build/collapse.min.js
theme/boost/amd/build/collapse.min.js.map
theme/boost/amd/build/dropdown.min.js
theme/boost/amd/build/dropdown.min.js.map
theme/boost/amd/build/modal.min.js
theme/boost/amd/build/modal.min.js.map
theme/boost/amd/build/popover.min.js
theme/boost/amd/build/popover.min.js.map
theme/boost/amd/build/sanitizer.min.js
theme/boost/amd/build/sanitizer.min.js.map
theme/boost/amd/build/scrollspy.min.js
theme/boost/amd/build/scrollspy.min.js.map
theme/boost/amd/build/tether.min.js
theme/boost/amd/build/tether.min.js.map
theme/boost/amd/build/toast.min.js
theme/boost/amd/build/toast.min.js.map
theme/boost/amd/build/tooltip.min.js
theme/boost/amd/build/tooltip.min.js.map
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/chat.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/print.scss
theme/boost/scss/moodle/tables.scss
theme/boost/style/moodle.css
theme/boost/templates/navbar-secure.mustache
theme/boost/tests/behat/settingstabs.feature
theme/boost/tests/behat/tour_filter.feature
theme/classic/style/moodle.css
theme/classic/templates/navbar-secure.mustache
theme/upgrade.txt
user/amd/build/status_field.min.js
user/amd/build/status_field.min.js.map
user/tests/behat/course_preference.feature
user/tests/behat/custom_profile_fields.feature
user/tests/behat/filter_idnumber.feature
user/tests/behat/filter_participants.feature
user/tests/behat/set_default_homepage.feature
user/tests/behat/view_participants.feature
version.php

index 9439956..50de756 100644 (file)
@@ -3,6 +3,8 @@
     "Feature": 0,
     "Background": 2,
     "Scenario": 2,
+    "Examples": 4,
+    "example": 6,
     "Step": 4,
     "given": 4,
     "and": 4
@@ -14,7 +16,9 @@
   "no-multiple-empty-lines": "on",
   "no-partially-commented-tag-lines": "on",
   "no-trailing-spaces": "on",
-  "no-unamed-features": "on",
-  "no-unamed-scenarios": "on",
-  "no-scenario-outlines-without-examples": "on"
+  "no-unnamed-features": "on",
+  "no-unnamed-scenarios": "on",
+  "no-scenario-outlines-without-examples": "on",
+  "no-examples-in-scenarios": "on",
+  "new-line-at-eof": ["on", "yes"]
 }
diff --git a/.nvmrc b/.nvmrc
index 7796292..01f1a56 100644 (file)
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v8.16.1
+v14.0.0
index 657bd77..a7b728f 100644 (file)
@@ -149,6 +149,7 @@ module.exports = function(grunt) {
     const watchmanClient = new watchman.Client();
     const fs = require('fs');
     const ComponentList = require(path.resolve('GruntfileComponents.js'));
+    const sass = require('node-sass');
 
     // Verify the node version is new enough.
     var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node);
@@ -358,6 +359,7 @@ module.exports = function(grunt) {
                 }
             },
             options: {
+                implementation: sass,
                 includePaths: ["theme/boost/scss/", "theme/classic/scss/"]
             }
         },
@@ -527,23 +529,28 @@ module.exports = function(grunt) {
         const options = grunt.config('gherkinlint.options');
 
         // Grab the gherkin-lint linter and required scaffolding.
-        const linter = require('gherkin-lint/src/linter.js');
-        const featureFinder = require('gherkin-lint/src/feature-finder.js');
-        const configParser = require('gherkin-lint/src/config-parser.js');
-        const formatter = require('gherkin-lint/src/formatters/stylish.js');
+        const linter = require('gherkin-lint/dist/linter.js');
+        const featureFinder = require('gherkin-lint/dist/feature-finder.js');
+        const configParser = require('gherkin-lint/dist/config-parser.js');
+        const formatter = require('gherkin-lint/dist/formatters/stylish.js');
 
         // Run the linter.
-        const results = linter.lint(
+        return linter.lint(
             featureFinder.getFeatureFiles(grunt.file.expand(options.files)),
             configParser.getConfiguration(configParser.defaultConfigFileName)
-        );
-
-        // Print the results out uncondtionally.
-        formatter.printResults(results);
-
-        // Report on the results.
-        // The done function takes a bool whereby a falsey statement causes the task to fail.
-        done(results.every(result => result.errors.length === 0));
+        )
+        .then(results => {
+            // Print the results out uncondtionally.
+            formatter.printResults(results);
+
+            return results;
+        })
+        .then(results => {
+            // Report on the results.
+            // The done function takes a bool whereby a falsey statement causes the task to fail.
+            return results.every(result => result.errors.length === 0);
+        })
+        .then(done); // eslint-disable-line promise/no-callback-in-promise
     };
 
     tasks.startup = function() {
diff --git a/admin/cli/adhoc_task.php b/admin/cli/adhoc_task.php
new file mode 100644 (file)
index 0000000..b0ed21d
--- /dev/null
@@ -0,0 +1,122 @@
+<?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/>.
+
+/**
+ * Task executor for adhoc tasks.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+require_once("{$CFG->libdir}/clilib.php");
+require_once("{$CFG->libdir}/cronlib.php");
+
+list($options, $unrecognized) = cli_get_params(
+    [
+        'execute' => false,
+        'help' => false,
+        'keep-alive' => 0,
+        'showsql' => false,
+        'showdebugging' => false,
+        'ignorelimits' => false,
+    ], [
+        'h' => 'help',
+        'e' => 'execute',
+        'k' => 'keep-alive',
+        'i' => 'ignorelimits',
+    ]
+);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help'] or empty($options['execute'])) {
+    $help = <<<EOT
+Ad hoc cron tasks.
+
+Options:
+ -h, --help                Print out this help
+     --showsql             Show sql queries before they are executed
+     --showdebugging       Show developer level debugging information
+ -e, --execute             Run all queued adhoc tasks
+ -k, --keep-alive=N        Keep this script alive for N seconds and poll for new adhoc tasks
+ -i  --ignorelimits        Ignore task_adhoc_concurrency_limit and task_adhoc_max_runtime limits
+
+Example:
+\$sudo -u www-data /usr/bin/php admin/cli/adhoc_task.php --execute
+
+EOT;
+
+    echo $help;
+    die;
+}
+
+if ($options['showdebugging']) {
+    set_debugging(DEBUG_DEVELOPER, true);
+}
+
+if ($options['showsql']) {
+    $DB->set_debug(true);
+}
+
+if (CLI_MAINTENANCE) {
+    echo "CLI maintenance mode active, cron execution suspended.\n";
+    exit(1);
+}
+
+if (moodle_needs_upgrading()) {
+    echo "Moodle upgrade pending, cron execution suspended.\n";
+    exit(1);
+}
+
+if (empty($options['execute'])) {
+    exit(0);
+}
+if (empty($options['keep-alive'])) {
+    $options['keep-alive'] = 0;
+}
+
+if (!empty($CFG->showcronsql)) {
+    $DB->set_debug(true);
+}
+if (!empty($CFG->showcrondebugging)) {
+    set_debugging(DEBUG_DEVELOPER, true);
+}
+
+$checklimits = empty($options['ignorelimits']);
+
+core_php_time_limit::raise();
+
+// Increase memory limit.
+raise_memory_limit(MEMORY_EXTRA);
+
+// Emulate normal session - we use admin account by default.
+cron_setup_user();
+
+$humantimenow = date('r', time());
+$keepalive = (int)$options['keep-alive'];
+
+\core\local\cli\shutdown::script_supports_graceful_exit();
+
+mtrace("Server Time: {$humantimenow}\n");
+cron_run_adhoc_tasks(time(), $keepalive, $checklimits);
diff --git a/admin/cli/scheduled_task.php b/admin/cli/scheduled_task.php
new file mode 100644 (file)
index 0000000..f825f46
--- /dev/null
@@ -0,0 +1,151 @@
+<?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/>.
+
+/**
+ * CLI task execution.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2014 Petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+require_once("$CFG->libdir/clilib.php");
+require_once("$CFG->libdir/cronlib.php");
+
+list($options, $unrecognized) = cli_get_params(
+    array('help' => false, 'list' => false, 'execute' => false, 'showsql' => false, 'showdebugging' => false),
+    array('h' => 'help')
+);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help'] or (!$options['list'] and !$options['execute'])) {
+    $help =
+    "Scheduled cron tasks.
+
+    Options:
+    --execute=\\some\\task  Execute scheduled task manually
+    --list                List all scheduled tasks
+    --showsql             Show sql queries before they are executed
+    --showdebugging       Show developer level debugging information
+    -h, --help            Print out this help
+
+    Example:
+    \$sudo -u www-data /usr/bin/php admin/cli/scheduled_task.php --execute=\\core\\task\\session_cleanup_task
+
+    ";
+
+    echo $help;
+    die;
+}
+
+if ($options['showdebugging']) {
+    set_debugging(DEBUG_DEVELOPER, true);
+}
+
+if ($options['showsql']) {
+    $DB->set_debug(true);
+}
+if ($options['list']) {
+    cli_heading("List of scheduled tasks ($CFG->wwwroot)");
+
+    $shorttime = get_string('strftimedatetimeshort');
+
+    $tasks = \core\task\manager::get_all_scheduled_tasks();
+    echo str_pad(get_string('scheduledtasks', 'tool_task'), 50, ' ') . ' ' . str_pad(get_string('runpattern', 'tool_task'), 17, ' ')
+        . ' ' . str_pad(get_string('lastruntime', 'tool_task'), 40, ' ') . get_string('nextruntime', 'tool_task') . "\n";
+    foreach ($tasks as $task) {
+        $class = '\\' . get_class($task);
+        $schedule = $task->get_minute() . ' '
+            . $task->get_hour() . ' '
+            . $task->get_day() . ' '
+            . $task->get_day_of_week() . ' '
+            . $task->get_month() . ' '
+            . $task->get_day_of_week();
+        $nextrun = $task->get_next_run_time();
+        $lastrun = $task->get_last_run_time();
+
+        $plugininfo = core_plugin_manager::instance()->get_plugin_info($task->get_component());
+        $plugindisabled = $plugininfo && $plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled();
+
+        if ($plugindisabled) {
+            $nextrun = get_string('plugindisabled', 'tool_task');
+        } else if ($task->get_disabled()) {
+            $nextrun = get_string('taskdisabled', 'tool_task');
+        } else if ($nextrun > time()) {
+            $nextrun = userdate($nextrun);
+        } else {
+            $nextrun = get_string('asap', 'tool_task');
+        }
+
+        if ($lastrun) {
+            $lastrun = userdate($lastrun);
+        } else {
+            $lastrun = get_string('never');
+        }
+
+        echo str_pad($class, 50, ' ') . ' ' . str_pad($schedule, 17, ' ') .
+            ' ' . str_pad($lastrun, 40, ' ') . ' ' . $nextrun . "\n";
+    }
+    exit(0);
+}
+
+if ($execute = $options['execute']) {
+    if (!$task = \core\task\manager::get_scheduled_task($execute)) {
+        mtrace("Task '$execute' not found");
+        exit(1);
+    }
+
+    if (moodle_needs_upgrading()) {
+        mtrace("Moodle upgrade pending, cannot execute tasks.");
+        exit(1);
+    }
+
+    // Increase memory limit.
+    raise_memory_limit(MEMORY_EXTRA);
+
+    // Emulate normal session - we use admin account by default.
+    cron_setup_user();
+
+    // Execute the task.
+    \core\local\cli\shutdown::script_supports_graceful_exit();
+    $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
+    if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
+        mtrace('Cannot obtain cron lock');
+        exit(129);
+    }
+    if (!$lock = $cronlockfactory->get_lock('\\' . get_class($task), 10)) {
+        $cronlock->release();
+        mtrace('Cannot obtain task lock');
+        exit(130);
+    }
+
+    $task->set_lock($lock);
+    if (!$task->is_blocking()) {
+        $cronlock->release();
+    } else {
+        $task->set_cron_lock($cronlock);
+    }
+
+    cron_run_inner_scheduled_task($task);
+}
index 5931a26..45f46a5 100644 (file)
@@ -361,14 +361,15 @@ class core_admin_renderer extends plugin_renderer_base {
         $output = '';
 
         if ($checker->enabled()) {
-            $output .= $this->container_start('checkforupdates');
+            $output .= $this->container_start('checkforupdates mb-4');
             $output .= $this->single_button(
                 new moodle_url($reloadurl, array('fetchupdates' => 1)),
                 get_string('checkforupdates', 'core_plugin')
             );
             if ($timefetched = $checker->get_last_timefetched()) {
                 $timefetched = userdate($timefetched, get_string('strftimedatetime', 'core_langconfig'));
-                $output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched), 'lasttimefetched');
+                $output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched),
+                    'lasttimefetched small text-muted mt-1');
             }
             $output .= $this->container_end();
         }
@@ -1045,7 +1046,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 $displayname = new html_table_cell(
                     $icon.
                     html_writer::span($plugin->displayname, 'pluginname').
-                    html_writer::div($plugin->get_dir(), 'plugindir')
+                    html_writer::div($plugin->get_dir(), 'plugindir text-muted small')
                 );
 
                 $versiondb = new html_table_cell($plugin->versiondb);
@@ -1082,7 +1083,7 @@ class core_admin_renderer extends plugin_renderer_base {
                         break;
                     case core_plugin_manager::PLUGIN_STATUS_NODB:
                     case core_plugin_manager::PLUGIN_STATUS_UPTODATE:
-                        $statusclass .= $dependenciesok ? '' : 'badge-warning';
+                        $statusclass .= $dependenciesok ? 'badge-light' : 'badge-warning';
                         break;
                 }
                 $status = html_writer::span(get_string('status_' . $statuscode, 'core_plugin'), $statusclass);
@@ -1092,7 +1093,7 @@ class core_admin_renderer extends plugin_renderer_base {
                         new moodle_url($this->page->url, array('abortinstall' => $plugin->component)),
                         get_string('cancelinstallone', 'core_plugin'),
                         'post',
-                        array('class' => 'actionbutton cancelinstallone')
+                        array('class' => 'actionbutton cancelinstallone d-block mt-1')
                     );
                 }
 
@@ -1101,7 +1102,7 @@ class core_admin_renderer extends plugin_renderer_base {
                         new moodle_url($this->page->url, array('abortupgrade' => $plugin->component)),
                         get_string('cancelupgradeone', 'core_plugin'),
                         'post',
-                        array('class' => 'actionbutton cancelupgradeone')
+                        array('class' => 'actionbutton cancelupgradeone d-block mt-1')
                     );
                 }
 
@@ -1174,7 +1175,7 @@ class core_admin_renderer extends plugin_renderer_base {
             }
         }
 
-        $out .= $this->output->container_start('actions');
+        $out .= $this->output->container_start('actions mb-2');
 
         $installableupdates = $pluginman->filter_installable($pluginman->available_updates());
         if ($installableupdates) {
@@ -1182,7 +1183,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 new moodle_url($this->page->url, array('installupdatex' => 1)),
                 get_string('updateavailableinstallall', 'core_admin', count($installableupdates)),
                 'post',
-                array('class' => 'singlebutton updateavailableinstallall')
+                array('class' => 'singlebutton updateavailableinstallall mr-1')
             );
         }
 
@@ -1191,7 +1192,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 new moodle_url($this->page->url, array('abortinstallx' => 1)),
                 get_string('cancelinstallall', 'core_plugin', count($installabortable)),
                 'post',
-                array('class' => 'singlebutton cancelinstallall')
+                array('class' => 'singlebutton cancelinstallall mr-1')
             );
         }
 
@@ -1200,15 +1201,17 @@ class core_admin_renderer extends plugin_renderer_base {
                 new moodle_url($this->page->url, array('abortupgradex' => 1)),
                 get_string('cancelupgradeall', 'core_plugin', count($upgradeabortable)),
                 'post',
-                array('class' => 'singlebutton cancelupgradeall')
+                array('class' => 'singlebutton cancelupgradeall mr-1')
             );
         }
 
         $out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 0)),
-            get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge'));
+            get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge badge-light'),
+            'btn btn-link mr-1');
 
         $out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 1)),
-            get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge'));
+            get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge badge-light'),
+            'btn btn-link mr-1');
 
         $out .= $this->output->container_end(); // End of .actions container.
         $out .= $this->output->container_end(); // End of #plugins-check-info container.
@@ -1276,12 +1279,13 @@ class core_admin_renderer extends plugin_renderer_base {
             }
         }
 
-        $out  = $this->output->container_start('plugins-check-dependencies');
+        $out  = $this->output->container_start('plugins-check-dependencies mb-4');
 
         if ($unavailable or $unknown) {
             $out .= $this->output->heading(get_string('misdepsunavail', 'core_plugin'));
             if ($unknown) {
-                $out .= $this->output->notification(get_string('misdepsunknownlist', 'core_plugin', implode(', ', $unknown)));
+                $out .= $this->output->render((new \core\output\notification(get_string('misdepsunknownlist', 'core_plugin',
+                    implode(', ', $unknown))))->set_show_closebutton(false));
             }
             if ($unavailable) {
                 $unavailablelist = array();
@@ -1295,18 +1299,18 @@ class core_admin_renderer extends plugin_renderer_base {
                     }
                     $unavailablelist[] = $unavailablelistitem;
                 }
-                $out .= $this->output->notification(get_string('misdepsunavaillist', 'core_plugin',
-                    implode(', ', $unavailablelist)));
+                $out .= $this->output->render((new \core\output\notification(get_string('misdepsunavaillist', 'core_plugin',
+                    implode(', ', $unavailablelist))))->set_show_closebutton(false));
             }
-            $out .= $this->output->container_start('plugins-check-dependencies-actions');
+            $out .= $this->output->container_start('plugins-check-dependencies-actions mb-4');
             $out .= ' '.html_writer::link(new moodle_url('/admin/tool/installaddon/'),
-                get_string('dependencyuploadmissing', 'core_plugin'));
+                get_string('dependencyuploadmissing', 'core_plugin'), array('class' => 'btn btn-secondary'));
             $out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
         }
 
         if ($available) {
             $out .= $this->output->heading(get_string('misdepsavail', 'core_plugin'));
-            $out .= $this->output->container_start('plugins-check-dependencies-actions');
+            $out .= $this->output->container_start('plugins-check-dependencies-actions mb-2');
 
             $installable = $pluginman->filter_installable($available);
             if ($installable) {
@@ -1314,12 +1318,13 @@ class core_admin_renderer extends plugin_renderer_base {
                     new moodle_url($this->page->url, array('installdepx' => 1)),
                     get_string('dependencyinstallmissing', 'core_plugin', count($installable)),
                     'post',
-                    array('class' => 'singlebutton dependencyinstallmissing')
+                    array('class' => 'singlebutton dependencyinstallmissing d-inline-block mr-1')
                 );
             }
 
             $out .= html_writer::div(html_writer::link(new moodle_url('/admin/tool/installaddon/'),
-                get_string('dependencyuploadmissing', 'core_plugin')), 'dependencyuploadmissing');
+                get_string('dependencyuploadmissing', 'core_plugin'), array('class' => 'btn btn-link')),
+                'dependencyuploadmissing d-inline-block mr-1');
 
             $out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
 
@@ -1360,7 +1365,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 if ($CFG->branch == str_replace('.', '', $moodle->release)) {
                     $supportedmoodles[] = html_writer::span($moodle->release, 'badge badge-success');
                 } else {
-                    $supportedmoodles[] = html_writer::span($moodle->release, 'label');
+                    $supportedmoodles[] = html_writer::span($moodle->release, 'badge badge-light');
                 }
             }
 
@@ -1374,7 +1379,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 }
                 $info = html_writer::div(
                     get_string('requiredby', 'core_plugin', implode(', ', $requriedby)),
-                    'requiredby'
+                    'requiredby mb-1'
                 );
             } else {
                 $info = '';
@@ -1385,35 +1390,36 @@ class core_admin_renderer extends plugin_renderer_base {
             $info .= html_writer::div(
                 html_writer::link('https://moodle.org/plugins/view.php?plugin='.$plugin->component,
                     get_string('misdepinfoplugin', 'core_plugin')),
-                'misdepinfoplugin'
+                'misdepinfoplugin d-inline-block mr-3 mb-1'
             );
 
             $info .= html_writer::div(
                 html_writer::link('https://moodle.org/plugins/pluginversion.php?id='.$plugin->version->id,
                     get_string('misdepinfoversion', 'core_plugin')),
-                'misdepinfoversion'
+                'misdepinfoversion d-inline-block mr-3 mb-1'
             );
 
-            $info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')), 'misdepdownload');
+            $info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')),
+                'misdepdownload d-inline-block mr-3 mb-1');
 
             if ($pluginman->is_remote_plugin_installable($plugin->component, $plugin->version->version, $reason)) {
                 $info .= $this->output->single_button(
                     new moodle_url($this->page->url, array('installdep' => $plugin->component)),
                     get_string('dependencyinstall', 'core_plugin'),
                     'post',
-                    array('class' => 'singlebutton dependencyinstall')
+                    array('class' => 'singlebutton dependencyinstall mr-3 mb-1')
                 );
             } else {
                 $reasonhelp = $this->info_remote_plugin_not_installable($reason);
                 if ($reasonhelp) {
-                    $info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall');
+                    $info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall d-inline-block mr-3 mb-1');
                 }
             }
 
             $info .= $this->output->container_end(); // End of .actions container.
 
             $table->data[] = array(
-                html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component'),
+                html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component text-muted small'),
                 $plugin->version->release,
                 $plugin->version->version,
                 implode(' ', $supportedmoodles),
@@ -1461,7 +1467,7 @@ class core_admin_renderer extends plugin_renderer_base {
         foreach ($requirements as $reqname => $reqinfo) {
             if ($reqname === 'core') {
                 if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
-                    $class = 'requires-ok';
+                    $class = 'requires-ok text-muted';
                     $label = '';
                 } else {
                     $class = 'requires-failed';
@@ -1490,7 +1496,7 @@ class core_admin_renderer extends plugin_renderer_base {
 
                 if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
                     $label = '';
-                    $class = 'requires-ok';
+                    $class = 'requires-ok text-muted';
 
                 } else if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_MISSING) {
                     if ($reqinfo->availability == $pluginman::REQUIREMENT_AVAILABLE) {
@@ -1544,13 +1550,14 @@ class core_admin_renderer extends plugin_renderer_base {
             return '';
         }
 
-        $out = html_writer::tag('ul', implode("\n", $requires));
+        $out = html_writer::tag('ul', implode("\n", $requires), array('class' => 'm-0'));
 
         if ($displayuploadlink) {
             $out .= html_writer::div(
                 html_writer::link(
                     new moodle_url('/admin/tool/installaddon/'),
-                    get_string('dependencyuploadmissing', 'core_plugin')
+                    get_string('dependencyuploadmissing', 'core_plugin'),
+                    array('class' => 'btn btn-secondary btn-sm m-1')
                 ),
                 'dependencyuploadmissing'
             );
@@ -1560,7 +1567,8 @@ class core_admin_renderer extends plugin_renderer_base {
             $out .= html_writer::div(
                 html_writer::link(
                     new moodle_url($this->page->url, array('sesskey' => sesskey(), 'fetchupdates' => 1)),
-                    get_string('checkforupdates', 'core_plugin')
+                    get_string('checkforupdates', 'core_plugin'),
+                    array('class' => 'btn btn-secondary btn-sm m-1')
                 ),
                 'checkforupdates'
             );
index c9d406a..4aa50b4 100644 (file)
@@ -25,6 +25,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // sp
     $temp->add(new admin_setting_configcheckbox('allowcohortthemes',  new lang_string('allowcohortthemes', 'admin'), new lang_string('configallowcohortthemes', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowthemechangeonurl',  new lang_string('allowthemechangeonurl', 'admin'), new lang_string('configallowthemechangeonurl', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowuserblockhiding', new lang_string('allowuserblockhiding', 'admin'), new lang_string('configallowuserblockhiding', 'admin'), 1));
+    $temp->add(new admin_setting_configcheckbox('langmenuinsecurelayout',
+        new lang_string('langmenuinsecurelayout', 'admin'),
+        new lang_string('langmenuinsecurelayout_desc', 'admin'), 0));
+    $temp->add(new admin_setting_configcheckbox('logininfoinsecurelayout',
+        new lang_string('logininfoinsecurelayout', 'admin'),
+        new lang_string('logininfoinsecurelayout_desc', 'admin'), 0));
     $temp->add(new admin_setting_configtextarea('custommenuitems', new lang_string('custommenuitems', 'admin'),
         new lang_string('configcustommenuitems', 'admin'), '', PARAM_RAW, '50', '10'));
     $temp->add(new admin_setting_configtextarea(
index 3b9302d..a512853 100644 (file)
@@ -33,7 +33,7 @@ $ADMIN->add('h5p', new admin_externalpage('h5pmanagelibraries', get_string('h5pm
     new moodle_url('/h5p/libraries.php'), ['moodle/site:config', 'moodle/h5p:updatelibraries']));
 
 // H5P settings.
-$defaulth5plib = \core_h5p\local\library\autoloader::get_default_handler();
+$defaulth5plib = \core_h5p\local\library\autoloader::get_default_handler_library();
 if (!empty($defaulth5plib)) {
     // As for now this page only has this setting, it will be hidden if there isn't any H5P libraries handler defined.
     $settings = new admin_settingpage('h5psettings', new lang_string('h5psettings', 'core_h5p'));
index 51e7960..c11b613 100644 (file)
@@ -115,4 +115,4 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
     And I set the field "id_department" to "red"
     And I press "Add filter"
     And I should see "User One"
-    And I should not see "User Two"
\ No newline at end of file
+    And I should not see "User Two"
index 697a214..6520ca2 100644 (file)
Binary files a/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js and b/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js differ
index b00dbac..e71b3c8 100644 (file)
Binary files a/admin/tool/lp/amd/build/menubar.min.js and b/admin/tool/lp/amd/build/menubar.min.js differ
index 209a5c7..6c52abb 100644 (file)
Binary files a/admin/tool/lp/amd/build/menubar.min.js.map and b/admin/tool/lp/amd/build/menubar.min.js.map differ
index 25b8682..2ef194d 100644 (file)
Binary files a/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-min.js and b/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-min.js differ
index c6160d3..914ef5f 100644 (file)
@@ -31,6 +31,8 @@ use moodle_url;
 use moodle_exception;
 use lang_string;
 use curl;
+use core_qrcode;
+use stdClass;
 
 /**
  * API exposed by tool_mobile, to be used mostly by external functions and the plugin settings.
@@ -49,6 +51,16 @@ 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';
+    /** @var int seconds a QR login key will expire. */
+    const LOGIN_QR_KEY_TTL = 600;
+    /** @var int QR code disabled value */
+    const QR_CODE_DISABLED = 0;
+    /** @var int QR code type URL value */
+    const QR_CODE_URL = 1;
+    /** @var int QR code type login value */
+    const QR_CODE_LOGIN = 2;
 
     /**
      * Returns a list of Moodle plugins supporting the mobile app.
@@ -334,6 +346,7 @@ class api {
 
     /**
      * Creates an auto-login key for the current user, this key is restricted by time and ip address.
+     * This key is used for automatically login the user in the site when the Moodle app opens the site in a mobile browser.
      *
      * @return string the key
      * @since Moodle 3.2
@@ -349,6 +362,24 @@ class api {
         return create_user_key('tool_mobile', $USER->id, null, $iprestriction, $validuntil);
     }
 
+    /**
+     * Creates a QR login key for the current user, this key is restricted by time and ip address.
+     * This key is used for automatically login the user in the site when the user scans a QR code in the Moodle app.
+     *
+     * @return string the key
+     * @since Moodle 3.9
+     */
+    public static function get_qrlogin_key() {
+        global $USER;
+        // Delete previous keys.
+        delete_user_key('tool_mobile', $USER->id);
+
+        // Create a new key.
+        $iprestriction = getremoteaddr(null);
+        $validuntil = time() + self::LOGIN_QR_KEY_TTL;
+        return create_user_key('tool_mobile', $USER->id, null, $iprestriction, $validuntil);
+    }
+
     /**
      * Get a list of the Mobile app features.
      *
@@ -599,4 +630,31 @@ class api {
 
         return $warnings;
     }
+
+    /**
+     * Generates a QR code with the site URL or for automatic login from the mobile app.
+     *
+     * @param  stdClass $mobilesettings tool_mobile settings
+     * @return string base64 data image contents, null if qr disabled
+     */
+    public static function generate_login_qrcode(stdClass $mobilesettings) {
+        global $CFG, $USER;
+
+        if ($mobilesettings->qrcodetype == static::QR_CODE_DISABLED) {
+            return null;
+        }
+
+        $urlscheme = !empty($mobilesettings->forcedurlscheme) ? $mobilesettings->forcedurlscheme : 'moodlemobile';
+        $data = $urlscheme . '://' . $CFG->wwwroot;
+
+        if ($mobilesettings->qrcodetype == static::QR_CODE_LOGIN) {
+            $qrloginkey = static::get_qrlogin_key();
+            $data .= '?qrlogin=' . $qrloginkey . '&userid=' . $USER->id;
+        }
+
+        $qrcode = new core_qrcode($data);
+        $imagedata = 'data:image/png;base64,' . base64_encode($qrcode->getBarcodePngData(5, 5));
+
+        return $imagedata;
+    }
 }
index fe1dad0..fa13085 100644 (file)
@@ -39,6 +39,7 @@ use context_system;
 use moodle_exception;
 use moodle_url;
 use core_text;
+use core_user;
 use coding_exception;
 
 /**
@@ -593,4 +594,102 @@ class external extends external_api {
              )
         ]);
     }
+
+    /**
+     * Returns description of get_tokens_for_qr_login() parameters.
+     *
+     * @return external_function_parameters
+     * @since  Moodle 3.9
+     */
+    public static function get_tokens_for_qr_login_parameters() {
+        return new external_function_parameters (
+            [
+                'qrloginkey' => new external_value(PARAM_ALPHANUMEXT, 'The user key for validating the request.'),
+                'userid' => new external_value(PARAM_INT, 'The user the key belongs to.'),
+            ]
+        );
+    }
+
+    /**
+     * Returns a WebService token (and private token) for QR login
+     *
+     * @param string $qrloginkey the user key generated and embedded into the QR code for validating the request
+     * @param int $userid the user the key belongs to
+     * @return array with the tokens and warnings
+     * @since  Moodle 3.9
+     */
+    public static function get_tokens_for_qr_login($qrloginkey, $userid) {
+        global $PAGE, $DB;
+
+        $params = self::validate_parameters(self::get_tokens_for_qr_login_parameters(),
+            ['qrloginkey' => $qrloginkey, 'userid' => $userid]);
+
+        $context = context_system::instance();
+        // We need this to make work the format text functions.
+        $PAGE->set_context($context);
+
+        $qrcodetype = get_config('tool_mobile', 'qrcodetype');
+        if ($qrcodetype != api::QR_CODE_LOGIN) {
+            throw new moodle_exception('qrcodedisabled', 'tool_mobile');
+        }
+
+        // Only requests from the Moodle mobile or desktop app. This enhances security to avoid any type of XSS attack.
+        // This code goes intentionally here and not inside the check_autologin_prerequisites() function because it
+        // is used by other PHP scripts that can be opened in any browser.
+        if (!\core_useragent::is_moodle_app()) {
+            throw new moodle_exception('apprequired', 'tool_mobile');
+        }
+        api::check_autologin_prerequisites($params['userid']);  // Checks https, avoid site admins using this...
+
+        // Validate and delete the key.
+        $key = validate_user_key($params['qrloginkey'], 'tool_mobile', null);
+        delete_user_key('tool_mobile', $params['userid']);
+
+        // Double check key belong to user.
+        if ($key->userid != $params['userid']) {
+            throw new moodle_exception('invalidkey');
+        }
+
+        // Key validated, check user.
+        $user = core_user::get_user($key->userid, '*', MUST_EXIST);
+        core_user::require_active_user($user, true, true);
+
+        // Generate WS tokens.
+        \core\session\manager::set_user($user);
+
+        // Check if the service exists and is enabled.
+        $service = $DB->get_record('external_services', ['shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE, 'enabled' => 1]);
+        if (empty($service)) {
+            // will throw exception if no token found
+            throw new moodle_exception('servicenotavailable', 'webservice');
+        }
+
+        // Get an existing token or create a new one.
+        $token = external_generate_token_for_current_user($service);
+        $privatetoken = $token->privatetoken; // Save it here, the next function removes it.
+        external_log_token_request($token);
+
+        $result = [
+            'token' => $token->token,
+            'privatetoken' => $privatetoken ?: '',
+            'warnings' => [],
+        ];
+        return $result;
+    }
+
+    /**
+     * Returns description of get_tokens_for_qr_login() result value.
+     *
+     * @return external_description
+     * @since  Moodle 3.9
+     */
+    public static function get_tokens_for_qr_login_returns() {
+        return new external_single_structure(
+            [
+                'token' => new external_value(PARAM_ALPHANUM, 'A valid WebService token for the official mobile app service.'),
+                'privatetoken' => new external_value(PARAM_ALPHANUM, 'Private token used for auto-login processes.'),
+                'warnings' => new external_warnings(),
+            ]
+        );
+    }
 }
index d53f7b4..530267a 100644 (file)
@@ -78,5 +78,14 @@ $functions = array(
         'type'        => 'write',
         'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
-);
 
+    'tool_mobile_get_tokens_for_qr_login' => array(
+        'classname'   => 'tool_mobile\external',
+        'methodname'  => 'get_tokens_for_qr_login',
+        'description' => 'Returns a WebService token (and private token) for QR login.',
+        'type'        => 'read',
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'ajax'          => true,
+        'loginrequired' => false,
+    ),
+);
index 092f7da..789cfd2 100644 (file)
@@ -91,10 +91,20 @@ $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';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
+$string['qrcodedisabled'] = 'Access via QR code disabled';
+$string['qrcodeformobileappaccess'] = 'QR code for mobile app access';
+$string['qrcodeformobileapploginabout'] = 'Scan the QR code with your mobile app and you will be automatically logged in. The QR code will expire in {$a} minutes.';
+$string['qrcodeformobileappurlabout'] = 'Scan the QR code with your mobile app to fill in the site URL in your app.';
+$string['qrsiteadminsnotallowed'] = 'For security reasons login via QR code is not allowed for site administrators or if you are logged in as another user.';
+$string['qrcodetype'] = 'QR code access';
+$string['qrcodetype_desc'] = 'A QR code can be provided for mobile app users to scan and either have the site URL filled in or be automatically logged in without having to enter their credentials.';
+$string['qrcodetypeurl'] = 'QR code with site URL';
+$string['qrcodetypelogin'] = 'QR code with automatic login';
 $string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
@@ -107,3 +117,4 @@ $string['getmoodleonyourmobile'] = 'Get the mobile app';
 $string['privacy:metadata:preference:tool_mobile_autologin_request_last'] = 'The date of the last auto-login key request. Between each request 6 minutes are required.';
 $string['privacy:metadata:core_userkey'] = 'User\'s keys used to create auto-login key for the current user.';
 $string['responsivemainmenuitems'] = 'Responsive menu items';
+$string['viewqrcode'] = 'View QR code';
index 74b3c0b..43d6cc2 100644 (file)
@@ -126,24 +126,64 @@ function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree
         return;
     }
 
-    if (!$url = tool_mobile_create_app_download_url()) {
-        return;
+    $newnodes = [];
+    $mobilesettings = get_config('tool_mobile');
+
+    // Check if we should display a QR code.
+    if (!empty($mobilesettings->qrcodetype)) {
+        $mobileqr = null;
+        $qrcodeforappstr = get_string('qrcodeformobileappaccess', 'tool_mobile');
+
+        if ($mobilesettings->qrcodetype == tool_mobile\api::QR_CODE_LOGIN && is_https()) {
+
+            if (is_siteadmin() || \core\session\manager::is_loggedinas()) {
+                $mobileqr = get_string('qrsiteadminsnotallowed', 'tool_mobile');
+            } else {
+                $qrcodeimg = tool_mobile\api::generate_login_qrcode($mobilesettings);
+
+                $minutes = tool_mobile\api::LOGIN_QR_KEY_TTL / MINSECS;
+                $mobileqr = html_writer::tag('p', get_string('qrcodeformobileapploginabout', 'tool_mobile', $minutes));
+                $mobileqr .= html_writer::link('#qrcode', get_string('viewqrcode', 'tool_mobile'),
+                    ['class' => 'btn btn-primary mt-2', 'data-toggle' => 'collapse',
+                    'role' => 'button', 'aria-expanded' => 'false']);
+                $mobileqr .= html_writer::div(html_writer::img($qrcodeimg, $qrcodeforappstr), 'collapse mt-4', ['id' => 'qrcode']);
+            }
+
+        } else if ($mobilesettings->qrcodetype == tool_mobile\api::QR_CODE_URL) {
+            $qrcodeimg = tool_mobile\api::generate_login_qrcode($mobilesettings);
+
+            $mobileqr = get_string('qrcodeformobileappurlabout', 'tool_mobile');
+            $mobileqr .= html_writer::div(html_writer::img($qrcodeimg, $qrcodeforappstr));
+        }
+
+        if ($mobileqr) {
+            $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappqr', $qrcodeforappstr, null, null, $mobileqr);
+        }
     }
 
+    // Check if the user is using the app, encouraging him to use it otherwise.
     $userhastoken = tool_mobile_user_has_token($user->id);
-
-    $mobilecategory = new core_user\output\myprofile\category('mobile', get_string('mobileapp', 'tool_mobile'),
-            'loginactivity');
-    $tree->add_category($mobilecategory);
+    $mobilestrconnected = null;
 
     if ($userhastoken) {
-        $mobilestr = get_string('mobileappconnected', 'tool_mobile');
-    } else {
-        $mobilestr = get_string('mobileappenabled', 'tool_mobile', $url->out());
+        $mobilestrconnected = get_string('mobileappconnected', 'tool_mobile');
+    } else if ($url = tool_mobile_create_app_download_url()) {
+         $mobilestrconnected = get_string('mobileappenabled', 'tool_mobile', $url->out());
     }
 
-    $node = new  core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestr, null);
-    $tree->add_node($node);
+    if ($mobilestrconnected) {
+        $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestrconnected, null);
+    }
+
+    // Add nodes, if any.
+    if (!empty($newnodes)) {
+        $mobilecat = new core_user\output\myprofile\category('mobile', get_string('mobileapp', 'tool_mobile'), 'loginactivity');
+        $tree->add_category($mobilecat);
+
+        foreach ($newnodes as $node) {
+            $tree->add_node($node);
+        }
+    }
 }
 
 /**
index a9cdbcf..732bd71 100644 (file)
@@ -50,8 +50,17 @@ 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'));
+
+        $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesauth', '', $featuresnotice));
+
         $options = array(
             tool_mobile\api::LOGIN_VIA_APP => new lang_string('loginintheapp', 'tool_mobile'),
             tool_mobile\api::LOGIN_VIA_BROWSER => new lang_string('logininthebrowser', 'tool_mobile'),
@@ -61,6 +70,15 @@ if ($hassiteconfig) {
                     new lang_string('typeoflogin', 'tool_mobile'),
                     new lang_string('typeoflogin_desc', 'tool_mobile'), 1, $options));
 
+        $options = [
+            tool_mobile\api::QR_CODE_DISABLED => new lang_string('qrcodedisabled', 'tool_mobile'),
+            tool_mobile\api::QR_CODE_URL => new lang_string('qrcodetypeurl', 'tool_mobile'),
+            tool_mobile\api::QR_CODE_LOGIN => new lang_string('qrcodetypelogin', 'tool_mobile'),
+        ];
+        $temp->add(new admin_setting_configselect('tool_mobile/qrcodetype',
+                    new lang_string('qrcodetype', 'tool_mobile'),
+                    new lang_string('qrcodetype_desc', 'tool_mobile'), tool_mobile\api::QR_CODE_LOGIN, $options));
+
         $temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
                     new lang_string('forcedurlscheme_key', 'tool_mobile'),
                     new lang_string('forcedurlscheme', 'tool_mobile'), 'moodlemobile', PARAM_NOTAGS));
@@ -74,6 +92,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 +126,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 9534f05..7b05175 100644 (file)
@@ -600,4 +600,129 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => true]);
         $this->assertEquals($expected, $data->courses[0]->summary);
     }
+
+    /*
+     * Test get_tokens_for_qr_login.
+     */
+    public function test_get_tokens_for_qr_login() {
+        global $DB, $CFG, $USER;
+
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $qrloginkey = api::get_qrlogin_key();
+
+        // Generate new tokens, the ones we expect to receive.
+        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
+        $token = external_generate_token_for_current_user($service);
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+                'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        $result = external::get_tokens_for_qr_login($qrloginkey, $USER->id);
+        $result = external_api::clean_returnvalue(external::get_tokens_for_qr_login_returns(), $result);
+
+        $this->assertEmpty($result['warnings']);
+        $this->assertEquals($token->token, $result['token']);
+        $this->assertEquals($token->privatetoken, $result['privatetoken']);
+
+        // Now, try with an invalid key.
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('invalidkey', 'error'));
+        $result = external::get_tokens_for_qr_login(random_string('64'), $user->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing QR code enabled.
+     */
+    public function test_get_tokens_for_qr_login_missing_enableqr() {
+        global $CFG, $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        set_config('qrcodetype', tool_mobile\api::QR_CODE_DISABLED, 'tool_mobile');
+
+        $this->expectExceptionMessage(get_string('qrcodedisabled', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing ws.
+     */
+    public function test_get_tokens_for_qr_login_missing_ws() {
+        global $CFG;
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        // Need to disable webservices to verify that's checked.
+        $CFG->enablewebservices = 0;
+        $CFG->enablemobilewebservice = 0;
+
+        $this->setAdminUser();
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
+        $result = external::get_tokens_for_qr_login('', $user->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing https.
+     */
+    public function test_get_tokens_for_qr_login_missing_https() {
+        global $CFG, $USER;
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        // Need to simulate a non HTTPS site here.
+        $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing admin.
+     */
+    public function test_get_tokens_for_qr_login_missing_admin() {
+        global $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing app_request.
+     */
+    public function test_get_tokens_for_qr_login_missing_app_request() {
+        global $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('apprequired', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
 }
index 392d5b2..afba9ff 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2019111800; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019111801; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019111200; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
 $plugin->dependencies = array(
index bf74086..2e8537e 100644 (file)
Binary files a/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-min.js and b/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-min.js differ
index 52bf795..da4ef21 100644 (file)
@@ -26,7 +26,7 @@
     font-weight: 100;
     line-height: 1;
     color: #a2a2a2;
-    filter: alpha(opacity=20);
+    opacity: 0.2;
     position: absolute;
     font-family: helvetica, arial, verdana, sans-serif;
     top: 0;
index 06e24ef..9b91a14 100644 (file)
@@ -17,6 +17,9 @@
 /**
  * Form for scheduled tasks admin pages.
  *
+ * @deprecated since Moodle 3.9 MDL-63580. Please use the \core\task\manager.
+ * @todo final deprecation. To be removed in Moodle 4.3 MDL-63594.
+ *
  * @package    tool_task
  * @copyright  2018 Toni Barbera <toni@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -55,7 +58,9 @@ class run_from_cli {
      * @return bool
      */
     public static function is_runnable():bool {
-        return self::find_php_cli_path() !== false;
+        debugging('run_from_cli class is deprecated. Please use \core\task\manager::run_from_cli() instead.',
+            DEBUG_DEVELOPER);
+        return \core\task\manager::is_runnable();
     }
 
     /**
@@ -66,30 +71,8 @@ class run_from_cli {
      * @throws \moodle_exception
      */
     public static function execute(\core\task\task_base $task):bool {
-        global $CFG;
-
-        if (!self::is_runnable()) {
-            $redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
-            throw new \moodle_exception('cannotfindthepathtothecli', 'tool_task', $redirecturl->out());
-        } else {
-            // Shell-escaped path to the PHP binary.
-            $phpbinary = escapeshellarg(self::find_php_cli_path());
-
-            // Shell-escaped path CLI script.
-            $pathcomponents = [$CFG->dirroot, $CFG->admin, 'tool', 'task', 'cli', 'schedule_task.php'];
-            $scriptpath     = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents));
-
-            // Shell-escaped task name.
-            $classname = get_class($task);
-            $taskarg   = escapeshellarg("--execute={$classname}");
-
-            // Build the CLI command.
-            $command = "{$phpbinary} {$scriptpath} {$taskarg}";
-
-            // Execute it.
-            passthru($command);
-        }
-
-        return true;
+        debugging('run_from_cli class is deprecated. Please use \core\task\manager::run_from_cli() instead.',
+            DEBUG_DEVELOPER);
+        return \core\task\manager::run_from_cli($task);
     }
 }
index 1a04ba8..07cded8 100644 (file)
@@ -17,6 +17,9 @@
 /**
  * Task executor for adhoc tasks.
  *
+ * @deprecated since Moodle 3.9 MDL-63580. Please use the admin/cli/adhoc_task.php.
+ * @todo final deprecation. To be removed in Moodle 4.3 MDL-63594.
+ *
  * @package    tool_task
  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -44,6 +47,8 @@ list($options, $unrecognized) = cli_get_params(
     ]
 );
 
+debugging('admin/tool/task/cli/adhoc_task.php is deprecated. Please use admin/cli/adhoc_task.php instead.', DEBUG_DEVELOPER);
+
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
     cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
index 85d7a4f..073075e 100644 (file)
@@ -17,6 +17,9 @@
 /**
  * CLI task execution.
  *
+ * @deprecated since Moodle 3.9 MDL-63580. Please use the admin/cli/schedule_task.php.
+ * @todo final deprecation. To be removed in Moodle 4.3 MDL-63594.
+ *
  * @package    tool_task
  * @copyright  2014 Petr Skoda
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -33,6 +36,8 @@ list($options, $unrecognized) = cli_get_params(
     array('h' => 'help')
 );
 
+debugging('admin/tool/task/cli/schedule_task.php is deprecated. Please use admin/cli/scheduled_task.php instead.', DEBUG_DEVELOPER);
+
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
     cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
@@ -65,6 +70,7 @@ if ($options['showdebugging']) {
 if ($options['showsql']) {
     $DB->set_debug(true);
 }
+
 if ($options['list']) {
     cli_heading("List of scheduled tasks ($CFG->wwwroot)");
 
index 0a01d0a..a003eab 100644 (file)
@@ -75,7 +75,7 @@ class tool_task_renderer extends plugin_renderer_base {
         $data = [];
         $yes = get_string('yes');
         $no = get_string('no');
-        $canruntasks = tool_task\run_from_cli::is_runnable();
+        $canruntasks = \core\task\manager::is_runnable();
         foreach ($tasks as $task) {
             $classname = get_class($task);
             $defaulttask = \core\task\manager::get_default_scheduled_task($classname, false);
index b404a82..1b0de98 100644 (file)
@@ -88,7 +88,7 @@ echo html_writer::start_tag('pre');
 $CFG->mtrace_wrapper = 'tool_task_mtrace_wrapper';
 
 // Run the specified task (this will output an error if it doesn't exist).
-\tool_task\run_from_cli::execute($task);
+\core\task\manager::run_from_cli($task);
 
 echo html_writer::end_tag('pre');
 
index a17fc2f..75ae6cd 100644 (file)
@@ -104,4 +104,4 @@ Feature: An admin can create courses using a CSV file
     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
+    And I should see "Field 5: Some text"
index dbca1f4..3d86dc6 100644 (file)
@@ -56,4 +56,4 @@ Feature: An admin can update courses using a CSV file
     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
+    And I should see "Field 5: Goodbye"
index fc55002..d21da4e 100644 (file)
@@ -187,4 +187,4 @@ Feature: Upload users
     Then I should see "2 January 2020" in the "Enrolment starts" "table_row"
     And I should see "12 January 2020" in the "Enrolment ends" "table_row"
     And I click on "Close" "button"
-    And I log out
\ No newline at end of file
+    And I log out
index 72237ea..5be936a 100644 (file)
Binary files a/admin/tool/usertours/amd/build/tour.min.js and b/admin/tool/usertours/amd/build/tour.min.js differ
index 125bbab..bc48f5c 100644 (file)
Binary files a/admin/tool/usertours/amd/build/tour.min.js.map and b/admin/tool/usertours/amd/build/tour.min.js.map differ
index 9e6bd0c..58f023e 100644 (file)
@@ -68,13 +68,13 @@ class view_xml extends XMLDBAction {
 
         // Get the file parameter
         $file = required_param('file', PARAM_PATH);
-        $file = $CFG->dirroot . $file;
-        // File must be under $CFG->wwwroot and
-        // under one db directory (simple protection)
-        if (substr($file, 0, strlen($CFG->dirroot)) == $CFG->dirroot &&
-            substr(dirname($file), -2, 2) == 'db') {
+
+        $fullpath = $CFG->dirroot . $file;
+        // File param must start with / and end with /db/install.xml to be safe.
+        if (substr($file, 0, 1) == '/' &&
+            substr($file, -15, 15) == '/db/install.xml') {
             // Everything is ok. Load the file to memory
-            $this->output = file_get_contents($file);
+            $this->output = file_get_contents($fullpath);
         } else {
             // Switch to HTML and error
             $this->does_generate = ACTION_GENERATE_HTML;
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';
index 10be26c..67acacb 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js differ
index 6a9564d..42a4d65 100644 (file)
Binary files a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js and b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js differ
index 50c96e9..9f523aa 100644 (file)
Binary files a/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js and b/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js differ
index 52658fe..bc86932 100644 (file)
Binary files a/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js and b/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js differ
index ebd80d8..4430bd6 100644 (file)
Binary files a/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js and b/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js differ
index f870dda..945137a 100644 (file)
Binary files a/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js and b/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js differ
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 6411461..dd09dd5 100644 (file)
Binary files a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js and b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js differ
index d4cf469..41ad459 100644 (file)
@@ -122,4 +122,4 @@ Feature: Restore Moodle 2 course backups with different user data settings
       | Settings |  Include enrolled users | 0 |
     Then I should see "Test database name"
     When I follow "Test database name"
-    Then I should not see "Student entry"
\ No newline at end of file
+    Then I should not see "Student entry"
index 129195e..db4d0c0 100644 (file)
Binary files a/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-min.js and b/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-min.js differ
index 20654af..74d17a3 100644 (file)
Binary files a/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-min.js and b/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-min.js differ
index 67c5b6e..76b29b6 100644 (file)
@@ -126,4 +126,4 @@ Feature: Award badges with separate groups
     And I follow "Course Badge"
     And I press "Award badge"
     # Teacher 2 shouldn't be able to go further
-    Then I should see "Sorry, but you need to be part of a group to see this page."
\ No newline at end of file
+    Then I should see "Sorry, but you need to be part of a group to see this page."