Merge branch 'MDL-70769-master' of https://github.com/sharidas/moodle
authorVíctor Déniz <victor@moodle.com>
Thu, 8 Apr 2021 23:54:18 +0000 (00:54 +0100)
committerVíctor Déniz <victor@moodle.com>
Thu, 8 Apr 2021 23:54:18 +0000 (00:54 +0100)
699 files changed:
.eslintignore
.eslintrc
.gitignore
.grunt/babel-plugin-add-module-to-define.js [moved from babel-plugin-add-module-to-define.js with 98% similarity]
.grunt/components.js [moved from GruntfileComponents.js with 81% similarity]
.grunt/tasks/eslint.js [new file with mode: 0644]
.grunt/tasks/gherkinlint.js [new file with mode: 0644]
.grunt/tasks/ignorefiles.js [new file with mode: 0644]
.grunt/tasks/javascript.js [new file with mode: 0644]
.grunt/tasks/sass.js [new file with mode: 0644]
.grunt/tasks/shifter.js [new file with mode: 0644]
.grunt/tasks/startup.js [new file with mode: 0644]
.grunt/tasks/style.js [new file with mode: 0644]
.grunt/tasks/stylelint.js [new file with mode: 0644]
.grunt/tasks/watch.js [new file with mode: 0644]
Gruntfile.js
TRADEMARK.txt
admin/classes/task_log_table.php
admin/roles/assign.php
admin/settings/courses.php
admin/settings/plugins.php
admin/settings/server.php
admin/tests/behat/browse_users.feature [new file with mode: 0644]
admin/tests/behat/manage_tokens.feature
admin/tests/behat/webservice_users.feature [new file with mode: 0644]
admin/tool/behat/tests/behat/datetime_strings.feature
admin/tool/behat/tests/manager_util_test.php
admin/tool/cohortroles/classes/output/cohort_role_assignments_table.php
admin/tool/cohortroles/index.php
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/createdatarequest_form.php
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/coverage.php
admin/tool/dataprivacy/tests/expired_contexts_test.php
admin/tool/dataprivacy/tests/filtered_userlist_test.php
admin/tool/health/tests/healthlib_test.php
admin/tool/httpsreplace/tests/httpsreplace_test.php
admin/tool/langimport/tests/locale_test.php
admin/tool/lp/classes/external.php
admin/tool/lp/classes/output/template_plans_table.php
admin/tool/lpmigrate/tests/processor_test.php
admin/tool/monitor/tests/eventobservers_test.php
admin/tool/monitor/tests/subscription_test.php
admin/tool/oauth2/classes/form/issuer.php
admin/tool/oauth2/issuers.php
admin/tool/oauth2/lang/en/deprecated.txt [new file with mode: 0644]
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/oauth2/tests/behat/basic_settings.feature [new file with mode: 0644]
admin/tool/policy/classes/acceptances_table.php
admin/tool/policy/classes/api.php
admin/tool/policy/classes/form/accept_policy.php
admin/tool/uploaduser/classes/process.php
admin/tool/usertours/tests/step_test.php
admin/tool/usertours/tests/tour_test.php
admin/upgrade.txt
admin/user.php
admin/user/user_bulk_cohortadd.php
admin/user/user_bulk_display.php
admin/webservice/forms.php
admin/webservice/service_users.php
admin/webservice/tokens.php
analytics/tests/indicator_test.php
analytics/tests/prediction_test.php
auth/cas/CAS/moodle_readme.txt
auth/cas/thirdpartylibs.xml
availability/classes/info.php
availability/condition/completion/tests/condition_test.php
availability/condition/date/tests/condition_test.php
availability/condition/grade/tests/condition_test.php
availability/condition/group/tests/condition_test.php
availability/condition/grouping/tests/condition_test.php
availability/condition/profile/classes/condition.php
availability/condition/profile/classes/frontend.php
availability/condition/profile/tests/condition_test.php
availability/tests/info_test.php
availability/tests/tree_test.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_course_format_test.php
backup/moodle2/tests/restore_gradebook_structure_step_test.php
backup/tests/backup_cleanup_task_test.php [new file with mode: 0644]
backup/util/helper/backup_helper.class.php
backup/util/helper/tests/async_helper_test.php
backup/util/loggers/file_logger.class.php
backup/util/settings/tests/settings_test.php
backup/util/ui/backup_ui_stage.class.php
badges/assertion.php
badges/backpack.js [deleted file]
badges/badge.php
badges/classes/assertion.php
badges/classes/form/external_backpack.php
badges/classes/output/external_badge.php
badges/classes/output/issued_badge.php
badges/criteria/award_criteria_profile.php
badges/index.php
badges/mybadges.php
badges/newbadge.php
badges/recipients.php
badges/renderer.php
badges/tests/badgeslib_test.php
badges/tests/events_test.php
badges/upgrade.txt
blocks/activity_results/block_activity_results.php
blocks/mentees/block_mentees.php
blocks/online_users/classes/fetcher.php
blocks/rss_client/tests/cron_test.php
blog/locallib.php
blog/rsslib.php
blog/tests/privacy_test.php
cache/stores/file/lib.php
cache/tests/cache_test.php
calendar/classes/privacy/provider.php
calendar/lib.php
calendar/renderer.php
calendar/tests/calendar_event_exporter_test.php
calendar/tests/event_vault_test.php
calendar/tests/externallib_test.php
calendar/tests/local_api_test.php
calendar/tests/privacy_test.php
calendar/tests/rrule_manager_test.php
cohort/tests/privacy_test.php
comment/lib.php
comment/locallib.php
competency/tests/api_test.php
competency/tests/event_test.php
competency/tests/external_test.php
completion/classes/cm_completion_details.php [new file with mode: 0644]
completion/tests/behat/custom_completion_display_conditions.feature [new file with mode: 0644]
completion/tests/cm_completion_details_test.php [new file with mode: 0644]
composer.json
composer.lock
contentbank/tests/contentbank_test.php
contentbank/tests/privacy_test.php
course/amd/build/manual_completion_toggle.min.js [new file with mode: 0644]
course/amd/build/manual_completion_toggle.min.js.map [new file with mode: 0644]
course/amd/build/repository.min.js
course/amd/build/repository.min.js.map
course/amd/src/manual_completion_toggle.js [new file with mode: 0644]
course/amd/src/repository.js
course/classes/cache/course_image.php [new file with mode: 0644]
course/classes/category.php
course/classes/course_format.php [new file with mode: 0644]
course/classes/external/course_summary_exporter.php
course/classes/output/activity_information.php [new file with mode: 0644]
course/classes/output/cm_format.php [new file with mode: 0644]
course/classes/output/course_format.php [new file with mode: 0644]
course/classes/output/course_format/addsection.php [new file with mode: 0644]
course/classes/output/course_format/frontpagesection.php [new file with mode: 0644]
course/classes/output/course_format/sectionnavigation.php [new file with mode: 0644]
course/classes/output/course_format/sectionselector.php [new file with mode: 0644]
course/classes/output/section_format.php [new file with mode: 0644]
course/classes/output/section_format/availability.php [new file with mode: 0644]
course/classes/output/section_format/cmitem.php [new file with mode: 0644]
course/classes/output/section_format/cmlist.php [new file with mode: 0644]
course/classes/output/section_format/cmsummary.php [new file with mode: 0644]
course/classes/output/section_format/controlmenu.php [new file with mode: 0644]
course/classes/output/section_format/header.php [new file with mode: 0644]
course/classes/output/section_format/summary.php [new file with mode: 0644]
course/dnduploadlib.php
course/edit_form.php
course/externallib.php
course/format/README.txt
course/format/formatlegacy.php
course/format/lib.php
course/format/renderer.php
course/format/singleactivity/lib.php
course/format/singleactivity/renderer.php
course/format/social/lib.php
course/format/topics/classes/output/section_format/controlmenu.php [new file with mode: 0644]
course/format/topics/format.php
course/format/topics/lib.php
course/format/topics/renderer.php
course/format/weeks/format.php
course/format/weeks/lib.php
course/format/weeks/renderer.php
course/lib.php
course/modlib.php
course/recent_form.php
course/renderer.php
course/templates/activity_date.mustache [new file with mode: 0644]
course/templates/activity_info.mustache [new file with mode: 0644]
course/templates/completion_automatic.mustache [new file with mode: 0644]
course/templates/completion_manual.mustache [new file with mode: 0644]
course/templates/local/cm_format.mustache [new file with mode: 0644]
course/templates/local/course_format.mustache [new file with mode: 0644]
course/templates/local/course_format/addsection.mustache [new file with mode: 0644]
course/templates/local/course_format/frontpagesection.mustache [new file with mode: 0644]
course/templates/local/course_format/sectionnavigation.mustache [new file with mode: 0644]
course/templates/local/course_format/sectionselector.mustache [new file with mode: 0644]
course/templates/local/section_format.mustache [new file with mode: 0644]
course/templates/local/section_format/availability.mustache [new file with mode: 0644]
course/templates/local/section_format/cmitem.mustache [new file with mode: 0644]
course/templates/local/section_format/cmlist.mustache [new file with mode: 0644]
course/templates/local/section_format/cmsummary.mustache [new file with mode: 0644]
course/templates/local/section_format/controlmenu.mustache [new file with mode: 0644]
course/templates/local/section_format/header.mustache [new file with mode: 0644]
course/templates/local/section_format/summary.mustache [new file with mode: 0644]
course/tests/behat/course_activity_dates.feature [new file with mode: 0644]
course/tests/behat/paged_course_navigation.feature
course/tests/category_hooks_test.php
course/tests/course_image_cache_test.php [new file with mode: 0644]
course/tests/course_summary_exporter_test.php [new file with mode: 0644]
course/tests/courseformat_test.php
course/tests/courselib_test.php
course/tests/fixtures/format_theunittest.php
course/tests/fixtures/image.jpg [new file with mode: 0644]
course/upgrade.txt
customfield/amd/build/form.min.js
customfield/amd/build/form.min.js.map
customfield/amd/src/form.js
customfield/templates/list.mustache
customfield/templates/nofields.mustache [new file with mode: 0644]
enrol/ajax.php
enrol/externallib.php
enrol/flatfile/tests/privacy_provider_test.php
enrol/locallib.php
enrol/manual/classes/enrol_users_form.php
enrol/otherusers.php
enrol/paypal/tests/privacy_provider_test.php
enrol/self/lib.php
enrol/self/locallib.php
favourites/tests/component_favourite_service_test.php
favourites/tests/user_favourite_service_test.php
files/converter/unoconv/tests/converter_test.php
files/tests/converter_test.php
files/tests/privacy_test.php
filter/data/tests/filter_test.php
filter/displayh5p/tests/filter_test.php
grade/edit/tree/lib.php
grade/grading/form/guide/tests/coverage.php
grade/grading/form/rubric/tests/coverage.php
grade/grading/lib.php
grade/grading/tests/coverage.php
grade/grading/tests/privacy_test.php
grade/report/grader/ajax_callbacks.php
grade/report/grader/lib.php
grade/report/history/classes/helper.php
grade/report/history/classes/output/tablelog.php
grade/report/history/users_ajax.php
grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-debug.js
grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-min.js
grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector.js
grade/report/history/yui/src/userselector/js/userselector.js
grade/tests/coverage.php
grade/tests/reportlib_test.php
group/autogroup.php
group/index.php
group/lib.php
group/overview.php
group/tests/privacy_provider_test.php
h5p/tests/coverage.php
install/lang/hat/admin.php
install/lang/he/langconfig.php
install/lang/it/install.php
install/lang/mk/admin.php
install/lang/sk/install.php
install/lang/sr_cr/install.php
install/lang/sr_lt/install.php
install/lang/sv/error.php
install/lang/sv/install.php
install/lang/sv/langconfig.php
lang/en/admin.php
lang/en/badges.php
lang/en/cache.php
lang/en/completion.php
lang/en/course.php
lang/en/deprecated.txt
lang/en/moodle.php
lang/en/webservice.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/modal.min.js
lib/amd/build/modal.min.js.map
lib/amd/build/templates.min.js
lib/amd/build/templates.min.js.map
lib/amd/src/modal.js
lib/amd/src/templates.js
lib/antivirus/clamav/tests/scanner_test.php
lib/authlib.php
lib/badgeslib.php
lib/behat/classes/util.php
lib/classes/activity_dates.php [new file with mode: 0644]
lib/classes/check/access/riskadmin.php
lib/classes/check/access/riskbackup_result.php
lib/classes/check/access/riskxss_result.php
lib/classes/navigation/views/secondary.php [new file with mode: 0644]
lib/classes/navigation/views/view.php [new file with mode: 0644]
lib/classes/oauth2/api.php
lib/classes/oauth2/discovery/base_definition.php [new file with mode: 0644]
lib/classes/oauth2/discovery/imsbadgeconnect.php [new file with mode: 0644]
lib/classes/oauth2/discovery/openidconnect.php [new file with mode: 0644]
lib/classes/oauth2/issuer.php
lib/classes/oauth2/service/custom.php [new file with mode: 0644]
lib/classes/oauth2/service/google.php [new file with mode: 0644]
lib/classes/oauth2/service/imsobv2p1.php [new file with mode: 0644]
lib/classes/oauth2/service/issuer_interface.php [new file with mode: 0644]
lib/classes/task/backup_cleanup_task.php
lib/classes/task/file_temp_cleanup_task.php
lib/classes/task/send_failed_login_notifications_task.php
lib/classes/task/send_new_user_passwords_task.php
lib/classes/user.php
lib/completionlib.php
lib/datalib.php
lib/db/caches.php
lib/db/install.xml
lib/db/renamedclasses.php
lib/db/services.php
lib/db/upgrade.php
lib/ddl/tests/ddl_test.php
lib/deprecatedlib.php
lib/dml/tests/dml_read_slave_test.php
lib/dml/tests/dml_test.php
lib/dml/tests/pgsql_native_moodle_database_test.php
lib/dml/tests/pgsql_native_recordset_test.php
lib/editor/atto/plugins/html/thirdpartylibs.xml
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-debug.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-beautify/moodle-atto_html-beautify.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-debug.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror.js
lib/editor/atto/plugins/html/yui/src/beautify/LICENSE
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-css.js
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-html.js
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify.js
lib/editor/atto/plugins/html/yui/src/beautify/readme_moodle.txt
lib/editor/atto/plugins/html/yui/src/codemirror/js/codemirror.js
lib/editor/atto/plugins/html/yui/src/codemirror/js/css.js
lib/editor/atto/plugins/html/yui/src/codemirror/js/htmlmixed.js
lib/editor/atto/plugins/html/yui/src/codemirror/js/javascript.js
lib/editor/atto/plugins/html/yui/src/codemirror/js/xml.js
lib/filestorage/file_system_filedir.php
lib/filestorage/tests/file_storage_test.php
lib/filestorage/tests/file_system_filedir_test.php
lib/filestorage/tests/file_system_test.php
lib/filestorage/tests/zip_packer_test.php
lib/form/amd/build/modalform.min.js
lib/form/amd/build/modalform.min.js.map
lib/form/amd/src/modalform.js
lib/form/templates/element-autocomplete-inline.mustache
lib/formslib.php
lib/gdlib.php
lib/grouplib.php
lib/modinfolib.php
lib/moodlelib.php
lib/myprofilelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/pagelib.php
lib/phpunit/bootstrap.php
lib/phpunit/classes/autoloader.php [deleted file]
lib/phpunit/classes/base_testcase.php
lib/phpunit/classes/constraint_object_is_equal_with_exceptions.php
lib/phpunit/classes/coverage_info.php
lib/phpunit/classes/util.php
lib/phpunit/phpunit.xsd
lib/plist/CODE_OF_CONDUCT.md [new file with mode: 0644]
lib/plist/CONTRIBUTING.md [new file with mode: 0644]
lib/plist/LICENSE.md [moved from lib/plist/LICENSE with 91% similarity]
lib/plist/README.md
lib/plist/classes/CFPropertyList/CFArray.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFBinaryPropertyList.php
lib/plist/classes/CFPropertyList/CFBoolean.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFData.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFDate.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFDictionary.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFNumber.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFPropertyList.php
lib/plist/classes/CFPropertyList/CFString.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFType.php
lib/plist/classes/CFPropertyList/CFTypeDetector.php
lib/plist/classes/CFPropertyList/CFUid.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/IOException.php
lib/plist/classes/CFPropertyList/PListException.php
lib/plist/readme_moodle.txt
lib/rsslib.php
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/selectors.min.js
lib/table/amd/build/local/dynamic/selectors.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/selectors.js
lib/table/tests/coverage.php
lib/table/tests/local/filter/filterset_test.php
lib/tablelib.php
lib/testing/generator/data_generator.php
lib/testing/tests/generator_test.php
lib/tests/accesslib_test.php
lib/tests/admintree_test.php
lib/tests/antivirus_test.php
lib/tests/behat/app_behat_runtime.js
lib/tests/behat/behat_transformations.php
lib/tests/completionlib_test.php
lib/tests/component_test.php
lib/tests/content/export/exportable_items/exportable_filearea_test.php
lib/tests/content/export/exportable_items/exportable_stored_file_test.php
lib/tests/content/export/exportable_items/exportable_textarea_test.php
lib/tests/content/export/exporters/course_exporter_test.php
lib/tests/core_media_player_native.php
lib/tests/coverage.php
lib/tests/curl_security_helper_test.php
lib/tests/datalib_test.php
lib/tests/event_test.php
lib/tests/filelib_test.php
lib/tests/filter_manager_test.php
lib/tests/filterlib_test.php
lib/tests/grouplib_test.php
lib/tests/h5p_get_content_types_task_test.php
lib/tests/message_test.php
lib/tests/messagelib_test.php
lib/tests/moodle_url_test.php
lib/tests/moodlelib_test.php
lib/tests/navigation/views/secondary_test.php [new file with mode: 0644]
lib/tests/oauth2_test.php
lib/tests/outputcomponents_test.php
lib/tests/plugin_manager_test.php
lib/tests/portfoliolib_test.php
lib/tests/setuplib_test.php
lib/tests/tablelib_test.php
lib/tests/task_database_logger_test.php
lib/tests/theme_config_test.php
lib/tests/user_test.php
lib/tests/weblib_format_text_test.php
lib/tests/weblib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
lib/xapi/tests/coverage.php
lib/yuilib/2in3/2.9.0/build/yui2-treeview/yui2-treeview-debug.js
lib/yuilib/2in3/2.9.0/build/yui2-treeview/yui2-treeview-min.js
lib/yuilib/2in3/2.9.0/build/yui2-treeview/yui2-treeview.js
lib/yuilib/readme_moodle.txt
login/lib.php
login/tests/lib_test.php
media/player/html5audio/tests/player_test.php
media/player/html5video/tests/player_test.php
media/player/swf/tests/player_test.php
media/player/videojs/tests/player_test.php
media/player/vimeo/tests/player_test.php
media/player/youtube/tests/player_test.php
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_conversation_patcher.min.js.map
message/amd/src/message_drawer_view_conversation_patcher.js
message/classes/api.php
message/classes/helper.php
message/externallib.php
message/lib.php
message/output/email/classes/task/send_email_task.php
message/output/email/tests/send_email_task_test.php
message/tests/api_test.php
message/tests/behat/delete_messages.feature
message/tests/behat/message_delete_conversation.feature
message/tests/behat/message_send_messages.feature
message/tests/behat/self_conversation.feature
message/tests/behat/unread_messages.feature
message/tests/externallib_test.php
message/tests/privacy_provider_test.php
mnet/peer.php
mod/assign/classes/completion/custom_completion.php [new file with mode: 0644]
mod/assign/classes/output/grading_app.php
mod/assign/extensionform.php
mod/assign/externallib.php
mod/assign/feedback/editpdf/classes/pdf.php
mod/assign/feedback/file/locallib.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/override_form.php
mod/assign/overridedelete.php
mod/assign/overrides.php
mod/assign/submission/comments/tests/privacy_test.php
mod/assign/tests/behat/set_availability.feature [new file with mode: 0644]
mod/assign/tests/custom_completion_test.php [new file with mode: 0644]
mod/assign/tests/externallib_test.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/book/tests/lib_test.php
mod/chat/lib.php
mod/chat/tests/format_message_test.php
mod/choice/classes/dates.php [new file with mode: 0644]
mod/choice/lib.php
mod/choice/report.php
mod/choice/tests/behat/activity_info_completion_automatic.feature [new file with mode: 0644]
mod/choice/tests/behat/activity_info_completion_manual.feature [new file with mode: 0644]
mod/choice/tests/dates_test.php [new file with mode: 0644]
mod/choice/view.php
mod/data/classes/completion/custom_completion.php [new file with mode: 0644]
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/locallib.php
mod/data/preset.php
mod/data/tests/custom_completion_test.php [new file with mode: 0644]
mod/data/tests/lib_test.php
mod/data/tests/search_test.php
mod/data/view.php
mod/feedback/classes/completion/custom_completion.php [new file with mode: 0644]
mod/feedback/classes/responses_table.php
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/feedback/tests/custom_completion_test.php [new file with mode: 0644]
mod/forum/classes/completion/custom_completion.php [new file with mode: 0644]
mod/forum/classes/form/export_form.php
mod/forum/classes/local/vaults/discussion_list.php
mod/forum/classes/subscriptions.php
mod/forum/deprecatedlib.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/renderer.php
mod/forum/report/summary/classes/summary_table.php
mod/forum/rsslib.php
mod/forum/tests/coverage.php
mod/forum/tests/cron_trait.php
mod/forum/tests/custom_completion_test.php [new file with mode: 0644]
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/tests/mail_test.php
mod/glossary/classes/completion/custom_completion.php [new file with mode: 0644]
mod/glossary/classes/entry_query_builder.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/rsslib.php
mod/glossary/tests/custom_completion_test.php [new file with mode: 0644]
mod/glossary/tests/lib_test.php
mod/h5pactivity/mod_form.php
mod/h5pactivity/tests/behat/attempt_options.feature [new file with mode: 0644]
mod/h5pactivity/tests/coverage.php
mod/h5pactivity/tests/privacy_test.php
mod/imscp/locallib.php
mod/lesson/classes/completion/custom_completion.php [new file with mode: 0644]
mod/lesson/essay.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/override_form.php
mod/lesson/overridedelete.php
mod/lesson/overrides.php
mod/lesson/tests/custom_completion_test.php [new file with mode: 0644]
mod/lti/locallib.php
mod/lti/service.php
mod/quiz/accessrule/seb/tests/access_manager_test.php
mod/quiz/accessrule/seb/tests/quiz_settings_test.php
mod/quiz/accessrule/seb/tests/rule_test.php
mod/quiz/classes/completion/custom_completion.php [new file with mode: 0644]
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/override_form.php
mod/quiz/overridedelete.php
mod/quiz/overrides.php
mod/quiz/report/attemptsreport.php
mod/quiz/report/attemptsreport_table.php
mod/quiz/report/default.php
mod/quiz/report/grading/report.php
mod/quiz/report/overview/report.php
mod/quiz/tests/custom_completion_test.php [new file with mode: 0644]
mod/quiz/tests/lib_test.php
mod/scorm/classes/completion/custom_completion.php [new file with mode: 0644]
mod/scorm/datamodels/scormlib.php
mod/scorm/lang/en/scorm.php
mod/scorm/report/basic/classes/report.php
mod/scorm/report/interactions/classes/report.php
mod/scorm/report/objectives/classes/report.php
mod/scorm/report/userreport.php
mod/scorm/report/userreportinteractions.php
mod/scorm/report/userreporttracks.php
mod/scorm/tests/custom_completion_test.php [new file with mode: 0644]
mod/scorm/tests/privacy_test.php
mod/survey/classes/completion/custom_completion.php [new file with mode: 0644]
mod/survey/lang/en/survey.php
mod/survey/lib.php
mod/survey/tests/custom_completion_test.php [new file with mode: 0644]
mod/wiki/tests/lib_test.php
mod/wiki/tests/wikiparser_test.php
mod/workshop/allocation/manual/lib.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/renderer.php
mod/workshop/view.php
phpunit.xml.dist
plagiarism/upgrade.txt
privacy/classes/tests/request/content_writer.php
privacy/tests/contextlist_test.php
privacy/tests/coverage.php
privacy/tests/manager_test.php
privacy/tests/moodle_content_writer_test.php
privacy/tests/sitepolicy_test.php
privacy/tests/tests_content_writer_test.php
question/behaviour/adaptive/tests/walkthrough_test.php
question/behaviour/adaptivenopenalty/tests/walkthrough_test.php
question/behaviour/deferredcbm/tests/walkthrough_test.php
question/behaviour/deferredfeedback/tests/walkthrough_test.php
question/behaviour/immediatecbm/tests/walkthrough_test.php
question/behaviour/immediatefeedback/tests/walkthrough_test.php
question/behaviour/missing/tests/missingbehaviour_test.php
question/classes/bank/creator_name_column.php
question/classes/bank/modifier_name_column.php
question/classes/bank/search/category_condition.php
question/engine/tests/helpers.php
question/engine/tests/questionattempt_test.php
question/format/xml/tests/xmlformat_test.php
question/question.php
question/type/ddwtos/tests/behat/add.feature
question/type/edit_question_form.php
question/type/essay/renderer.php
question/type/essay/tests/walkthrough_test.php
question/type/gapselect/edit_form_base.php
question/type/gapselect/questiontypebase.php
question/type/gapselect/tests/behat/add.feature [new file with mode: 0644]
question/type/gapselect/tests/edit_form_test.php
question/type/match/tests/question_test.php
question/type/missingtype/tests/missingtype_test.php
question/type/multianswer/tests/walkthrough_test.php
question/type/multichoice/edit_multichoice_form.php
question/type/multichoice/questiontype.php
question/type/multichoice/tests/behat/add.feature
question/type/multichoice/tests/question_multi_test.php
question/type/questiontypebase.php
question/type/random/tests/questiontype_test.php
question/type/randomsamatch/tests/question_test.php
question/type/shortanswer/edit_shortanswer_form.php
question/type/shortanswer/questiontype.php
question/type/shortanswer/tests/behat/add.feature
question/type/truefalse/tests/walkthrough_test.php
question/type/upgrade.txt
rating/classes/external.php
rating/index.php
rating/lib.php
report/competency/lib.php
report/completion/index.php
report/configlog/classes/output/report_table.php
report/insights/classes/output/insight.php
report/log/classes/renderable.php
report/log/classes/table_log.php
report/log/locallib.php
report/loglive/classes/table_log.php
report/participation/index.php
report/progress/index.php
report/stats/locallib.php
repository/dropbox/tests/api_test.php
repository/flickr_public/lib.php
search/classes/engine.php
search/engine/solr/tests/engine_test.php
search/tests/area_category_test.php
search/tests/base_activity_test.php
search/tests/base_test.php
tag/classes/manage_table.php
theme/boost/scss/moodle/contentbank.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/tables.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/action_redir.php
user/amd/build/form_user_selector.min.js [new file with mode: 0644]
user/amd/build/form_user_selector.min.js.map [new file with mode: 0644]
user/amd/build/local/participantsfilter/filtertypes/country.min.js [new file with mode: 0644]
user/amd/build/local/participantsfilter/filtertypes/country.min.js.map [new file with mode: 0644]
user/amd/build/participants.min.js
user/amd/build/participants.min.js.map
user/amd/src/form_user_selector.js [new file with mode: 0644]
user/amd/src/local/participantsfilter/filtertypes/country.js [new file with mode: 0644]
user/amd/src/participants.js
user/classes/external/search_identity.php [new file with mode: 0644]
user/classes/external/user_summary_exporter.php
user/classes/fields.php [moved from lib/classes/user_fields.php with 93% similarity]
user/classes/output/participants_filter.php
user/classes/search/user.php
user/classes/table/participants.php
user/classes/table/participants_filterset.php
user/classes/table/participants_search.php
user/editlib.php
user/index.php
user/lib.php
user/selector/lib.php
user/templates/form_user_selector_suggestion.mustache [new file with mode: 0644]
user/tests/behat/filter_participants.feature
user/tests/behat/view_full_profile.feature
user/tests/externallib_test.php
user/tests/fields_test.php [moved from lib/tests/user_fields_test.php with 84% similarity]
user/tests/table/participants_search_test.php
user/upgrade.txt
userpix/index.php
version.php
webservice/classes/token_filter.php [new file with mode: 0644]
webservice/classes/token_form.php [new file with mode: 0644]
webservice/classes/token_table.php
webservice/lib.php
webservice/renderer.php
webservice/tests/generator/behat_core_webservice_generator.php [new file with mode: 0644]
webservice/tests/generator/lib.php [new file with mode: 0644]
webservice/tests/lib_test.php
webservice/upgrade.txt
webservice/xmlrpc/tests/locallib_test.php
webservice/xmlrpc/tests/xmlrpc_server_test.php

index 0d0a6ea..a6d5413 100644 (file)
@@ -1,4 +1,5 @@
 # Generated by "grunt ignorefiles"
+!/.grunt
 */**/yui/src/*/meta/
 */**/build/
 node_modules/
index e44591a..0dce8d8 100644 (file)
--- a/.eslintrc
+++ b/.eslintrc
       }
     },
     {
-      files: ["**/amd/src/*.js", "**/amd/src/**/*.js", "Gruntfile*.js", "babel-plugin-add-module-to-define.js"],
+      files: ["**/amd/src/*.js", "**/amd/src/**/*.js", "Gruntfile.js", ".grunt/*.js", ".grunt/tasks/*.js"],
       // We support es6 now. Woot!
       env: {
         es6: true
index 41e19ac..8bc6f60 100644 (file)
@@ -46,3 +46,4 @@ composer.phar
 atlassian-ide-plugin.xml
 /node_modules/
 /.vscode/
+moodle-plugin-ci.phar
similarity index 98%
rename from babel-plugin-add-module-to-define.js
rename to .grunt/babel-plugin-add-module-to-define.js
index dfe68c6..2e76d31 100644 (file)
@@ -39,7 +39,7 @@ module.exports = ({template, types}) => {
     const fs = require('fs');
     const path = require('path');
     const cwd = process.cwd();
-    const ComponentList = require(path.resolve('GruntfileComponents.js'));
+    const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
 
     /**
      * Search the list of components that match the given file name
similarity index 81%
rename from GruntfileComponents.js
rename to .grunt/components.js
index 74bd9de..848632d 100644 (file)
@@ -92,6 +92,16 @@ const fetchComponentData = () => {
     return componentData;
 };
 
+/**
+ * Get the list of component paths.
+ *
+ * @param   {string} relativeTo
+ * @returns {array}
+ */
+const getComponentPaths = (relativeTo = '') => fetchComponentData().pathList.map(componentPath => {
+    return componentPath.replace(relativeTo, '');
+});
+
 /**
  * Get the list of paths to build AMD sources.
  *
@@ -140,6 +150,49 @@ const getThirdPartyLibsList = relativeTo => {
         .sort();
 };
 
+/**
+ * Get the list of thirdparty library paths.
+ *
+ * @returns {array}
+ */
+const getThirdPartyPaths = () => {
+    const DOMParser = require('xmldom').DOMParser;
+    const fs = require('fs');
+    const path = require('path');
+    const xpath = require('xpath');
+
+    const thirdpartyfiles = getThirdPartyLibsList(fs.realpathSync('./'));
+    const libs = ['node_modules/', 'vendor/'];
+
+    const addLibToList = lib => {
+        if (!lib.match('\\*') && fs.statSync(lib).isDirectory()) {
+            // Ensure trailing slash on dirs.
+            lib = lib.replace(/\/?$/, '/');
+        }
+
+        // Look for duplicate paths before adding to array.
+        if (libs.indexOf(lib) === -1) {
+            libs.push(lib);
+        }
+    };
+
+    thirdpartyfiles.forEach(function(file) {
+        const dirname = path.dirname(file);
+
+        const xmlContent = fs.readFileSync(file, 'utf8');
+        const doc = new DOMParser().parseFromString(xmlContent);
+        const nodes = xpath.select("/libraries/library/location/text()", doc);
+
+        nodes.forEach(function(node) {
+            let lib = path.posix.join(dirname, node.toString());
+            addLibToList(lib);
+        });
+    });
+
+    return libs;
+
+};
+
 /**
  * Find the name of the component matching the specified path.
  *
@@ -182,7 +235,9 @@ const getOwningComponentDirectory = checkPath => {
 module.exports = {
     getAmdSrcGlobList,
     getComponentFromPath,
+    getComponentPaths,
     getOwningComponentDirectory,
     getYuiSrcGlobList,
     getThirdPartyLibsList,
+    getThirdPartyPaths,
 };
diff --git a/.grunt/tasks/eslint.js b/.grunt/tasks/eslint.js
new file mode 100644 (file)
index 0000000..b7408ac
--- /dev/null
@@ -0,0 +1,64 @@
+// 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/>.
+/* jshint node: true, browser: false */
+/* eslint-env node */
+
+/**
+ * @copyright  2021 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+module.exports = grunt => {
+    const files = grunt.moodleEnv.files;
+
+    // Project configuration.
+    grunt.config.merge({
+        eslint: {
+            // Even though warnings dont stop the build we don't display warnings by default because
+            // at this moment we've got too many core warnings.
+            // To display warnings call: grunt eslint --show-lint-warnings
+            // To fail on warnings call: grunt eslint --max-lint-warnings=0
+            // Also --max-lint-warnings=-1 can be used to display warnings but not fail.
+            options: {
+                quiet: (!grunt.option('show-lint-warnings')) && (typeof grunt.option('max-lint-warnings') === 'undefined'),
+                maxWarnings: ((typeof grunt.option('max-lint-warnings') !== 'undefined') ? grunt.option('max-lint-warnings') : -1)
+            },
+
+            // Check AMD src files.
+            amd: {src: files ? files : grunt.moodleEnv.amdSrc},
+
+            // Check YUI module source files.
+            yui: {src: files ? files : grunt.moodleEnv.yuiSrc},
+        },
+    });
+
+    grunt.loadNpmTasks('grunt-eslint');
+
+    // On watch, we dynamically modify config to build only affected files. This
+    // method is slightly complicated to deal with multiple changed files at once (copied
+    // from the grunt-contrib-watch readme).
+    let changedFiles = Object.create(null);
+    const onChange = grunt.util._.debounce(function() {
+        const files = Object.keys(changedFiles);
+        grunt.config('eslint.amd.src', files);
+        grunt.config('eslint.yui.src', files);
+        changedFiles = Object.create(null);
+    }, 200);
+
+    grunt.event.on('watch', (action, filepath) => {
+        changedFiles[filepath] = action;
+        onChange();
+    });
+};
diff --git a/.grunt/tasks/gherkinlint.js b/.grunt/tasks/gherkinlint.js
new file mode 100644 (file)
index 0000000..cc269a5
--- /dev/null
@@ -0,0 +1,89 @@
+// 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/>.
+/* jshint node: true, browser: false */
+/* eslint-env node */
+
+/**
+ * @copyright  2021 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+module.exports = grunt => {
+    /**
+     * Get the list of feature files to pass to the gherkin linter.
+     *
+     * @returns {Array}
+     */
+    const getGherkinLintTargets = () => {
+        if (grunt.moodleEnv.files) {
+            // Specific files were requested. Only check these.
+            return grunt.moodleEnv.files;
+        }
+
+        if (grunt.moodleEnv.inComponent) {
+            return [`${grunt.moodleEnv.runDir}/tests/behat/*.feature`];
+        }
+
+        return ['**/tests/behat/*.feature'];
+    };
+
+    const handler = function() {
+        const done = this.async();
+        const options = grunt.config('gherkinlint.options');
+
+        // Grab the gherkin-lint linter and required scaffolding.
+        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.
+        return linter.lint(
+            featureFinder.getFeatureFiles(grunt.file.expand(options.files)),
+            configParser.getConfiguration(configParser.defaultConfigFileName)
+        )
+        .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
+    };
+
+    grunt.registerTask('gherkinlint', 'Run gherkinlint against the current directory', handler);
+
+    grunt.config.set('gherkinlint', {
+        options: {
+            files: getGherkinLintTargets(),
+        }
+    });
+
+    grunt.config.merge({
+        watch: {
+            gherkinlint: {
+                files: [grunt.moodleEnv.inComponent ? 'tests/behat/*.feature' : '**/tests/behat/*.feature'],
+                tasks: ['gherkinlint'],
+            },
+        },
+    });
+
+    return handler;
+};
diff --git a/.grunt/tasks/ignorefiles.js b/.grunt/tasks/ignorefiles.js
new file mode 100644 (file)
index 0000000..d8b9ec1
--- /dev/null
@@ -0,0 +1,59 @@
+// 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/>.
+/* jshint node: true, browser: false */
+/* eslint-env node */
+
+/**
+ * @copyright  2021 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+module.exports = grunt => {
+    /**
+     * Generate ignore files (utilising thirdpartylibs.xml data)
+     */
+    const handler = function() {
+        const path = require('path');
+        const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
+
+        // An array of paths to third party directories.
+        const thirdPartyPaths = ComponentList.getThirdPartyPaths();
+
+        // Generate .eslintignore.
+        const eslintIgnores = [
+            '# Generated by "grunt ignorefiles"',
+            // Do not ignore the .grunt directory.
+            '!/.grunt',
+
+            // Ignore all yui/src meta directories and build directories.
+            '*/**/yui/src/*/meta/',
+            '*/**/build/',
+        ].concat(thirdPartyPaths);
+        grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
+
+        // Generate .stylelintignore.
+        const stylelintIgnores = [
+            '# Generated by "grunt ignorefiles"',
+            '**/yui/build/*',
+            'theme/boost/style/moodle.css',
+            'theme/classic/style/moodle.css',
+        ].concat(thirdPartyPaths);
+        grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
+    };
+
+    grunt.registerTask('ignorefiles', 'Generate ignore files for linters', handler);
+
+    return handler;
+};
diff --git a/.grunt/tasks/javascript.js b/.grunt/tasks/javascript.js
new file mode 100644 (file)
index 0000000..4cc309c
--- /dev/null
@@ -0,0 +1,141 @@
+// 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/>.
+/* jshint node: true, browser: false */
+/* eslint-env node */
+
+/**
+ * @copyright  2021 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Function to generate the destination for the uglify task
+ * (e.g. build/file.min.js). This function will be passed to
+ * the rename property of files array when building dynamically:
+ * http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically
+ *
+ * @param {String} destPath the current destination
+ * @param {String} srcPath the  matched src path
+ * @return {String} The rewritten destination path.
+ */
+const babelRename = function(destPath, srcPath) {
+    destPath = srcPath.replace('src', 'build');
+    destPath = destPath.replace('.js', '.min.js');
+    return destPath;
+};
+
+module.exports = grunt => {
+    // Load the Shifter tasks.
+    require('./shifter')(grunt);
+
+    // Load ESLint.
+    require('./eslint')(grunt);
+
+    const path = require('path');
+
+    // Register JS tasks.
+    grunt.registerTask('yui', ['eslint:yui', 'shifter']);
+    grunt.registerTask('amd', ['eslint:amd', 'babel']);
+    grunt.registerTask('js', ['amd', 'yui']);
+
+    // Register NPM tasks.
+    grunt.loadNpmTasks('grunt-contrib-uglify');
+    grunt.loadNpmTasks('grunt-contrib-watch');
+
+    // Load the Babel tasks and config.
+    grunt.loadNpmTasks('grunt-babel');
+    grunt.config.merge({
+        babel: {
+            options: {
+                sourceMaps: true,
+                comments: false,
+                plugins: [
+                    'transform-es2015-modules-amd-lazy',
+                    'system-import-transformer',
+                    // This plugin modifies the Babel transpiling for "export default"
+                    // so that if it's used then only the exported value is returned
+                    // by the generated AMD module.
+                    //
+                    // It also adds the Moodle plugin name to the AMD module definition
+                    // so that it can be imported as expected in other modules.
+                    path.resolve('.grunt/babel-plugin-add-module-to-define.js'),
+                    '@babel/plugin-syntax-dynamic-import',
+                    '@babel/plugin-syntax-import-meta',
+                    ['@babel/plugin-proposal-class-properties', {'loose': false}],
+                    '@babel/plugin-proposal-json-strings'
+                ],
+                presets: [
+                    ['minify', {
+                        // This minification plugin needs to be disabled because it breaks the
+                        // source map generation and causes invalid source maps to be output.
+                        simplify: false,
+                        builtIns: false
+                    }],
+                    ['@babel/preset-env', {
+                        targets: {
+                            browsers: [
+                                ">0.25%",
+                                "last 2 versions",
+                                "not ie <= 10",
+                                "not op_mini all",
+                                "not Opera > 0",
+                                "not dead"
+                            ]
+                        },
+                        modules: false,
+                        useBuiltIns: false
+                    }]
+                ]
+            },
+            dist: {
+                files: [{
+                    expand: true,
+                    src: grunt.moodleEnv.files ? grunt.moodleEnv.files : grunt.moodleEnv.amdSrc,
+                    rename: babelRename
+                }]
+            }
+        },
+    });
+
+    grunt.config.merge({
+        watch: {
+            amd: {
+                files: grunt.moodleEnv.inComponent
+                    ? ['amd/src/*.js', 'amd/src/**/*.js']
+                    : ['**/amd/src/**/*.js'],
+                tasks: ['amd']
+            },
+        },
+    });
+
+    // On watch, we dynamically modify config to build only affected files. This
+    // method is slightly complicated to deal with multiple changed files at once (copied
+    // from the grunt-contrib-watch readme).
+    let changedFiles = Object.create(null);
+    const onChange = grunt.util._.debounce(function() {
+        const files = Object.keys(changedFiles);
+        grunt.config('babel.dist.files', [{expand: true, src: files, rename: babelRename}]);
+        changedFiles = Object.create(null);
+    }, 200);
+
+    grunt.event.on('watch', function(action, filepath) {
+        changedFiles[filepath] = action;
+        onChange();
+    });
+
+    return {
+        babelRename,
+    };
+};
diff --git a/.grunt/tasks/sass.js b/.grunt/tasks/sass.js
new file mode 100644 (file)
index 0000000..cdb8bb1
--- /dev/null
@@ -0,0 +1,40 @@
+// 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/>.
+/* jshint node: true, browser: false */
+/* eslint-env node */
+
+/**
+ * @copyright  2021 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+module.exports = grunt => {
+    grunt.loadNpmTasks('grunt-sass');
+
+    grunt.config.merge({
+        sass: {
+            dist: {
+                files: {
+                    "theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss",
+                    "theme/classic/style/moodle.css": "theme/classic/scss/classicgrunt.scss"
+                }
+            },
+            options: {
+                implementation: require('node-sass'),
+                includePaths: ["theme/boost/scss/", "theme/classic/scss/"]
+            }
+        },
+    });
+};
diff --git a/.grunt/tasks/shifter.js b/.grunt/tasks/shifter.js
new file mode 100644 (file)
index 0000000..b364af5
--- /dev/null
@@ -0,0 +1,155 @@
+// 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/>.
+/* jshint node: true, browser: false */
+/* eslint-env node */
+
+/**
+ * @copyright  2021 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/* eslint-env node */
+
+module.exports = grunt => {
+    /**
+     * Shifter task. Is configured with a path to a specific file or a directory,
+     * in the case of a specific file it will work out the right module to be built.
+     *
+     * Note that this task runs the invidiaul shifter jobs async (becase it spawns
+     * so be careful to to call done().
+     */
+    const handler = function() {
+        const done = this.async();
+        const options = grunt.config('shifter.options');
+        const async = require('async');
+        const path = require('path');
+
+        // Run the shifter processes one at a time to avoid confusing output.
+        async.eachSeries(options.paths, function(src, filedone) {
+            var args = [];
+            args.push(path.normalize(process.cwd() + '/node_modules/shifter/bin/shifter'));
+
+            // Always ignore the node_modules directory.
+            args.push('--excludes', 'node_modules');
+
+            // Determine the most appropriate options to run with based upon the current location.
+            if (grunt.file.isMatch('**/yui/**/*.js', src)) {
+                // When passed a JS file, build our containing module (this happen with
+                // watch).
+                grunt.log.debug('Shifter passed a specific JS file');
+                src = path.dirname(path.dirname(src));
+                options.recursive = false;
+            } else if (grunt.file.isMatch('**/yui/src', src)) {
+                // When in a src directory --walk all modules.
+                grunt.log.debug('In a src directory');
+                args.push('--walk');
+                options.recursive = false;
+            } else if (grunt.file.isMatch('**/yui/src/*', src)) {
+                // When in module, only build our module.
+                grunt.log.debug('In a module directory');
+                options.recursive = false;
+            } else if (grunt.file.isMatch('**/yui/src/*/js', src)) {
+                // When in module src, only build our module.
+                grunt.log.debug('In a source directory');
+                src = path.dirname(src);
+                options.recursive = false;
+            }
+
+            if (grunt.option('watch')) {
+                grunt.fail.fatal('The --watch option has been removed, please use `grunt watch` instead');
+            }
+
+            // Add the stderr option if appropriate
+            if (grunt.option('verbose')) {
+                args.push('--lint-stderr');
+            }
+
+            if (grunt.option('no-color')) {
+                args.push('--color=false');
+            }
+
+            var execShifter = function() {
+
+                grunt.log.ok("Running shifter on " + src);
+                grunt.util.spawn({
+                    cmd: "node",
+                    args: args,
+                    opts: {cwd: src, stdio: 'inherit', env: process.env}
+                }, function(error, result, code) {
+                    if (code) {
+                        grunt.fail.fatal('Shifter failed with code: ' + code);
+                    } else {
+                        grunt.log.ok('Shifter build complete.');
+                        filedone();
+                    }
+                });
+            };
+
+            // Actually run shifter.
+            if (!options.recursive) {
+                execShifter();
+            } else {
+                // Check that there are yui modules otherwise shifter ends with exit code 1.
+                if (grunt.file.expand({cwd: src}, '**/yui/src/**/*.js').length > 0) {
+                    args.push('--recursive');
+                    execShifter();
+                } else {
+                    grunt.log.ok('No YUI modules to build.');
+                    filedone();
+                }
+            }
+        }, done);
+    };
+
+    // Register the shifter task.
+    grunt.registerTask('shifter', 'Run Shifter against the current directory', handler);
+
+    // Configure it.
+    grunt.config.set('shifter', {
+        options: {
+            recursive: true,
+            // Shifter takes a relative path.
+            paths: grunt.moodleEnv.files ? grunt.moodleEnv.files : [grunt.moodleEnv.runDir]
+        }
+    });
+
+    grunt.config.merge({
+        watch: {
+            yui: {
+                files: grunt.moodleEnv.inComponent
+                    ? ['yui/src/*.json', 'yui/src/**/*.js']
+                    : ['**/yui/src/**/*.js'],
+                tasks: ['yui']
+            },
+        },
+    });
+
+    // On watch, we dynamically modify config to build only affected files. This
+    // method is slightly complicated to deal with multiple changed files at once (copied
+    // from the grunt-contrib-watch readme).
+    let changedFiles = Object.create(null);
+    const onChange = grunt.util._.debounce(function() {
+        const files = Object.keys(changedFiles);
+        grunt.config('shifter.options.paths', files);
+        changedFiles = Object.create(null);
+    }, 200);
+
+    grunt.event.on('watch', (action, filepath) => {
+        changedFiles[filepath] = action;
+        onChange();
+    });
+
+    return handler;
+};
diff --git a/.grunt/tasks/startup.js b/.grunt/tasks/startup.js
new file mode 100644 (file)
index 0000000..b532852
--- /dev/null
@@ -0,0 +1,48 @@
+// 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/>.
+/* jshint node: true, browser: false */
+/* eslint-env node */
+
+/**
+ * @copyright  2021 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+module.exports = grunt => {
+    /**
+     * Generate ignore files (utilising thirdpartylibs.xml data)
+     */
+    const handler = function() {
+        const path = require('path');
+
+        // Are we in a YUI directory?
+        if (path.basename(path.resolve(grunt.moodleEnv.cwd, '../../')) == 'yui') {
+            grunt.task.run('yui');
+        // Are we in an AMD directory?
+        } else if (grunt.moodleEnv.inAMD) {
+            grunt.task.run('amd');
+        } else {
+            // Run them all!.
+            grunt.task.run('css');
+            grunt.task.run('js');
+            grunt.task.run('gherkinlint');
+        }
+    };
+
+    // Register the startup task.
+    grunt.registerTask('startup', 'Run the correct tasks for the current directory', handler);
+
+    return handler;
+};
diff --git a/.grunt/tasks/style.js b/.grunt/tasks/style.js
new file mode 100644 (file)
index 0000000..05f713c
--- /dev/null
@@ -0,0 +1,29 @@
+// 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/>.
+/* jshint node: true, browser: false */
+/* eslint-env node */
+
+/**
+ * @copyright  2021 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+module.exports = grunt => {
+    // Load the Style Lint tasks.
+    require('./stylelint')(grunt);
+
+    // Load the SASS tasks.
+    require('./sass')(grunt);
+};
diff --git a/.grunt/tasks/stylelint.js b/.grunt/tasks/stylelint.js
new file mode 100644 (file)
index 0000000..4da9067
--- /dev/null
@@ -0,0 +1,181 @@
+// 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/>.
+/* jshint node: true, browser: false */
+/* eslint-env node */
+
+/**
+ * @copyright  2021 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+module.exports = grunt => {
+
+    const getCssConfigForFiles = files => {
+        return {
+            stylelint: {
+                css: {
+                    // Use a fully-qualified path.
+                    src: files,
+                    options: {
+                        configOverrides: {
+                            rules: {
+                                // These rules have to be disabled in .stylelintrc for scss compat.
+                                "at-rule-no-unknown": true,
+                            }
+                        }
+                    }
+                },
+            },
+        };
+    };
+
+    const getScssConfigForFiles = files => {
+        return {
+            stylelint: {
+                scss: {
+                    options: {syntax: 'scss'},
+                    src: files,
+                },
+            },
+        };
+    };
+
+    /**
+     * Register any stylelint tasks.
+     *
+     * @param {Object} grunt
+     * @param {Array} files
+     * @param {String} fullRunDir
+     */
+    const registerStyleLintTasks = () => {
+        const glob = require('glob');
+
+        // The stylelinters do not handle the case where a configuration was provided but no files were included.
+        // Keep track of whether any files were found.
+        let hasCss = false;
+        let hasScss = false;
+
+        // The stylelint processors do not take a path argument. They always check all provided values.
+        // As a result we must check through each glob and determine if any files match the current directory.
+        const scssFiles = [];
+        const cssFiles = [];
+
+        const requestedFiles = grunt.moodleEnv.files;
+        if (requestedFiles) {
+            // Grunt was called with a files argument.
+            // Check whether each of the requested files matches either the CSS or SCSS source file list.
+
+            requestedFiles.forEach(changedFilePath => {
+                let matchesGlob;
+
+                // Check whether this watched path matches any watched SCSS file.
+                matchesGlob = grunt.moodleEnv.scssSrc.some(watchedPathGlob => {
+                    return glob.sync(watchedPathGlob).indexOf(changedFilePath) !== -1;
+                });
+                if (matchesGlob) {
+                    scssFiles.push(changedFilePath);
+                    hasScss = true;
+                }
+
+                // Check whether this watched path matches any watched CSS file.
+                matchesGlob = grunt.moodleEnv.cssSrc.some(watchedPathGlob => {
+                    return glob.sync(watchedPathGlob).indexOf(changedFilePath) !== -1;
+                });
+                if (matchesGlob) {
+                    cssFiles.push(changedFilePath);
+                    hasCss = true;
+                }
+            });
+        } else {
+            // Grunt was called without a list of files.
+            // The start directory (runDir) may be a child dir of the project.
+            // Check each scssSrc file to see if it's in the start directory.
+            // This means that we can lint just mod/*/styles.css if started in the mod directory.
+
+            grunt.moodleEnv.scssSrc.forEach(path => {
+                if (path.startsWith(grunt.moodleEnv.runDir)) {
+                    scssFiles.push(path);
+                    hasScss = true;
+                }
+            });
+
+            grunt.moodleEnv.cssSrc.forEach(path => {
+                if (path.startsWith(grunt.moodleEnv.runDir)) {
+                    cssFiles.push(path);
+                    hasCss = true;
+                }
+            });
+        }
+
+        // Register the tasks.
+        const scssTasks = ['sass'];
+        if (hasScss) {
+            grunt.config.merge(getScssConfigForFiles(scssFiles));
+            scssTasks.unshift('stylelint:scss');
+        }
+
+        const cssTasks = [];
+        if (hasCss) {
+            grunt.config.merge(getCssConfigForFiles(cssFiles));
+            cssTasks.push('stylelint:css');
+        }
+
+        // The tasks must be registered, even if empty to ensure a consistent command list.
+        // They jsut won't run anything.
+        grunt.registerTask('scss', scssTasks);
+        grunt.registerTask('rawcss', cssTasks);
+    };
+
+    // Register CSS tasks.
+    grunt.loadNpmTasks('grunt-stylelint');
+
+    // Register the style lint tasks.
+    registerStyleLintTasks();
+    grunt.registerTask('css', ['scss', 'rawcss']);
+
+    const getCoreThemeMatches = () => {
+        const scssMatch = 'scss/**/*.scss';
+
+        if (grunt.moodleEnv.inTheme) {
+            return [scssMatch];
+        }
+
+        if (grunt.moodleEnv.runDir.startsWith('theme')) {
+            return [`*/${scssMatch}`];
+        }
+
+        return [`theme/*/${scssMatch}`];
+    };
+
+    // Add the watch configuration for rawcss, and scss.
+    grunt.config.merge({
+        watch: {
+            rawcss: {
+                files: [
+                    '**/*.css',
+                ],
+                excludes: [
+                    '**/moodle.css',
+                    '**/editor.css',
+                ],
+                tasks: ['rawcss']
+            },
+            scss: {
+                files: getCoreThemeMatches(),
+                tasks: ['scss']
+            },
+        },
+    });
+};
diff --git a/.grunt/tasks/watch.js b/.grunt/tasks/watch.js
new file mode 100644 (file)
index 0000000..25c6487
--- /dev/null
@@ -0,0 +1,272 @@
+/**
+ * This is a wrapper task to handle the grunt watch command. It attempts to use
+ * Watchman to monitor for file changes, if it's installed, because it's much faster.
+ *
+ * If Watchman isn't installed then it falls back to the grunt-contrib-watch file
+ * watcher for backwards compatibility.
+ */
+
+/* eslint-env node */
+
+module.exports = grunt => {
+    /**
+     * This is a wrapper task to handle the grunt watch command. It attempts to use
+     * Watchman to monitor for file changes, if it's installed, because it's much faster.
+     *
+     * If Watchman isn't installed then it falls back to the grunt-contrib-watch file
+     * watcher for backwards compatibility.
+     */
+    const watchHandler = function() {
+        const async = require('async');
+        const watchTaskDone = this.async();
+        let watchInitialised = false;
+        let watchTaskQueue = {};
+        let processingQueue = false;
+
+        const watchman = require('fb-watchman');
+        const watchmanClient = new watchman.Client();
+
+        // Grab the tasks and files that have been queued up and execute them.
+        var processWatchTaskQueue = function() {
+            if (!Object.keys(watchTaskQueue).length || processingQueue) {
+                // If there is nothing in the queue or we're already processing then wait.
+                return;
+            }
+
+            processingQueue = true;
+
+            // Grab all tasks currently in the queue.
+            var queueToProcess = watchTaskQueue;
+            // Reset the queue.
+            watchTaskQueue = {};
+
+            async.forEachSeries(
+                Object.keys(queueToProcess),
+                function(task, next) {
+                    var files = queueToProcess[task];
+                    var filesOption = '--files=' + files.join(',');
+                    grunt.log.ok('Running task ' + task + ' for files ' + filesOption);
+
+                    // Spawn the task in a child process so that it doesn't kill this one
+                    // if it failed.
+                    grunt.util.spawn(
+                        {
+                            // Spawn with the grunt bin.
+                            grunt: true,
+                            // Run from current working dir and inherit stdio from process.
+                            opts: {
+                                cwd: grunt.moodleEnv.fullRunDir,
+                                stdio: 'inherit'
+                            },
+                            args: [task, filesOption]
+                        },
+                        function(err, res, code) {
+                            if (code !== 0) {
+                                // The grunt task failed.
+                                grunt.log.error(err);
+                            }
+
+                            // Move on to the next task.
+                            next();
+                        }
+                    );
+                },
+                function() {
+                    // No longer processing.
+                    processingQueue = false;
+                    // Once all of the tasks are done then recurse just in case more tasks
+                    // were queued while we were processing.
+                    processWatchTaskQueue();
+                }
+            );
+        };
+
+        const originalWatchConfig = grunt.config.get(['watch']);
+        const watchConfig = Object.keys(originalWatchConfig).reduce(function(carry, key) {
+            if (key == 'options') {
+                return carry;
+            }
+
+            const value = originalWatchConfig[key];
+
+            const taskNames = value.tasks;
+            const files = value.files;
+            let excludes = [];
+            if (value.excludes) {
+                excludes = value.excludes;
+            }
+
+            taskNames.forEach(function(taskName) {
+                carry[taskName] = {
+                    files,
+                    excludes,
+                };
+            });
+
+            return carry;
+        }, {});
+
+        watchmanClient.on('error', function(error) {
+            // We have to add an error handler here and parse the error string because the
+            // example way from the docs to check if Watchman is installed doesn't actually work!!
+            // See: https://github.com/facebook/watchman/issues/509
+            if (error.message.match('Watchman was not found')) {
+                // If watchman isn't installed then we should fallback to the other watch task.
+                grunt.log.ok('It is recommended that you install Watchman for better performance using the "watch" command.');
+
+                // Fallback to the old grunt-contrib-watch task.
+                grunt.renameTask('watch-grunt', 'watch');
+                grunt.task.run(['watch']);
+                // This task is finished.
+                watchTaskDone(0);
+            } else {
+                grunt.log.error(error);
+                // Fatal error.
+                watchTaskDone(1);
+            }
+        });
+
+        watchmanClient.on('subscription', function(resp) {
+            if (resp.subscription !== 'grunt-watch') {
+                return;
+            }
+
+            resp.files.forEach(function(file) {
+                grunt.log.ok('File changed: ' + file.name);
+
+                var fullPath = grunt.moodleEnv.fullRunDir + '/' + file.name;
+                Object.keys(watchConfig).forEach(function(task) {
+
+                    const fileGlobs = watchConfig[task].files;
+                    var match = fileGlobs.some(function(fileGlob) {
+                        return grunt.file.isMatch(`**/${fileGlob}`, fullPath);
+                    });
+
+                    if (match) {
+                        // If we are watching a subdirectory then the file.name will be relative
+                        // to that directory. However the grunt tasks  expect the file paths to be
+                        // relative to the Gruntfile.js location so let's normalise them before
+                        // adding them to the queue.
+                        var relativePath = fullPath.replace(grunt.moodleEnv.gruntFilePath + '/', '');
+                        if (task in watchTaskQueue) {
+                            if (!watchTaskQueue[task].includes(relativePath)) {
+                                watchTaskQueue[task] = watchTaskQueue[task].concat(relativePath);
+                            }
+                        } else {
+                            watchTaskQueue[task] = [relativePath];
+                        }
+                    }
+                });
+            });
+
+            processWatchTaskQueue();
+        });
+
+        process.on('SIGINT', function() {
+            // Let the user know that they may need to manually stop the Watchman daemon if they
+            // no longer want it running.
+            if (watchInitialised) {
+                grunt.log.ok('The Watchman daemon may still be running and may need to be stopped manually.');
+            }
+
+            process.exit();
+        });
+
+        // Initiate the watch on the current directory.
+        watchmanClient.command(['watch-project', grunt.moodleEnv.fullRunDir], function(watchError, watchResponse) {
+            if (watchError) {
+                grunt.log.error('Error initiating watch:', watchError);
+                watchTaskDone(1);
+                return;
+            }
+
+            if ('warning' in watchResponse) {
+                grunt.log.error('warning: ', watchResponse.warning);
+            }
+
+            var watch = watchResponse.watch;
+            var relativePath = watchResponse.relative_path;
+            watchInitialised = true;
+
+            watchmanClient.command(['clock', watch], function(clockError, clockResponse) {
+                if (clockError) {
+                    grunt.log.error('Failed to query clock:', clockError);
+                    watchTaskDone(1);
+                    return;
+                }
+
+                // Generate the expression query used by watchman.
+                // Documentation is limited, but see https://facebook.github.io/watchman/docs/expr/allof.html for examples.
+                // We generate an expression to match any value in the files list of all of our tasks, but excluding
+                // all value in the  excludes list of that task.
+                //
+                // [anyof, [
+                //      [allof, [
+                //          [anyof, [
+                //              ['match', validPath, 'wholename'],
+                //              ['match', validPath, 'wholename'],
+                //          ],
+                //          [not,
+                //              [anyof, [
+                //                  ['match', invalidPath, 'wholename'],
+                //                  ['match', invalidPath, 'wholename'],
+                //              ],
+                //          ],
+                //      ],
+                var matchWholeName = fileGlob => ['match', fileGlob, 'wholename'];
+                var matches = Object.keys(watchConfig).map(function(task) {
+                    const matchAll = [];
+                    matchAll.push(['anyof'].concat(watchConfig[task].files.map(matchWholeName)));
+
+                    if (watchConfig[task].excludes.length) {
+                        matchAll.push(['not', ['anyof'].concat(watchConfig[task].excludes.map(matchWholeName))]);
+                    }
+
+                    return ['allof'].concat(matchAll);
+                });
+
+                matches = ['anyof'].concat(matches);
+
+                var sub = {
+                    expression: matches,
+                    // Which fields we're interested in.
+                    fields: ["name", "size", "type"],
+                    // Add our time constraint.
+                    since: clockResponse.clock
+                };
+
+                if (relativePath) {
+                    /* eslint-disable camelcase */
+                    sub.relative_root = relativePath;
+                }
+
+                watchmanClient.command(['subscribe', watch, 'grunt-watch', sub], function(subscribeError) {
+                    if (subscribeError) {
+                        // Probably an error in the subscription criteria.
+                        grunt.log.error('failed to subscribe: ', subscribeError);
+                        watchTaskDone(1);
+                        return;
+                    }
+
+                    grunt.log.ok('Listening for changes to files in ' + grunt.moodleEnv.fullRunDir);
+                });
+            });
+        });
+    };
+
+    // Rename the grunt-contrib-watch "watch" task because we're going to wrap it.
+    grunt.renameTask('watch', 'watch-grunt');
+
+    // Register the new watch handler.
+    grunt.registerTask('watch', 'Run tasks on file changes', watchHandler);
+
+    grunt.config.merge({
+        watch: {
+            options: {
+                nospawn: true // We need not to spawn so config can be changed dynamically.
+            },
+        },
+    });
+
+    return watchHandler;
+};
index a7b728f..89836af 100644 (file)
 /* eslint-env node */
 
 /**
+ * Grunt configuration for Moodle.
+ *
  * @copyright  2014 Andrew Nicols
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-/* eslint-env node */
-
 /**
- * Calculate the cwd, taking into consideration the `root` option (for Windows).
+ * Setup the Grunt Moodle environment.
  *
- * @param {Object} grunt
- * @returns {String} The current directory as best we can determine
+ * @param   {Grunt} grunt
+ * @returns {Object}
  */
-const getCwd = grunt => {
+const setupMoodleEnvironment = grunt => {
     const fs = require('fs');
     const path = require('path');
+    const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
 
-    let cwd = fs.realpathSync(process.env.PWD || process.cwd());
+    const getAmdConfiguration = () => {
+        // If the cwd is the amd directory in the current component then it will be empty.
+        // If the cwd is a child of the component's AMD directory, the relative directory will not start with ..
+        let inAMD = !path.relative(`${componentDirectory}/amd`, cwd).startsWith('..');
 
-    // Windows users can't run grunt in a subdirectory, so allow them to set
-    // the root by passing --root=path/to/dir.
-    if (grunt.option('root')) {
-        const root = grunt.option('root');
-        if (grunt.file.exists(__dirname, root)) {
-            cwd = fs.realpathSync(path.join(__dirname, root));
-            grunt.log.ok('Setting root to ' + cwd);
+        // Globbing pattern for matching all AMD JS source files.
+        let amdSrc = [];
+        if (inComponent) {
+            amdSrc.push(
+                componentDirectory + "/amd/src/*.js",
+                componentDirectory + "/amd/src/**/*.js"
+            );
         } else {
-            grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist');
+            amdSrc = ComponentList.getAmdSrcGlobList();
         }
-    }
 
-    return cwd;
-};
-
-/**
- * Register any stylelint tasks.
- *
- * @param {Object} grunt
- * @param {Array} files
- * @param {String} fullRunDir
- */
-const registerStyleLintTasks = (grunt, files, fullRunDir) => {
-    const getCssConfigForFiles = files => {
         return {
-            stylelint: {
-                css: {
-                    // Use a fully-qualified path.
-                    src: files,
-                    options: {
-                        configOverrides: {
-                            rules: {
-                                // These rules have to be disabled in .stylelintrc for scss compat.
-                                "at-rule-no-unknown": true,
-                            }
-                        }
-                    }
-                },
-            },
+            inAMD,
+            amdSrc,
         };
     };
 
-    const getScssConfigForFiles = files => {
+    const getYuiConfiguration = () => {
+        let yuiSrc = [];
+        if (inComponent) {
+            yuiSrc.push(componentDirectory + "/yui/src/**/*.js");
+        } else {
+            yuiSrc = ComponentList.getYuiSrcGlobList(gruntFilePath + '/');
+        }
+
         return {
-            stylelint: {
-                scss: {
-                    options: {syntax: 'scss'},
-                    src: files,
-                },
-            },
+            yuiSrc,
         };
     };
 
-    let hasCss = true;
-    let hasScss = true;
-
-    if (files) {
-        // Specific files were passed. Just set them up.
-        grunt.config.merge(getCssConfigForFiles(files));
-        grunt.config.merge(getScssConfigForFiles(files));
-    } else {
-        // The stylelint system does not handle the case where there was no file to lint.
-        // Check whether there are any files to lint in the current directory.
-        const glob = require('glob');
-
-        const scssSrc = [];
-        glob.sync(`${fullRunDir}/**/*.scss`).forEach(path => scssSrc.push(path));
+    const getStyleConfiguration = () => {
+        const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
+        // Build the cssSrc and scssSrc.
+        // Valid paths are:
+        // [component]/styles.css; and either
+        // [theme/[themename]]/scss/**/*.scss; or
+        // [theme/[themename]]/style/*.css.
+        //
+        // If a theme has scss, then it is assumed that the style directory contains generated content.
+        let cssSrc = [];
+        let scssSrc = [];
+
+        const checkComponentDirectory = componentDirectory => {
+            const isTheme = componentDirectory.startsWith('theme/');
+            if (isTheme) {
+                const scssDirectory = `${componentDirectory}/scss`;
+
+                if (fs.existsSync(scssDirectory)) {
+                    // This theme has an SCSS directory.
+                    // Include all scss files within it recursively, but do not check for css files.
+                    scssSrc.push(`${scssDirectory}/*.scss`);
+                    scssSrc.push(`${scssDirectory}/**/*.scss`);
+                } else {
+                    // This theme has no SCSS directory.
+                    // Only hte CSS files in the top-level directory are checked.
+                    cssSrc.push(`${componentDirectory}/style/*.css`);
+                }
+            } else {
+                // This is not a theme.
+                // All other plugin types are restricted to a single styles.css in their top level.
+                cssSrc.push(`${componentDirectory}/styles.css`);
+            }
+        };
 
-        if (scssSrc.length) {
-            grunt.config.merge(getScssConfigForFiles(scssSrc));
+        if (inComponent) {
+            checkComponentDirectory(componentDirectory);
         } else {
-            hasScss = false;
+            ComponentList.getComponentPaths(`${gruntFilePath}/`).forEach(componentPath => {
+                checkComponentDirectory(componentPath);
+            });
         }
 
-        const cssSrc = [];
-        glob.sync(`${fullRunDir}/**/*.css`).forEach(path => cssSrc.push(path));
+        return {
+            cssSrc,
+            scssSrc,
+        };
+    };
 
-        if (cssSrc.length) {
-            grunt.config.merge(getCssConfigForFiles(cssSrc));
-        } else {
-            hasCss = false;
+    /**
+     * Calculate the cwd, taking into consideration the `root` option (for Windows).
+     *
+     * @param {Object} grunt
+     * @returns {String} The current directory as best we can determine
+     */
+    const getCwd = grunt => {
+        let cwd = fs.realpathSync(process.env.PWD || process.cwd());
+
+        // Windows users can't run grunt in a subdirectory, so allow them to set
+        // the root by passing --root=path/to/dir.
+        if (grunt.option('root')) {
+            const root = grunt.option('root');
+            if (grunt.file.exists(__dirname, root)) {
+                cwd = fs.realpathSync(path.join(__dirname, root));
+                grunt.log.ok('Setting root to ' + cwd);
+            } else {
+                grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist');
+            }
         }
-    }
-
-    const scssTasks = ['sass'];
-    if (hasScss) {
-        scssTasks.unshift('stylelint:scss');
-    }
-    grunt.registerTask('scss', scssTasks);
-
-    const cssTasks = [];
-    if (hasCss) {
-        cssTasks.push('stylelint:css');
-    }
-    grunt.registerTask('rawcss', cssTasks);
-
-    grunt.registerTask('css', ['scss', 'rawcss']);
-};
-
-/**
- * Grunt configuration.
- *
- * @param {Object} grunt
- */
-module.exports = function(grunt) {
-    const path = require('path');
-    const tasks = {};
-    const async = require('async');
-    const DOMParser = require('xmldom').DOMParser;
-    const xpath = require('xpath');
-    const semver = require('semver');
-    const watchman = require('fb-watchman');
-    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);
-    var actual = semver.valid(process.version);
-    if (!semver.satisfies(actual, expected)) {
-        grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual);
-    }
+        return cwd;
+    };
 
     // Detect directories:
     // * gruntFilePath          The real path on disk to this Gruntfile.js
@@ -171,8 +153,19 @@ module.exports = function(grunt) {
     const relativeCwd = path.relative(gruntFilePath, cwd);
     const componentDirectory = ComponentList.getOwningComponentDirectory(relativeCwd);
     const inComponent = !!componentDirectory;
+    const inTheme = !!componentDirectory && componentDirectory.startsWith('theme/');
     const runDir = inComponent ? componentDirectory : relativeCwd;
     const fullRunDir = fs.realpathSync(gruntFilePath + path.sep + runDir);
+    const {inAMD, amdSrc} = getAmdConfiguration();
+    const {yuiSrc} = getYuiConfiguration();
+    const {cssSrc, scssSrc} = getStyleConfiguration();
+
+    let files = null;
+    if (grunt.option('files')) {
+        // Accept a comma separated list of files to process.
+        files = grunt.option('files').split(',');
+    }
+
     grunt.log.debug('============================================================================');
     grunt.log.debug(`= Node version:        ${process.versions.node}`);
     grunt.log.debug(`= grunt version:       ${grunt.package.version}`);
@@ -192,667 +185,77 @@ module.exports = function(grunt) {
         grunt.log.ok(`Running tasks for component directory ${componentDirectory}`);
     }
 
-    let files = null;
-    if (grunt.option('files')) {
-        // Accept a comma separated list of files to process.
-        files = grunt.option('files').split(',');
-    }
-
-    // If the cwd is the amd directory in the current component then it will be empty.
-    // If the cwd is a child of the component's AMD directory, the relative directory will not start with ..
-    const inAMD = !path.relative(`${componentDirectory}/amd`, cwd).startsWith('..');
-
-    // Globbing pattern for matching all AMD JS source files.
-    let amdSrc = [];
-    if (inComponent) {
-        amdSrc.push(componentDirectory + "/amd/src/*.js");
-        amdSrc.push(componentDirectory + "/amd/src/**/*.js");
-    } else {
-        amdSrc = ComponentList.getAmdSrcGlobList();
-    }
-
-    let yuiSrc = [];
-    if (inComponent) {
-        yuiSrc.push(componentDirectory + "/yui/src/**/*.js");
-    } else {
-        yuiSrc = ComponentList.getYuiSrcGlobList(gruntFilePath + '/');
-    }
-
-    /**
-     * Function to generate the destination for the uglify task
-     * (e.g. build/file.min.js). This function will be passed to
-     * the rename property of files array when building dynamically:
-     * http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically
-     *
-     * @param {String} destPath the current destination
-     * @param {String} srcPath the  matched src path
-     * @return {String} The rewritten destination path.
-     */
-    var babelRename = function(destPath, srcPath) {
-        destPath = srcPath.replace('src', 'build');
-        destPath = destPath.replace('.js', '.min.js');
-        return destPath;
+    return {
+        amdSrc,
+        componentDirectory,
+        cwd,
+        cssSrc,
+        files,
+        fullRunDir,
+        gruntFilePath,
+        inAMD,
+        inComponent,
+        inTheme,
+        relativeCwd,
+        runDir,
+        scssSrc,
+        yuiSrc,
     };
+};
 
-    /**
-     * Find thirdpartylibs.xml and generate an array of paths contained within
-     * them (used to generate ignore files and so on).
-     *
-     * @return {array} The list of thirdparty paths.
-     */
-    var getThirdPartyPathsFromXML = function() {
-        const thirdpartyfiles = ComponentList.getThirdPartyLibsList(gruntFilePath + '/');
-        const libs = ['node_modules/', 'vendor/'];
-
-        thirdpartyfiles.forEach(function(file) {
-            const dirname = path.dirname(file);
-
-            const doc = new DOMParser().parseFromString(grunt.file.read(file));
-            const nodes = xpath.select("/libraries/library/location/text()", doc);
-
-            nodes.forEach(function(node) {
-                let lib = path.posix.join(dirname, node.toString());
-                if (grunt.file.isDir(lib)) {
-                    // Ensure trailing slash on dirs.
-                    lib = lib.replace(/\/?$/, '/');
-                }
-
-                // Look for duplicate paths before adding to array.
-                if (libs.indexOf(lib) === -1) {
-                    libs.push(lib);
-                }
-            });
-        });
-
-        return libs;
-    };
-
-    /**
-     * Get the list of feature files to pass to the gherkin linter.
-     *
-     * @returns {Array}
-     */
-    const getGherkinLintTargets = () => {
-        if (files) {
-            // Specific files were requested. Only check these.
-            return files;
-        }
-
-        if (inComponent) {
-            return [`${runDir}/tests/behat/*.feature`];
-        }
+/**
+ * Verify tha tthe current NodeJS version matches the required version in package.json.
+ *
+ * @param   {Grunt} grunt
+ */
+const verifyNodeVersion = grunt => {
+    const semver = require('semver');
 
-        return ['**/tests/behat/*.feature'];
-    };
+    // Verify the node version is new enough.
+    var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node);
+    var actual = semver.valid(process.version);
+    if (!semver.satisfies(actual, expected)) {
+        grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual);
+    }
+};
 
-    // Project configuration.
-    grunt.initConfig({
-        eslint: {
-            // Even though warnings dont stop the build we don't display warnings by default because
-            // at this moment we've got too many core warnings.
-            // To display warnings call: grunt eslint --show-lint-warnings
-            // To fail on warnings call: grunt eslint --max-lint-warnings=0
-            // Also --max-lint-warnings=-1 can be used to display warnings but not fail.
-            options: {
-                quiet: (!grunt.option('show-lint-warnings')) && (typeof grunt.option('max-lint-warnings') === 'undefined'),
-                maxWarnings: ((typeof grunt.option('max-lint-warnings') !== 'undefined') ? grunt.option('max-lint-warnings') : -1)
-            },
-            amd: {src: files ? files : amdSrc},
-            // Check YUI module source files.
-            yui: {src: files ? files : yuiSrc},
-        },
-        babel: {
-            options: {
-                sourceMaps: true,
-                comments: false,
-                plugins: [
-                    'transform-es2015-modules-amd-lazy',
-                    'system-import-transformer',
-                    // This plugin modifies the Babel transpiling for "export default"
-                    // so that if it's used then only the exported value is returned
-                    // by the generated AMD module.
-                    //
-                    // It also adds the Moodle plugin name to the AMD module definition
-                    // so that it can be imported as expected in other modules.
-                    path.resolve('babel-plugin-add-module-to-define.js'),
-                    '@babel/plugin-syntax-dynamic-import',
-                    '@babel/plugin-syntax-import-meta',
-                    ['@babel/plugin-proposal-class-properties', {'loose': false}],
-                    '@babel/plugin-proposal-json-strings'
-                ],
-                presets: [
-                    ['minify', {
-                        // This minification plugin needs to be disabled because it breaks the
-                        // source map generation and causes invalid source maps to be output.
-                        simplify: false,
-                        builtIns: false
-                    }],
-                    ['@babel/preset-env', {
-                        targets: {
-                            browsers: [
-                                ">0.25%",
-                                "last 2 versions",
-                                "not ie <= 10",
-                                "not op_mini all",
-                                "not Opera > 0",
-                                "not dead"
-                            ]
-                        },
-                        modules: false,
-                        useBuiltIns: false
-                    }]
-                ]
-            },
-            dist: {
-                files: [{
-                    expand: true,
-                    src: files ? files : amdSrc,
-                    rename: babelRename
-                }]
-            }
-        },
-        sass: {
-            dist: {
-                files: {
-                    "theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss",
-                    "theme/classic/style/moodle.css": "theme/classic/scss/classicgrunt.scss"
-                }
-            },
-            options: {
-                implementation: sass,
-                includePaths: ["theme/boost/scss/", "theme/classic/scss/"]
-            }
-        },
-        watch: {
-            options: {
-                nospawn: true // We need not to spawn so config can be changed dynamically.
-            },
-            amd: {
-                files: inComponent
-                    ? ['amd/src/*.js', 'amd/src/**/*.js']
-                    : ['**/amd/src/**/*.js'],
-                tasks: ['amd']
-            },
-            boost: {
-                files: [inComponent ? 'scss/**/*.scss' : 'theme/boost/scss/**/*.scss'],
-                tasks: ['scss']
-            },
-            rawcss: {
-                files: [
-                    '**/*.css',
-                ],
-                excludes: [
-                    '**/moodle.css',
-                    '**/editor.css',
-                ],
-                tasks: ['rawcss']
-            },
-            yui: {
-                files: inComponent
-                    ? ['yui/src/*.json', 'yui/src/**/*.js']
-                    : ['**/yui/src/**/*.js'],
-                tasks: ['yui']
-            },
-            gherkinlint: {
-                files: [inComponent ? 'tests/behat/*.feature' : '**/tests/behat/*.feature'],
-                tasks: ['gherkinlint']
-            }
-        },
-        shifter: {
-            options: {
-                recursive: true,
-                // Shifter takes a relative path.
-                paths: files ? files : [runDir]
-            }
-        },
-        gherkinlint: {
-            options: {
-                files: getGherkinLintTargets(),
-            }
-        },
-    });
+/**
+ * Grunt configuration.
+ *
+ * @param {Grunt} grunt
+ */
+module.exports = function(grunt) {
+    // Verify that the Node version meets our requirements.
+    verifyNodeVersion(grunt);
 
-    /**
-     * Generate ignore files (utilising thirdpartylibs.xml data)
-     */
-    tasks.ignorefiles = function() {
-        // An array of paths to third party directories.
-        const thirdPartyPaths = getThirdPartyPathsFromXML();
-        // Generate .eslintignore.
-        const eslintIgnores = [
-            '# Generated by "grunt ignorefiles"',
-            '*/**/yui/src/*/meta/',
-            '*/**/build/',
-        ].concat(thirdPartyPaths);
-        grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
-
-        // Generate .stylelintignore.
-        const stylelintIgnores = [
-            '# Generated by "grunt ignorefiles"',
-            '**/yui/build/*',
-            'theme/boost/style/moodle.css',
-            'theme/classic/style/moodle.css',
-        ].concat(thirdPartyPaths);
-        grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
-    };
+    // Setup the Moodle environemnt within the Grunt object.
+    grunt.moodleEnv = setupMoodleEnvironment(grunt);
 
     /**
-     * Shifter task. Is configured with a path to a specific file or a directory,
-     * in the case of a specific file it will work out the right module to be built.
+     * Add the named task.
      *
-     * Note that this task runs the invidiaul shifter jobs async (becase it spawns
-     * so be careful to to call done().
+     * @param   {string} name
+     * @param   {Grunt} grunt
      */
-    tasks.shifter = function() {
-        var done = this.async(),
-            options = grunt.config('shifter.options');
-
-        // Run the shifter processes one at a time to avoid confusing output.
-        async.eachSeries(options.paths, function(src, filedone) {
-            var args = [];
-            args.push(path.normalize(__dirname + '/node_modules/shifter/bin/shifter'));
-
-            // Always ignore the node_modules directory.
-            args.push('--excludes', 'node_modules');
-
-            // Determine the most appropriate options to run with based upon the current location.
-            if (grunt.file.isMatch('**/yui/**/*.js', src)) {
-                // When passed a JS file, build our containing module (this happen with
-                // watch).
-                grunt.log.debug('Shifter passed a specific JS file');
-                src = path.dirname(path.dirname(src));
-                options.recursive = false;
-            } else if (grunt.file.isMatch('**/yui/src', src)) {
-                // When in a src directory --walk all modules.
-                grunt.log.debug('In a src directory');
-                args.push('--walk');
-                options.recursive = false;
-            } else if (grunt.file.isMatch('**/yui/src/*', src)) {
-                // When in module, only build our module.
-                grunt.log.debug('In a module directory');
-                options.recursive = false;
-            } else if (grunt.file.isMatch('**/yui/src/*/js', src)) {
-                // When in module src, only build our module.
-                grunt.log.debug('In a source directory');
-                src = path.dirname(src);
-                options.recursive = false;
-            }
+    const addTask = (name, grunt) => {
+        const path = require('path');
+        const taskPath = path.resolve(`./.grunt/tasks/${name}.js`);
 
-            if (grunt.option('watch')) {
-                grunt.fail.fatal('The --watch option has been removed, please use `grunt watch` instead');
-            }
-
-            // Add the stderr option if appropriate
-            if (grunt.option('verbose')) {
-                args.push('--lint-stderr');
-            }
+        grunt.log.debug(`Including tasks for ${name} from ${taskPath}`);
 
-            if (grunt.option('no-color')) {
-                args.push('--color=false');
-            }
-
-            var execShifter = function() {
-
-                grunt.log.ok("Running shifter on " + src);
-                grunt.util.spawn({
-                    cmd: "node",
-                    args: args,
-                    opts: {cwd: src, stdio: 'inherit', env: process.env}
-                }, function(error, result, code) {
-                    if (code) {
-                        grunt.fail.fatal('Shifter failed with code: ' + code);
-                    } else {
-                        grunt.log.ok('Shifter build complete.');
-                        filedone();
-                    }
-                });
-            };
-
-            // Actually run shifter.
-            if (!options.recursive) {
-                execShifter();
-            } else {
-                // Check that there are yui modules otherwise shifter ends with exit code 1.
-                if (grunt.file.expand({cwd: src}, '**/yui/src/**/*.js').length > 0) {
-                    args.push('--recursive');
-                    execShifter();
-                } else {
-                    grunt.log.ok('No YUI modules to build.');
-                    filedone();
-                }
-            }
-        }, done);
-    };
-
-    tasks.gherkinlint = function() {
-        const done = this.async();
-        const options = grunt.config('gherkinlint.options');
-
-        // Grab the gherkin-lint linter and required scaffolding.
-        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.
-        return linter.lint(
-            featureFinder.getFeatureFiles(grunt.file.expand(options.files)),
-            configParser.getConfiguration(configParser.defaultConfigFileName)
-        )
-        .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
+        require(path.resolve(`./.grunt/tasks/${name}.js`))(grunt);
     };
 
-    tasks.startup = function() {
-        // Are we in a YUI directory?
-        if (path.basename(path.resolve(cwd, '../../')) == 'yui') {
-            grunt.task.run('yui');
-        // Are we in an AMD directory?
-        } else if (inAMD) {
-            grunt.task.run('amd');
-        } else {
-            // Run them all!.
-            grunt.task.run('css');
-            grunt.task.run('js');
-            grunt.task.run('gherkinlint');
-        }
-    };
-
-    /**
-     * This is a wrapper task to handle the grunt watch command. It attempts to use
-     * Watchman to monitor for file changes, if it's installed, because it's much faster.
-     *
-     * If Watchman isn't installed then it falls back to the grunt-contrib-watch file
-     * watcher for backwards compatibility.
-     */
-    tasks.watch = function() {
-        var watchTaskDone = this.async();
-        var watchInitialised = false;
-        var watchTaskQueue = {};
-        var processingQueue = false;
-
-        // Grab the tasks and files that have been queued up and execute them.
-        var processWatchTaskQueue = function() {
-            if (!Object.keys(watchTaskQueue).length || processingQueue) {
-                // If there is nothing in the queue or we're already processing then wait.
-                return;
-            }
-
-            processingQueue = true;
-
-            // Grab all tasks currently in the queue.
-            var queueToProcess = watchTaskQueue;
-            // Reset the queue.
-            watchTaskQueue = {};
-
-            async.forEachSeries(
-                Object.keys(queueToProcess),
-                function(task, next) {
-                    var files = queueToProcess[task];
-                    var filesOption = '--files=' + files.join(',');
-                    grunt.log.ok('Running task ' + task + ' for files ' + filesOption);
-
-                    // Spawn the task in a child process so that it doesn't kill this one
-                    // if it failed.
-                    grunt.util.spawn(
-                        {
-                            // Spawn with the grunt bin.
-                            grunt: true,
-                            // Run from current working dir and inherit stdio from process.
-                            opts: {
-                                cwd: fullRunDir,
-                                stdio: 'inherit'
-                            },
-                            args: [task, filesOption]
-                        },
-                        function(err, res, code) {
-                            if (code !== 0) {
-                                // The grunt task failed.
-                                grunt.log.error(err);
-                            }
-
-                            // Move on to the next task.
-                            next();
-                        }
-                    );
-                },
-                function() {
-                    // No longer processing.
-                    processingQueue = false;
-                    // Once all of the tasks are done then recurse just in case more tasks
-                    // were queued while we were processing.
-                    processWatchTaskQueue();
-                }
-            );
-        };
-
-        const originalWatchConfig = grunt.config.get(['watch']);
-        const watchConfig = Object.keys(originalWatchConfig).reduce(function(carry, key) {
-            if (key == 'options') {
-                return carry;
-            }
-
-            const value = originalWatchConfig[key];
-
-            const taskNames = value.tasks;
-            const files = value.files;
-            let excludes = [];
-            if (value.excludes) {
-                excludes = value.excludes;
-            }
-
-            taskNames.forEach(function(taskName) {
-                carry[taskName] = {
-                    files,
-                    excludes,
-                };
-            });
-
-            return carry;
-        }, {});
-
-        watchmanClient.on('error', function(error) {
-            // We have to add an error handler here and parse the error string because the
-            // example way from the docs to check if Watchman is installed doesn't actually work!!
-            // See: https://github.com/facebook/watchman/issues/509
-            if (error.message.match('Watchman was not found')) {
-                // If watchman isn't installed then we should fallback to the other watch task.
-                grunt.log.ok('It is recommended that you install Watchman for better performance using the "watch" command.');
-
-                // Fallback to the old grunt-contrib-watch task.
-                grunt.renameTask('watch-grunt', 'watch');
-                grunt.task.run(['watch']);
-                // This task is finished.
-                watchTaskDone(0);
-            } else {
-                grunt.log.error(error);
-                // Fatal error.
-                watchTaskDone(1);
-            }
-        });
-
-        watchmanClient.on('subscription', function(resp) {
-            if (resp.subscription !== 'grunt-watch') {
-                return;
-            }
-
-            resp.files.forEach(function(file) {
-                grunt.log.ok('File changed: ' + file.name);
-
-                var fullPath = fullRunDir + '/' + file.name;
-                Object.keys(watchConfig).forEach(function(task) {
-
-                    const fileGlobs = watchConfig[task].files;
-                    var match = fileGlobs.some(function(fileGlob) {
-                        return grunt.file.isMatch(`**/${fileGlob}`, fullPath);
-                    });
-
-                    if (match) {
-                        // If we are watching a subdirectory then the file.name will be relative
-                        // to that directory. However the grunt tasks  expect the file paths to be
-                        // relative to the Gruntfile.js location so let's normalise them before
-                        // adding them to the queue.
-                        var relativePath = fullPath.replace(gruntFilePath + '/', '');
-                        if (task in watchTaskQueue) {
-                            if (!watchTaskQueue[task].includes(relativePath)) {
-                                watchTaskQueue[task] = watchTaskQueue[task].concat(relativePath);
-                            }
-                        } else {
-                            watchTaskQueue[task] = [relativePath];
-                        }
-                    }
-                });
-            });
-
-            processWatchTaskQueue();
-        });
-
-        process.on('SIGINT', function() {
-            // Let the user know that they may need to manually stop the Watchman daemon if they
-            // no longer want it running.
-            if (watchInitialised) {
-                grunt.log.ok('The Watchman daemon may still be running and may need to be stopped manually.');
-            }
-
-            process.exit();
-        });
 
-        // Initiate the watch on the current directory.
-        watchmanClient.command(['watch-project', fullRunDir], function(watchError, watchResponse) {
-            if (watchError) {
-                grunt.log.error('Error initiating watch:', watchError);
-                watchTaskDone(1);
-                return;
-            }
-
-            if ('warning' in watchResponse) {
-                grunt.log.error('warning: ', watchResponse.warning);
-            }
-
-            var watch = watchResponse.watch;
-            var relativePath = watchResponse.relative_path;
-            watchInitialised = true;
-
-            watchmanClient.command(['clock', watch], function(clockError, clockResponse) {
-                if (clockError) {
-                    grunt.log.error('Failed to query clock:', clockError);
-                    watchTaskDone(1);
-                    return;
-                }
+    // Add Moodle task configuration.
+    addTask('gherkinlint', grunt);
+    addTask('ignorefiles', grunt);
 
-                // Generate the expression query used by watchman.
-                // Documentation is limited, but see https://facebook.github.io/watchman/docs/expr/allof.html for examples.
-                // We generate an expression to match any value in the files list of all of our tasks, but excluding
-                // all value in the  excludes list of that task.
-                //
-                // [anyof, [
-                //      [allof, [
-                //          [anyof, [
-                //              ['match', validPath, 'wholename'],
-                //              ['match', validPath, 'wholename'],
-                //          ],
-                //          [not,
-                //              [anyof, [
-                //                  ['match', invalidPath, 'wholename'],
-                //                  ['match', invalidPath, 'wholename'],
-                //              ],
-                //          ],
-                //      ],
-                var matchWholeName = fileGlob => ['match', fileGlob, 'wholename'];
-                var matches = Object.keys(watchConfig).map(function(task) {
-                    const matchAll = [];
-                    matchAll.push(['anyof'].concat(watchConfig[task].files.map(matchWholeName)));
-
-                    if (watchConfig[task].excludes.length) {
-                        matchAll.push(['not', ['anyof'].concat(watchConfig[task].excludes.map(matchWholeName))]);
-                    }
-
-                    return ['allof'].concat(matchAll);
-                });
-
-                matches = ['anyof'].concat(matches);
-
-                var sub = {
-                    expression: matches,
-                    // Which fields we're interested in.
-                    fields: ["name", "size", "type"],
-                    // Add our time constraint.
-                    since: clockResponse.clock
-                };
-
-                if (relativePath) {
-                    /* eslint-disable camelcase */
-                    sub.relative_root = relativePath;
-                }
-
-                watchmanClient.command(['subscribe', watch, 'grunt-watch', sub], function(subscribeError) {
-                    if (subscribeError) {
-                        // Probably an error in the subscription criteria.
-                        grunt.log.error('failed to subscribe: ', subscribeError);
-                        watchTaskDone(1);
-                        return;
-                    }
-
-                    grunt.log.ok('Listening for changes to files in ' + fullRunDir);
-                });
-            });
-        });
-    };
+    addTask('javascript', grunt);
+    addTask('style', grunt);
 
-    // On watch, we dynamically modify config to build only affected files. This
-    // method is slightly complicated to deal with multiple changed files at once (copied
-    // from the grunt-contrib-watch readme).
-    var changedFiles = Object.create(null);
-    var onChange = grunt.util._.debounce(function() {
-        var files = Object.keys(changedFiles);
-        grunt.config('eslint.amd.src', files);
-        grunt.config('eslint.yui.src', files);
-        grunt.config('shifter.options.paths', files);
-        grunt.config('gherkinlint.options.files', files);
-        grunt.config('babel.dist.files', [{expand: true, src: files, rename: babelRename}]);
-        changedFiles = Object.create(null);
-    }, 200);
-
-    grunt.event.on('watch', function(action, filepath) {
-        changedFiles[filepath] = action;
-        onChange();
-    });
-
-    // Register NPM tasks.
-    grunt.loadNpmTasks('grunt-contrib-uglify');
-    grunt.loadNpmTasks('grunt-contrib-watch');
-    grunt.loadNpmTasks('grunt-sass');
-    grunt.loadNpmTasks('grunt-eslint');
-    grunt.loadNpmTasks('grunt-stylelint');
-    grunt.loadNpmTasks('grunt-babel');
-
-    // Rename the grunt-contrib-watch "watch" task because we're going to wrap it.
-    grunt.renameTask('watch', 'watch-grunt');
-
-    // Register JS tasks.
-    grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter);
-    grunt.registerTask('gherkinlint', 'Run gherkinlint against the current directory', tasks.gherkinlint);
-    grunt.registerTask('ignorefiles', 'Generate ignore files for linters', tasks.ignorefiles);
-    grunt.registerTask('watch', 'Run tasks on file changes', tasks.watch);
-    grunt.registerTask('yui', ['eslint:yui', 'shifter']);
-    grunt.registerTask('amd', ['eslint:amd', 'babel']);
-    grunt.registerTask('js', ['amd', 'yui']);
-
-    // Register CSS tasks.
-    registerStyleLintTasks(grunt, files, fullRunDir);
-
-    // Register the startup task.
-    grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
+    addTask('watch', grunt);
+    addTask('startup', grunt);
 
     // Register the default task.
     grunt.registerTask('default', ['startup']);
index f1e3f15..1e94044 100644 (file)
@@ -2,7 +2,7 @@ MOODLE TRADEMARKS POLICY
 ========================
 
 The word "Moodle" is trademarked in many countries around the world. The word
-was originally an acronym: Modular Object-Oriented Dynamic Learning
+was originally coined as an acronym: Modular Object-Oriented Dynamic Learning
 Environment. The trademark is owned by Martin Dougiamas, Founder and Director
 of Moodle.
 
index c02458d..1ec045f 100644 (file)
@@ -125,7 +125,7 @@ class task_log_table extends \table_sql {
         }
 
         // TODO Does not support custom user profile fields (MDL-70456).
-        $userfieldsapi = \core\user_fields::for_identity(\context_system::instance(), false)->with_userpic();
+        $userfieldsapi = \core_user\fields::for_identity(\context_system::instance(), false)->with_userpic();
         $userfields = $userfieldsapi->get_sql('u', false, 'user', 'userid2', false)->selects;
 
         $where = '';
index 17e6b65..50557d6 100644 (file)
@@ -271,7 +271,7 @@ if ($roleid) {
     foreach ($assignableroles as $roleid => $notused) {
         $roleusers = '';
         if (0 < $assigncounts[$roleid] && $assigncounts[$roleid] <= MAX_USERS_TO_LIST_PER_ROLE) {
-            $userfieldsapi = \core\user_fields::for_name();
+            $userfieldsapi = \core_user\fields::for_name();
             $userfields = 'u.id, u.username' . $userfieldsapi->get_sql('u')->selects;
             $roleusers = get_role_users($roleid, $context, false, $userfields);
             if (!empty($roleusers)) {
index c0da1aa..86d198e 100644 (file)
@@ -139,6 +139,13 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
         new lang_string('coursehelpshowgrades'), 1, array(0 => new lang_string('no'), 1 => new lang_string('yes'))));
     $temp->add(new admin_setting_configselect('moodlecourse/showreports', new lang_string('showreports'), '', 0,
         array(0 => new lang_string('no'), 1 => new lang_string('yes'))));
+    $temp->add(new admin_setting_configselect('moodlecourse/showactivitydates',
+        new lang_string('showactivitydates'),
+        new lang_string('showactivitydates_help'), 1, [
+            0 => new lang_string('no'),
+            1 => new lang_string('yes')
+        ]
+    ));
 
     // Files and uploads.
     $temp->add(new admin_setting_heading('filesanduploadshdr', new lang_string('filesanduploads'), ''));
@@ -163,6 +170,15 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configselect('moodlecourse/enablecompletion', new lang_string('completion', 'completion'),
         new lang_string('enablecompletion_help', 'completion'), 1, array(0 => new lang_string('no'), 1 => new lang_string('yes'))));
 
+    // Display completion conditions.
+    $temp->add(new admin_setting_configselect('moodlecourse/showcompletionconditions',
+        new lang_string('showcompletionconditions', 'completion'),
+        new lang_string('showcompletionconditions_help', 'completion'), 1, [
+            0 => new lang_string('no'),
+            1 => new lang_string('yes')
+        ]
+    ));
+
     // Groups.
     $temp->add(new admin_setting_heading('groups', new lang_string('groups', 'group'), ''));
     $choices = array();
index 8def3d7..883539b 100644 (file)
@@ -401,55 +401,6 @@ if ($hassiteconfig) {
         /** @var \core\plugininfo\repository $plugin */
         $plugin->load_settings($ADMIN, 'repositorysettings', $hassiteconfig);
     }
-
-/// Web services
-    $ADMIN->add('modules', new admin_category('webservicesettings', new lang_string('webservices', 'webservice')));
-
-    /// overview page
-    $temp = new admin_settingpage('webservicesoverview', new lang_string('webservicesoverview', 'webservice'));
-    $temp->add(new admin_setting_webservicesoverview());
-    $ADMIN->add('webservicesettings', $temp);
-    //API documentation
-    $ADMIN->add('webservicesettings', new admin_externalpage('webservicedocumentation', new lang_string('wsdocapi', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/documentation.php", 'moodle/site:config', false));
-    /// manage service
-    $temp = new admin_settingpage('externalservices', new lang_string('externalservices', 'webservice'));
-    $temp->add(new admin_setting_heading('manageserviceshelpexplaination', new lang_string('information', 'webservice'), new lang_string('servicehelpexplanation', 'webservice')));
-    $temp->add(new admin_setting_manageexternalservices());
-    $ADMIN->add('webservicesettings', $temp);
-    $ADMIN->add('webservicesettings', new admin_externalpage('externalservice', new lang_string('editaservice', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/service.php", 'moodle/site:config', true));
-    $ADMIN->add('webservicesettings', new admin_externalpage('externalservicefunctions', new lang_string('externalservicefunctions', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/service_functions.php", 'moodle/site:config', true));
-    $ADMIN->add('webservicesettings', new admin_externalpage('externalserviceusers', new lang_string('externalserviceusers', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/service_users.php", 'moodle/site:config', true));
-    $ADMIN->add('webservicesettings', new admin_externalpage('externalserviceusersettings', new lang_string('serviceusersettings', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/service_user_settings.php", 'moodle/site:config', true));
-    /// manage protocol page link
-    $temp = new admin_settingpage('webserviceprotocols', new lang_string('manageprotocols', 'webservice'));
-    $temp->add(new admin_setting_managewebserviceprotocols());
-    if (empty($CFG->enablewebservices)) {
-        $temp->add(new admin_setting_heading('webservicesaredisabled', '', new lang_string('disabledwarning', 'webservice')));
-    }
-
-    // We cannot use $OUTPUT this early, doing so means that we lose the ability
-    // to set the page layout on all admin pages.
-    // $wsdoclink = $OUTPUT->doc_link('How_to_get_a_security_key');
-    $url = new moodle_url(get_docs_url('How_to_get_a_security_key'));
-    $wsdoclink = html_writer::tag('a', new lang_string('supplyinfo', 'webservice'), array('href'=>$url));
-    $temp->add(new admin_setting_configcheckbox('enablewsdocumentation', new lang_string('enablewsdocumentation',
-                        'admin'), new lang_string('configenablewsdocumentation', 'admin', $wsdoclink), false));
-    $ADMIN->add('webservicesettings', $temp);
-    /// links to protocol pages
-    $plugins = core_plugin_manager::instance()->get_plugins_of_type('webservice');
-    core_collator::asort_objects_by_property($plugins, 'displayname');
-    foreach ($plugins as $plugin) {
-        /** @var \core\plugininfo\webservice $plugin */
-        $plugin->load_settings($ADMIN, 'webservicesettings', $hassiteconfig);
-    }
-    /// manage token page link
-    $ADMIN->add('webservicesettings', new admin_externalpage('addwebservicetoken', new lang_string('managetokens', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/tokens.php", 'moodle/site:config', true));
-    $temp = new admin_settingpage('webservicetokens', new lang_string('managetokens', 'webservice'));
-    $temp->add(new admin_setting_managewebservicetokens());
-    if (empty($CFG->enablewebservices)) {
-        $temp->add(new admin_setting_heading('webservicesaredisabled', '', new lang_string('disabledwarning', 'webservice')));
-    }
-    $ADMIN->add('webservicesettings', $temp);
 }
 
 // Question type settings
index 179cc98..75e72ed 100644 (file)
@@ -546,4 +546,67 @@ if ($hassiteconfig) {
             new lang_string('updatenotifybuilds_desc', 'core_admin'), 0));
         $ADMIN->add('server', $temp);
     }
+
+    // Web services.
+    $ADMIN->add('server', new admin_category('webservicesettings', new lang_string('webservices', 'webservice')));
+
+    // Web services > Overview.
+    $temp = new admin_settingpage('webservicesoverview', new lang_string('webservicesoverview', 'webservice'));
+    $temp->add(new admin_setting_webservicesoverview());
+    $ADMIN->add('webservicesettings', $temp);
+
+    // Web services > API documentation.
+    $ADMIN->add('webservicesettings', new admin_externalpage('webservicedocumentation', new lang_string('wsdocapi', 'webservice'),
+        "{$CFG->wwwroot}/{$CFG->admin}/webservice/documentation.php", 'moodle/site:config', false));
+
+    // Web services > External services.
+    $temp = new admin_settingpage('externalservices', new lang_string('externalservices', 'webservice'));
+
+    $temp->add(new admin_setting_heading('manageserviceshelpexplaination', new lang_string('information', 'webservice'),
+        new lang_string('servicehelpexplanation', 'webservice')));
+
+    $temp->add(new admin_setting_manageexternalservices());
+
+    $ADMIN->add('webservicesettings', $temp);
+
+    $ADMIN->add('webservicesettings', new admin_externalpage('externalservice', new lang_string('editaservice', 'webservice'),
+        "{$CFG->wwwroot}/{$CFG->admin}/webservice/service.php", 'moodle/site:config', true));
+
+    $ADMIN->add('webservicesettings', new admin_externalpage('externalservicefunctions',
+        new lang_string('externalservicefunctions', 'webservice'), "{$CFG->wwwroot}/{$CFG->admin}/webservice/service_functions.php",
+        'moodle/site:config', true));
+
+    $ADMIN->add('webservicesettings', new admin_externalpage('externalserviceusers',
+        new lang_string('externalserviceusers', 'webservice'), "{$CFG->wwwroot}/{$CFG->admin}/webservice/service_users.php",
+        'moodle/site:config', true));
+
+    $ADMIN->add('webservicesettings', new admin_externalpage('externalserviceusersettings',
+        new lang_string('serviceusersettings', 'webservice'), "{$CFG->wwwroot}/{$CFG->admin}/webservice/service_user_settings.php",
+        'moodle/site:config', true));
+
+    // Web services > Manage protocols.
+    $temp = new admin_settingpage('webserviceprotocols', new lang_string('manageprotocols', 'webservice'));
+    $temp->add(new admin_setting_managewebserviceprotocols());
+    if (empty($CFG->enablewebservices)) {
+        $temp->add(new admin_setting_heading('webservicesaredisabled', '', new lang_string('disabledwarning', 'webservice')));
+    }
+
+    // We cannot use $OUTPUT->doc_link() this early, we would lose the ability to set the page layout on all admin pages.
+    $url = new moodle_url(get_docs_url('How_to_get_a_security_key'));
+    $wsdoclink = html_writer::link($url, new lang_string('supplyinfo', 'webservice'), ['target' => '_blank']);
+    $temp->add(new admin_setting_configcheckbox('enablewsdocumentation', new lang_string('enablewsdocumentation', 'admin'),
+        new lang_string('configenablewsdocumentation', 'admin', $wsdoclink), false));
+
+    $ADMIN->add('webservicesettings', $temp);
+
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('webservice');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
+        /** @var \core\plugininfo\webservice $plugin */
+        $plugin->load_settings($ADMIN, 'webservicesettings', $hassiteconfig);
+    }
+
+    // Web services > Manage tokens.
+    $ADMIN->add('webservicesettings', new admin_externalpage('webservicetokens', new lang_string('managetokens', 'webservice'),
+        new moodle_url('/admin/webservice/tokens.php')));
 }
diff --git a/admin/tests/behat/browse_users.feature b/admin/tests/behat/browse_users.feature
new file mode 100644 (file)
index 0000000..b6d9f41
--- /dev/null
@@ -0,0 +1,50 @@
+@core @core_admin
+Feature: An administrator can browse user accounts
+  In order to find the user accounts I am looking for
+  As an admin
+  I can browse users and see their basic information
+
+  Background:
+    Given the following "custom profile fields" exist:
+      | datatype | shortname | name           |
+      | text     | frog      | Favourite frog |
+    And the following "users" exist:
+      | username | firstname | lastname | email           | department | profile_field_frog | firstnamephonetic |
+      | user1    | User      | One      | one@example.com | Attack     | Kermit             | Yewzer            |
+      | user2    | User      | Two      | two@example.com | Defence    | Tree               | Yoozare           |
+    And I log in as "admin"
+
+  Scenario: User accounts display default fields
+    When I navigate to "Users > Accounts > Browse list of users" in site administration
+    # Name field always present, email field is default for showidentity.
+    Then the following should exist in the "users" table:
+      | First name / Surname | Email address   |
+      | User One             | one@example.com |
+      | User Two             | two@example.com |
+    # Should not see other identity fields or non-default name fields.
+    And I should not see "Department" in the "table" "css_element"
+    And I should not see "Attack"
+    And I should not see "Favourite frog" in the "table" "css_element"
+    And I should not see "Kermit"
+    And I should not see "First name - phonetic" in the "table" "css_element"
+    And I should not see "Yoozare"
+
+  Scenario: User accounts with extra name fields
+    Given the following config values are set as admin:
+      | alternativefullnameformat | firstnamephonetic lastname |
+    When I navigate to "Users > Accounts > Browse list of users" in site administration
+    Then the following should exist in the "users" table:
+      | First name - phonetic / Surname | Email address   |
+      | Yewzer One                      | one@example.com |
+      | Yoozare Two                     | two@example.com |
+
+  Scenario: User accounts with specified identity fields
+    Given the following config values are set as admin:
+      | showuseridentity | department,profile_field_frog |
+    When I navigate to "Users > Accounts > Browse list of users" in site administration
+    Then the following should exist in the "users" table:
+      | First name / Surname | Favourite frog  | Department |
+      | User One             | Kermit          | Attack     |
+      | User Two             | Tree            | Defence    |
+    And I should not see "Email address" in the "table" "css_element"
+    And I should not see "one@example.com"
index dc1cd94..e155ded 100644 (file)
@@ -1,27 +1,76 @@
 @core @core_admin
-Feature: Manage tokens
-  In order to manage webservice usage
+Feature: Manage external services tokens
+  In order to manage external service usage
   As an admin
-  I need to be able to create and delete tokens
+  I need to be able to create, filter and delete tokens
 
   Background:
     Given the following "users" exist:
-    | username  | password  | firstname | lastname |
-    | testuser  | testuser  | Joe | Bloggs |
-    | testuser2 | testuser2 | TestFirstname | TestLastname |
+      | username  | password  | firstname     | lastname    |
+      | user1     | user1     | Firstname1    | Lastname1   |
+      | user2     | user2     | Firstname2    | Lastname2   |
+      | user3     | user3     | Firstname3    | Lastname3   |
+      | user4     | user4     | Firstname4    | Lastname4   |
     And I change window size to "small"
-    And I log in as "admin"
-    And I am on site homepage
 
   @javascript
-  Scenario: Add & delete a token
-    Given I navigate to "Plugins > Web services > Manage tokens" in site administration
-    And I follow "Add"
-    And I set the field "User" to "Joe Bloggs"
+  Scenario: Add a token to user identified by name and then delete that token
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Server > Web services > Manage tokens" in site administration
+    And I press "Create token"
+    And I set the field "User" to "Firstname1 Lastname1"
+    And I set the field "Service" to "Moodle mobile web service"
     And I set the field "IP restriction" to "127.0.0.1"
     When I press "Save changes"
-    Then I should see "Joe Bloggs"
-    And I should see "127.0.0.1"
-    And I follow "Delete"
+    Then I should see "Moodle mobile web service" in the "Firstname1 Lastname1" "table_row"
+    And I should see "127.0.0.1" in the "Firstname1 Lastname1" "table_row"
+    And I click on "Delete" "link" in the "Firstname1 Lastname1" "table_row"
+    And I should see "Do you really want to delete this web service token for Firstname1 Lastname1 on the service Moodle mobile web service?"
     And I press "Delete"
-    And I should not see "Joe Bloggs"
+    And "Firstname1 Lastname1" "table_row" should not exist
+
+  @javascript
+  Scenario: Tokens can be filtered by user and by service
+    Given the following "core_webservice > Service" exists:
+      | name      | Site information              |
+      | shortname | siteinfo                      |
+      | enabled   | 1                             |
+    And the following "core_webservice > Service function" exists:
+      | service   | siteinfo                      |
+      | functions | core_webservice_get_site_info |
+    And the following "core_webservice > Tokens" exist:
+      | user      | service                       |
+      | user2     | siteinfo                      |
+      | user3     | moodle_mobile_app             |
+      | user4     | siteinfo                      |
+    When I log in as "admin"
+    And I navigate to "Server > Web services > Manage tokens" in site administration
+
+    # All created tokens are shown by default.
+    And "Firstname1 Lastname1" "table_row" should not exist
+    And I should see "Site information" in the "Firstname2 Lastname2" "table_row"
+    And I should see "Moodle mobile web service" in the "Firstname3 Lastname3" "table_row"
+    And I should see "Site information" in the "Firstname4 Lastname4" "table_row"
+
+    # Filter tokens by user (note we can select the user by the identity field here).
+    When I click on "Tokens filter" "link"
+    And I set the field "User" to "user2@example.com"
+    And I press "Show only matching tokens"
+    Then "Firstname3 Lastname3" "table_row" should not exist
+    And "Firstname4 Lastname4" "table_row" should not exist
+    And I should see "Site information" in the "Firstname2 Lastname2" "table_row"
+
+    # Reset the filter.
+    And I press "Show all tokens"
+    And I should see "Site information" in the "Firstname2 Lastname2" "table_row"
+    And I should see "Moodle mobile web service" in the "Firstname3 Lastname3" "table_row"
+    And I should see "Site information" in the "Firstname4 Lastname4" "table_row"
+
+    # Filter tokens by service.
+    And I click on "Tokens filter" "link"
+    And I set the field "Service" to "Site information"
+    And I press "Show only matching tokens"
+    And I should see "Site information" in the "Firstname2 Lastname2" "table_row"
+    And I should see "Site information" in the "Firstname4 Lastname4" "table_row"
+    And "Firstname3 Lastname3" "table_row" should not exist
diff --git a/admin/tests/behat/webservice_users.feature b/admin/tests/behat/webservice_users.feature
new file mode 100644 (file)
index 0000000..e334b0f
--- /dev/null
@@ -0,0 +1,31 @@
+@core @core_admin
+Feature: Web service user settings
+  In order to configure authorised users for a web service
+  As an admin
+  I need to use the page that lets you do that
+
+  Background:
+    # Include a custom profile field so we can check it gets displayed
+    Given the following "custom profile fields" exist:
+      | datatype | shortname | name           | param2 |
+      | text     | frog      | Favourite frog | 100    |
+    And the following config values are set as admin:
+      | showuseridentity | email,profile_field_frog |
+    And the following "users" exist:
+      | username | firstname | lastname | email         | profile_field_frog |
+      | user1    | User      | One      | 1@example.org | Kermit             |
+    And the following "core_webservice > Service" exists:
+      | name            | Silly service |
+      | shortname       | silly         |
+      | restrictedusers | 1             |
+      | enabled         | 1             |
+
+  Scenario: Add a user to a web service
+    When I log in as "admin"
+    And I navigate to "Server > Web services > External services" in site administration
+    And I click on "Authorised users" "link" in the "Silly service" "table_row"
+    And I set the field "Not authorised users" to "User One"
+    And I press "Add"
+    Then I should see "User One" in the ".alloweduserlist" "css_element"
+    And I should see "1@example.org" in the ".alloweduserlist" "css_element"
+    And I should see "Kermit" in the ".alloweduserlist" "css_element"
index 1e15acc..59799ac 100644 (file)
@@ -21,5 +21,5 @@ Feature: Transform date time string arguments
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
     And I follow "Test assignment name"
-    And I should see "##yesterday##l, j F Y##"
+    And I should see "##yesterday##%A, %d %B %Y##"
     And I log out
index 71b4b94..50563c4 100644 (file)
@@ -149,7 +149,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_get_config_file_contents_with_single_run() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
+        $mockbuilder->onlyMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
@@ -188,7 +188,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_get_config_file_contents_with_single_run_no_theme() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
+        $mockbuilder->onlyMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
@@ -242,7 +242,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_get_config_file_contents_with_parallel_run() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
+        $mockbuilder->onlyMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
@@ -345,7 +345,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_get_config_file_contents_with_parallel_run_optimize_tags() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
+        $mockbuilder->onlyMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
@@ -489,7 +489,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_get_config_file_contents_with_blacklisted_tags() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_blacklisted_tests_for_theme',
+        $mockbuilder->onlyMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_blacklisted_tests_for_theme',
             'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
@@ -553,7 +553,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_get_config_file_contents_with_blacklisted_features_contexts() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_blacklisted_tests_for_theme',
+        $mockbuilder->onlyMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_blacklisted_tests_for_theme',
             'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
@@ -632,7 +632,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_core_features_to_include_in_specified_theme() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
+        $mockbuilder->onlyMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
index 630f8a8..6172ef5 100644 (file)
@@ -127,7 +127,7 @@ class cohort_role_assignments_table extends table_sql {
      */
     protected function define_table_columns() {
         // TODO Does not support custom user profile fields (MDL-70456).
-        $extrafields = \core\user_fields::get_identity_fields($this->context, false);
+        $extrafields = \core_user\fields::get_identity_fields($this->context, false);
 
         // Define headers and columns.
         $cols = array(
@@ -175,7 +175,7 @@ class cohort_role_assignments_table extends table_sql {
 
         // Add extra user fields that we need for the graded user.
         // TODO Does not support custom user profile fields (MDL-70456).
-        $userfieldsapi = \core\user_fields::for_identity($this->context, false)->with_name();
+        $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_name();
         $fields .= $userfieldsapi->get_sql('u')->selects;
 
         if ($count) {
@@ -188,7 +188,12 @@ class cohort_role_assignments_table extends table_sql {
                    FROM {tool_cohortroles} uca
                    JOIN {user} u ON u.id = uca.userid
                    JOIN {cohort} c ON c.id = uca.cohortid";
-        $params = array();
+
+        // Check if any additional filtering is required.
+        [$sqlwhere, $params] = $this->get_sql_where();
+        if ($sqlwhere) {
+            $sql .= " WHERE {$sqlwhere}";
+        }
 
         // Add order by if needed.
         if (!$count && $sqlsort = $this->get_sql_sort()) {
index cc9947c..3396ddb 100644 (file)
@@ -86,8 +86,8 @@ if ($removeid) {
 
     $title = get_string('existingcohortroles', 'tool_cohortroles');
     echo $output->heading($title);
-    $url = new moodle_url('/admin/tool/cohortroles/index.php');
-    $table = new tool_cohortroles\output\cohort_role_assignments_table(uniqid(), $url);
+
+    $table = new tool_cohortroles\output\cohort_role_assignments_table('cohort-role-assignments', $pageurl);
     echo $table->out(50, true);
 
     echo $output->spacer();
index ceaee01..4547319 100644 (file)
@@ -189,7 +189,7 @@ class api {
         $dpos = [];
         $context = context_system::instance();
         foreach ($dporoles as $roleid) {
-            $userfieldsapi = \core\user_fields::for_name();
+            $userfieldsapi = \core_user\fields::for_name();
             $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
             $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
                       'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
index c2ab731..92dd398 100644 (file)
@@ -700,7 +700,7 @@ class external extends external_api {
         self::validate_context($context);
         require_capability('tool/dataprivacy:managedatarequests', $context);
 
-        $userfieldsapi = \core\user_fields::for_name();
+        $userfieldsapi = \core_user\fields::for_name();
         $allusernames = $userfieldsapi->get_sql('', false, '', '', false)->selects;
         // Exclude admins and guest user.
         $excludedusers = array_keys(get_admins()) + [guest_user()->id];
@@ -708,7 +708,7 @@ class external extends external_api {
         $fields = 'id,' . $allusernames;
 
         // TODO Does not support custom user profile fields (MDL-70456).
-        $extrafields = \core\user_fields::get_identity_fields($context, false);
+        $extrafields = \core_user\fields::get_identity_fields($context, false);
         if (!empty($extrafields)) {
             $fields .= ',' . implode(',', $extrafields);
         }
index b34cb8f..d05087e 100644 (file)
@@ -187,7 +187,7 @@ class helper {
         global $DB;
 
         // Get users that the user has role assignments to.
-        $userfieldsapi = \core\user_fields::for_name();
+        $userfieldsapi = \core_user\fields::for_name();
         $allusernames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
         $sql = "SELECT u.id, $allusernames
                   FROM {role_assignments} ra, {context} c, {user} u
index 39c3e3c..af2f3aa 100644 (file)
@@ -62,7 +62,7 @@ class tool_dataprivacy_data_request_form extends \core\form\persistent {
                 'valuehtmlcallback' => function($value) {
                     global $OUTPUT;
 
-                    $userfieldsapi = \core\user_fields::for_name();
+                    $userfieldsapi = \core_user\fields::for_name();
                     $allusernames = $userfieldsapi->get_sql('', false, '', '', false)->selects;
                     $fields = 'id, email, ' . $allusernames;
                     $user = \core_user::get_user($value, $fields);
index 4cb3450..8e41409 100644 (file)
@@ -133,7 +133,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0, $secondcomment);
         $this->assertTrue($result);
         $datarequest = new data_request($requestid);
-        $this->assertRegExp("/.*{$comment}.*{$secondcomment}/s", $datarequest->get('dpocomment'));
+        $this->assertMatchesRegularExpression("/.*{$comment}.*{$secondcomment}/s", $datarequest->get('dpocomment'));
 
         // Update with a valid status.
         $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_DOWNLOAD_READY);
@@ -237,9 +237,9 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         // There should only be one PO role.
         $this->assertCount(1, $roleids);
         // Confirm it contains the manager role.
-        $this->assertContains($managerroleid, $roleids);
+        $this->assertContainsEquals($managerroleid, $roleids);
         // And it does not contain the editing teacher role.
-        $this->assertNotContains($editingteacherroleid, $roleids);
+        $this->assertNotContainsEquals($editingteacherroleid, $roleids);
     }
 
     /**
@@ -838,7 +838,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
             $this->assertCount($filteredcount, $filteredrequests);
             // Confirm the filtered requests match the status filter(s).
             foreach ($filteredrequests as $request) {
-                $this->assertContains($request->get('status'), $statuses);
+                $this->assertContainsEquals($request->get('status'), $statuses);
             }
 
             if ($numstatus > 1) {
index 9af4b87..8cf8d5d 100644 (file)
@@ -32,18 +32,18 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 return new class extends phpunit_coverage_info {
-    /** @var array The list of folders relative to the plugin root to whitelist in coverage generation. */
-    protected $whitelistfolders = [
+    /** @var array The list of folders relative to the plugin root to include in coverage generation. */
+    protected $includelistfolders = [
         'classes',
     ];
 
-    /** @var array The list of files relative to the plugin root to whitelist in coverage generation. */
-    protected $whitelistfiles = [];
+    /** @var array The list of files relative to the plugin root to include in coverage generation. */
+    protected $includelistfiles = [];
 
-    /** @var array The list of folders relative to the plugin root to excludelist in coverage generation. */
+    /** @var array The list of folders relative to the plugin root to exclude from coverage generation. */
     protected $excludelistfolders = [
     ];
 
-    /** @var array The list of files relative to the plugin root to excludelist in coverage generation. */
+    /** @var array The list of files relative to the plugin root to exclude from coverage generation. */
     protected $excludelistfiles = [];
 };
index 6deb919..863cb82 100644 (file)
@@ -857,7 +857,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $expiredcontext->save();
 
         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
-            ->setMethods([
+            ->onlyMethods([
                 'delete_data_for_user',
                 'delete_data_for_users_in_context',
                 'delete_data_for_all_users_in_context',
@@ -868,7 +868,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $mockprivacymanager->expects($this->never())->method('delete_data_for_users_in_context');
 
         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
-            ->setMethods(['get_privacy_manager'])
+            ->onlyMethods(['get_privacy_manager'])
             ->getMock();
 
         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
@@ -935,7 +935,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $expiredcontext->save();
 
         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
-            ->setMethods([
+            ->onlyMethods([
                 'delete_data_for_user',
                 'delete_data_for_users_in_context',
                 'delete_data_for_all_users_in_context',
@@ -950,13 +950,13 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
                 $userids = $forumlist->get_userids();
                 $this->assertCount(1, $userids);
-                $this->assertContains($student->id, $userids);
-                $this->assertNotContains($teacher->id, $userids);
+                $this->assertContainsEquals($student->id, $userids);
+                $this->assertNotContainsEquals($teacher->id, $userids);
                 return true;
             }));
 
         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
-            ->setMethods(['get_privacy_manager'])
+            ->onlyMethods(['get_privacy_manager'])
             ->getMock();
 
         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
@@ -1023,7 +1023,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $expiredcontext->save();
 
         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
-            ->setMethods([
+            ->onlyMethods([
                 'delete_data_for_user',
                 'delete_data_for_users_in_context',
                 'delete_data_for_all_users_in_context',
@@ -1038,13 +1038,13 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
                 $userids = $forumlist->get_userids();
                 $this->assertCount(1, $userids);
-                $this->assertContains($student->id, $userids);
-                $this->assertNotContains($teacher->id, $userids);
+                $this->assertContainsEquals($student->id, $userids);
+                $this->assertNotContainsEquals($teacher->id, $userids);
                 return true;
             }));
 
         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
-            ->setMethods(['get_privacy_manager'])
+            ->onlyMethods(['get_privacy_manager'])
             ->getMock();
 
         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
@@ -1112,7 +1112,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $expiredcontext->save();
 
         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
-            ->setMethods([
+            ->onlyMethods([
                 'delete_data_for_user',
                 'delete_data_for_users_in_context',
                 'delete_data_for_all_users_in_context',
@@ -1127,13 +1127,13 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
                 $userids = $forumlist->get_userids();
                 $this->assertCount(1, $userids);
-                $this->assertContains($student->id, $userids);
-                $this->assertNotContains($teacher->id, $userids);
+                $this->assertContainsEquals($student->id, $userids);
+                $this->assertNotContainsEquals($teacher->id, $userids);
                 return true;
             }));
 
         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
-            ->setMethods(['get_privacy_manager'])
+            ->onlyMethods(['get_privacy_manager'])
             ->getMock();
 
         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
@@ -1208,7 +1208,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $expiredcontext->save();
 
         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
-            ->setMethods([
+            ->onlyMethods([
                 'delete_data_for_user',
                 'delete_data_for_users_in_context',
                 'delete_data_for_all_users_in_context',
@@ -1223,13 +1223,13 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
                 $forumlist = $userlist->get_userlist_for_component('mod_forum');
                 $userids = $forumlist->get_userids();
                 $this->assertCount(2, $userids);
-                $this->assertContains($student->id, $userids);
-                $this->assertContains($teacher->id, $userids);
+                $this->assertContainsEquals($student->id, $userids);
+                $this->assertContainsEquals($teacher->id, $userids);
                 return true;
             }));
 
         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
-            ->setMethods(['get_privacy_manager'])
+            ->onlyMethods(['get_privacy_manager'])
             ->getMock();
 
         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
@@ -1260,7 +1260,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $expiredcontext->save();
 
         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
-            ->setMethods([
+            ->onlyMethods([
                 'delete_data_for_user',
                 'delete_data_for_all_users_in_context',
             ])
@@ -1269,7 +1269,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
 
         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
-            ->setMethods(['get_privacy_manager'])
+            ->onlyMethods(['get_privacy_manager'])
             ->getMock();
         $manager->set_progress(new \null_progress_trace());
 
@@ -1299,7 +1299,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $expiredcontext->save();
 
         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
-            ->setMethods([
+            ->onlyMethods([
                 'delete_data_for_user',
                 'delete_data_for_all_users_in_context',
             ])
@@ -1308,7 +1308,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
 
         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
-            ->setMethods(['get_privacy_manager'])
+            ->onlyMethods(['get_privacy_manager'])
             ->getMock();
         $manager->set_progress(new \null_progress_trace());
 
@@ -1335,7 +1335,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $expiredcontext->save();
 
         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
-            ->setMethods([
+            ->onlyMethods([
                 'delete_data_for_user',
                 'delete_data_for_all_users_in_context',
             ])
@@ -1344,7 +1344,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
 
         $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
-            ->setMethods(['get_privacy_manager'])
+            ->onlyMethods(['get_privacy_manager'])
             ->getMock();
         $manager->set_progress(new \null_progress_trace());
 
@@ -1382,7 +1382,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $expiredcontext->save();
 
         $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
-            ->setMethods([
+            ->onlyMethods([
                 'delete_data_for_user',
                 'delete_data_for_all_users_in_context',
             ])