Merge branch 'MDL-69415-master' of git://github.com/sarjona/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Fri, 16 Apr 2021 03:25:55 +0000 (11:25 +0800)
committerVíctor Déniz <victor@moodle.com>
Fri, 16 Apr 2021 06:57:01 +0000 (07:57 +0100)
788 files changed:
.eslintignore
.eslintrc
.github/workflows/push.yml
.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]
.stylelintignore
.travis.yml
Gruntfile.js
TRADEMARK.txt
admin/classes/task_log_table.php
admin/oauth2callback.php
admin/purgecaches.php
admin/roles/assign.php
admin/settings/courses.php
admin/tests/behat/browse_users.feature [new file with mode: 0644]
admin/tests/behat/webservice_users.feature [new file with mode: 0644]
admin/tool/cohortroles/classes/output/cohort_role_assignments_table.php
admin/tool/cohortroles/index.php
admin/tool/dataprivacy/amd/build/contactdpo.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/contactdpo.min.js.map [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/myrequestactions.min.js
admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map
admin/tool/dataprivacy/amd/src/contactdpo.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/myrequestactions.js
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/form/contactdpo.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/classes/output/renderer.php
admin/tool/dataprivacy/createdatarequest_form.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/templates/contact_dpo.mustache [deleted file]
admin/tool/dataprivacy/tests/behat/contact_privacy_officer.feature
admin/tool/dataprivacy/tests/behat/my_data_requests.feature [new file with mode: 0644]
admin/tool/httpsreplace/tests/httpsreplace_test.php
admin/tool/lp/classes/external.php
admin/tool/lp/classes/output/template_plans_table.php
admin/tool/oauth2/issuers.php
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/oauth2/tests/behat/basic_settings.feature
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/classes/manager.php
admin/user.php
admin/user/user_bulk_cohortadd.php
admin/user/user_bulk_display.php
availability/classes/info.php
availability/condition/completion/tests/behat/availability_completion.feature
availability/condition/profile/classes/condition.php
availability/condition/profile/classes/frontend.php
availability/tests/info_test.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/util/ui/backup_ui_stage.class.php
backup/util/ui/tests/behat/import_contentbank_content.feature
badges/assertion.php
badges/backpack-connect.php
badges/backpack.js [deleted file]
badges/badge.php
badges/classes/assertion.php
badges/classes/form/external_backpack.php
badges/classes/oauth2/client.php
badges/classes/output/external_badge.php
badges/classes/output/issued_badge.php
badges/criteria/award_criteria_cohort.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/behat/award_badge.feature
badges/tests/behat/criteria_activity.feature
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
blog/locallib.php
blog/rsslib.php
calendar/classes/privacy/provider.php
calendar/lib.php
calendar/tests/event_vault_test.php
calendar/tests/externallib_test.php
calendar/tests/local_api_test.php
calendar/tests/privacy_test.php
comment/lib.php
comment/locallib.php
completion/classes/cm_completion_details.php [new file with mode: 0644]
completion/tests/behat/behat_completion.php
completion/tests/behat/completion_course_page_display.feature [moved from completion/tests/behat/completion_course_page_checkboxes.feature with 67% similarity]
completion/tests/behat/custom_completion_display_conditions.feature [new file with mode: 0644]
completion/tests/behat/enable_manual_complete_mark.feature
completion/tests/behat/restrict_section_availability.feature
completion/tests/cm_completion_details_test.php [new file with mode: 0644]
completion/upgrade.txt
contentbank/amd/build/upload.min.js [new file with mode: 0644]
contentbank/amd/build/upload.min.js.map [new file with mode: 0644]
contentbank/amd/src/upload.js [new file with mode: 0644]
contentbank/classes/form/upload_files.php [new file with mode: 0644]
contentbank/contenttype/h5p/classes/content.php
contentbank/contenttype/h5p/classes/contenttype.php
contentbank/contenttype/h5p/tests/behat/admin_replace_content.feature
contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature
contentbank/contenttype/h5p/tests/behat/disable_contenttypes.feature [new file with mode: 0644]
contentbank/contenttype/h5p/tests/behat/manage_content.feature
contentbank/contenttype/h5p/tests/behat/teacher_replace_content.feature
contentbank/contenttype/h5p/tests/behat/teacher_upload_content.feature
contentbank/contenttype/h5p/tests/content_h5p_test.php
contentbank/files_form.php [deleted file]
contentbank/index.php
contentbank/templates/bankcontent/toolbar.mustache
contentbank/tests/behat/delete_content.feature
contentbank/tests/behat/download_content.feature
contentbank/tests/behat/edit_content.feature
contentbank/tests/behat/events.feature
contentbank/tests/behat/search_content.feature
contentbank/tests/behat/sort_content.feature
contentbank/tests/behat/view_preferences.feature
contentbank/tests/behat/visibility.feature
contentbank/tests/contentbank_test.php
contentbank/upgrade.txt
contentbank/upload.php [deleted file]
contentbank/view.php
course/amd/build/events.min.js
course/amd/build/events.min.js.map
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/build/view.min.js [new file with mode: 0644]
course/amd/build/view.min.js.map [new file with mode: 0644]
course/amd/src/events.js
course/amd/src/manual_completion_toggle.js [new file with mode: 0644]
course/amd/src/repository.js
course/amd/src/view.js [new file with mode: 0644]
course/classes/category.php
course/classes/output/activity_information.php [new file with mode: 0644]
course/classes/output/cm_format.php
course/classes/output/section_format/cmitem.php
course/completion.js [deleted file]
course/edit_form.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
course/templates/local/section_format/cmitem.mustache
course/tests/behat/behat_course.php
course/tests/behat/course_activity_dates.feature [new file with mode: 0644]
course/togglecompletion.php
course/upgrade.txt
course/view.php
enrol/ajax.php
enrol/externallib.php
enrol/locallib.php
enrol/manual/classes/enrol_users_form.php
enrol/otherusers.php
enrol/self/lib.php
enrol/self/locallib.php
files/tests/converter_test.php
filter/displayh5p/tests/behat/h5p_filter.feature
filter/tex/mimetex.darwin
filter/tex/mimetex.exe
filter/tex/mimetex.freebsd
filter/tex/mimetex.linux
filter/tex/mimetex.linux.aarch64
filter/tex/readme_moodle.txt
filter/tex/thirdpartylibs.xml
grade/grading/form/guide/db/services.php
grade/grading/form/rubric/db/services.php
grade/grading/manage.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/report/user/externallib.php
grade/tests/reportlib_test.php
group/autogroup.php
group/index.php
group/lib.php
group/overview.php
group/tests/behat/custom_fields.feature [new file with mode: 0644]
group/tests/lib_test.php
h5p/classes/api.php
h5p/classes/core.php
h5p/classes/editor_ajax.php
h5p/classes/framework.php
h5p/classes/helper.php
h5p/classes/output/libraries.php
h5p/classes/player.php
h5p/libraries.php
h5p/templates/h5plibraries.mustache
h5p/tests/api_test.php
h5p/tests/behat/h5p_libraries.feature
h5p/tests/editor_framework_test.php
h5p/tests/generator/lib.php
h5p/tests/generator_test.php
h5p/upgrade.txt
install/lang/hat/admin.php
install/lang/he/langconfig.php
install/lang/it/install.php
install/lang/mk/admin.php
install/lang/pt/install.php
install/lang/sr_cr/install.php
install/lang/sr_lt/install.php
install/lang/sv/error.php
install/lang/sv/langconfig.php
lang/en/badges.php
lang/en/completion.php
lang/en/contentbank.php
lang/en/course.php
lang/en/deprecated.txt
lang/en/h5p.php
lang/en/moodle.php
lib/accesslib.php
lib/adminlib.php
lib/adodb/LICENSE.md
lib/adodb/README.md
lib/adodb/adodb-active-record.inc.php
lib/adodb/adodb-active-recordx.inc.php
lib/adodb/adodb-csvlib.inc.php
lib/adodb/adodb-datadict.inc.php
lib/adodb/adodb-error.inc.php
lib/adodb/adodb-errorhandler.inc.php
lib/adodb/adodb-errorpear.inc.php
lib/adodb/adodb-exceptions.inc.php
lib/adodb/adodb-iterator.inc.php
lib/adodb/adodb-lib.inc.php
lib/adodb/adodb-loadbalancer.inc.php [new file with mode: 0644]
lib/adodb/adodb-memcache.lib.inc.php
lib/adodb/adodb-pager.inc.php
lib/adodb/adodb-pear.inc.php
lib/adodb/adodb-perf.inc.php
lib/adodb/adodb-php4.inc.php
lib/adodb/adodb-time.inc.php
lib/adodb/adodb-xmlschema.inc.php
lib/adodb/adodb-xmlschema03.inc.php
lib/adodb/adodb.inc.php
lib/adodb/datadict/datadict-access.inc.php
lib/adodb/datadict/datadict-db2.inc.php
lib/adodb/datadict/datadict-firebird.inc.php
lib/adodb/datadict/datadict-generic.inc.php
lib/adodb/datadict/datadict-ibase.inc.php
lib/adodb/datadict/datadict-informix.inc.php
lib/adodb/datadict/datadict-mssql.inc.php
lib/adodb/datadict/datadict-mssqlnative.inc.php
lib/adodb/datadict/datadict-mysql.inc.php
lib/adodb/datadict/datadict-oci8.inc.php
lib/adodb/datadict/datadict-postgres.inc.php
lib/adodb/datadict/datadict-sapdb.inc.php
lib/adodb/datadict/datadict-sqlite.inc.php
lib/adodb/datadict/datadict-sybase.inc.php
lib/adodb/drivers/adodb-access.inc.php
lib/adodb/drivers/adodb-ado.inc.php
lib/adodb/drivers/adodb-ado5.inc.php
lib/adodb/drivers/adodb-ado_access.inc.php
lib/adodb/drivers/adodb-ado_mssql.inc.php
lib/adodb/drivers/adodb-ads.inc.php
lib/adodb/drivers/adodb-borland_ibase.inc.php
lib/adodb/drivers/adodb-csv.inc.php
lib/adodb/drivers/adodb-db2.inc.php
lib/adodb/drivers/adodb-db2oci.inc.php
lib/adodb/drivers/adodb-db2ora.inc.php
lib/adodb/drivers/adodb-fbsql.inc.php
lib/adodb/drivers/adodb-firebird.inc.php
lib/adodb/drivers/adodb-ibase.inc.php
lib/adodb/drivers/adodb-informix.inc.php
lib/adodb/drivers/adodb-informix72.inc.php
lib/adodb/drivers/adodb-ldap.inc.php
lib/adodb/drivers/adodb-mssql.inc.php
lib/adodb/drivers/adodb-mssql_n.inc.php
lib/adodb/drivers/adodb-mssqlnative.inc.php
lib/adodb/drivers/adodb-mssqlpo.inc.php
lib/adodb/drivers/adodb-mysql.inc.php
lib/adodb/drivers/adodb-mysqli.inc.php
lib/adodb/drivers/adodb-mysqlpo.inc.php
lib/adodb/drivers/adodb-mysqlt.inc.php
lib/adodb/drivers/adodb-netezza.inc.php
lib/adodb/drivers/adodb-oci8.inc.php
lib/adodb/drivers/adodb-oci805.inc.php
lib/adodb/drivers/adodb-oci8po.inc.php
lib/adodb/drivers/adodb-oci8quercus.inc.php
lib/adodb/drivers/adodb-odbc.inc.php
lib/adodb/drivers/adodb-odbc_db2.inc.php
lib/adodb/drivers/adodb-odbc_mssql.inc.php
lib/adodb/drivers/adodb-odbc_mssql2012.inc.php [new file with mode: 0644]
lib/adodb/drivers/adodb-odbc_oracle.inc.php
lib/adodb/drivers/adodb-odbtp.inc.php
lib/adodb/drivers/adodb-odbtp_unicode.inc.php
lib/adodb/drivers/adodb-oracle.inc.php
lib/adodb/drivers/adodb-pdo.inc.php
lib/adodb/drivers/adodb-pdo_dblib.inc.php [new file with mode: 0644]
lib/adodb/drivers/adodb-pdo_firebird.inc.php [new file with mode: 0644]
lib/adodb/drivers/adodb-pdo_mssql.inc.php
lib/adodb/drivers/adodb-pdo_mysql.inc.php
lib/adodb/drivers/adodb-pdo_oci.inc.php
lib/adodb/drivers/adodb-pdo_pgsql.inc.php
lib/adodb/drivers/adodb-pdo_sqlite.inc.php
lib/adodb/drivers/adodb-pdo_sqlsrv.inc.php
lib/adodb/drivers/adodb-postgres.inc.php
lib/adodb/drivers/adodb-postgres64.inc.php
lib/adodb/drivers/adodb-postgres7.inc.php
lib/adodb/drivers/adodb-postgres8.inc.php
lib/adodb/drivers/adodb-postgres9.inc.php
lib/adodb/drivers/adodb-proxy.inc.php
lib/adodb/drivers/adodb-sapdb.inc.php
lib/adodb/drivers/adodb-sqlanywhere.inc.php
lib/adodb/drivers/adodb-sqlite.inc.php
lib/adodb/drivers/adodb-sqlite3.inc.php
lib/adodb/drivers/adodb-sqlitepo.inc.php
lib/adodb/drivers/adodb-sybase.inc.php
lib/adodb/drivers/adodb-sybase_ase.inc.php
lib/adodb/drivers/adodb-text.inc.php [new file with mode: 0644]
lib/adodb/drivers/adodb-vfp.inc.php
lib/adodb/perf/perf-db2.inc.php
lib/adodb/perf/perf-informix.inc.php
lib/adodb/perf/perf-mssql.inc.php
lib/adodb/perf/perf-mssqlnative.inc.php
lib/adodb/perf/perf-mysql.inc.php
lib/adodb/perf/perf-oci8.inc.php
lib/adodb/perf/perf-postgres.inc.php
lib/adodb/pivottable.inc.php
lib/adodb/readme_moodle.txt
lib/adodb/rsfilter.inc.php
lib/adodb/toexport.inc.php
lib/adodb/tohtml.inc.php
lib/adodb/xmlschema03.dtd
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/build/toast.min.js.map
lib/amd/src/modal.js
lib/amd/src/templates.js
lib/amd/src/toast.js
lib/antivirus/clamav/settings.php
lib/authlib.php
lib/badgeslib.php
lib/behat/form_field/behat_form_date.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/event/base.php
lib/classes/navigation/views/primary.php [new file with mode: 0644]
lib/classes/navigation/views/secondary.php
lib/classes/navigation/views/view.php
lib/classes/oauth2/api.php
lib/classes/oauth2/issuer.php
lib/classes/oauth2/service/facebook.php [new file with mode: 0644]
lib/classes/oauth2/service/imsobv2p1.php
lib/classes/oauth2/service/microsoft.php [new file with mode: 0644]
lib/classes/oauth2/service/nextcloud.php [new file with mode: 0644]
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/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.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/filelib.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/grouplib.php
lib/modinfolib.php
lib/moodlelib.php
lib/myprofilelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/pagelib.php
lib/plagiarismlib.php
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/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/tablelib.php
lib/templates/local/toast/message.mustache
lib/testing/generator/data_generator.php
lib/tests/behat/app_behat_runtime.js
lib/tests/behat/datetime_any.feature [new file with mode: 0644]
lib/tests/completionlib_test.php
lib/tests/datalib_test.php
lib/tests/event/contentbank_content_uploaded_test.php
lib/tests/moodlelib_test.php
lib/tests/navigation/views/primary_test.php [new file with mode: 0644]
lib/tests/navigation/views/secondary_test.php [moved from lib/tests/secondary_test.php with 80% similarity]
lib/tests/outputcomponents_test.php
lib/tests/portfoliolib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
media/player/videojs/amd/build/loader.min.js
media/player/videojs/amd/build/loader.min.js.map
media/player/videojs/amd/src/loader.js
media/player/videojs/db/services.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/lib.php
message/output/airnotifier/checkconfiguration.php [new file with mode: 0644]
message/output/airnotifier/classes/manager.php
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/airnotifier/settings.php
message/output/airnotifier/tests/manager_test.php [new file with mode: 0644]
message/output/email/classes/task/send_email_task.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/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/renderer.php
mod/assign/tests/custom_completion_test.php [new file with mode: 0644]
mod/book/view.php
mod/chat/lib.php
mod/chat/view.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/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/feedback/view.php
mod/folder/view.php
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/privacy/provider.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/behat/completion_condition_number_discussions.feature
mod/forum/tests/custom_completion_test.php [new file with mode: 0644]
mod/forum/tests/privacy_provider_test.php
mod/forum/view.php
mod/glossary/classes/completion/custom_completion.php [new file with mode: 0644]
mod/glossary/classes/entry_query_builder.php
mod/glossary/db/services.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/view.php
mod/h5pactivity/db/services.php
mod/h5pactivity/mod_form.php
mod/h5pactivity/tests/behat/attempt_options.feature [new file with mode: 0644]
mod/h5pactivity/tests/behat/result_longfillin.feature
mod/h5pactivity/view.php
mod/imscp/view.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/renderer.php
mod/lesson/tests/behat/completion_condition_end_reached.feature
mod/lesson/tests/behat/completion_condition_time_spent.feature
mod/lesson/tests/custom_completion_test.php [new file with mode: 0644]
mod/lti/tests/behat/lti_activity_completion.feature [new file with mode: 0644]
mod/lti/view.php
mod/page/tests/behat/page_activity_completion.feature [new file with mode: 0644]
mod/page/view.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/renderer.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/behat/attempt_basic.feature
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_minimum_attempts.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/behat/editing_add_from_question_bank.feature
mod/quiz/tests/behat/editing_add_random.feature
mod/quiz/tests/behat/preview.feature
mod/quiz/tests/custom_completion_test.php [new file with mode: 0644]
mod/quiz/tests/lib_test.php
mod/resource/locallib.php
mod/scorm/classes/completion/custom_completion.php [new file with mode: 0644]
mod/scorm/lang/en/scorm.php
mod/scorm/mod_form.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/view.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/behat/survey_completion.feature
mod/survey/tests/custom_completion_test.php [new file with mode: 0644]
mod/survey/view.php
mod/url/locallib.php
mod/url/tests/behat/url_activity_completion.feature [new file with mode: 0644]
mod/wiki/renderer.php
mod/wiki/tests/behat/wiki_activity_completion.feature [new file with mode: 0644]
mod/workshop/allocation/manual/lib.php
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/renderer.php
mod/workshop/tests/behat/workshop_activity_completion.feature [new file with mode: 0644]
mod/workshop/tests/behat/workshop_assessment.feature
mod/workshop/view.php
payment/gateway/paypal/db/services.php
plagiarism/upgrade.txt
question/classes/bank/creator_name_column.php
question/classes/bank/modifier_name_column.php
question/question.php
question/type/ddwtos/tests/behat/add.feature
question/type/edit_question_form.php
question/type/essay/question.php
question/type/essay/renderer.php
question/type/essay/tests/question_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/multichoice/edit_multichoice_form.php
question/type/multichoice/questiontype.php
question/type/multichoice/tests/behat/add.feature
question/type/questiontypebase.php
question/type/shortanswer/edit_shortanswer_form.php
question/type/shortanswer/questiontype.php
question/type/shortanswer/tests/behat/add.feature
question/type/upgrade.txt
rating/classes/external.php
rating/index.php
rating/lib.php
report/completion/index.php
report/configlog/classes/output/report_table.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/progress/tests/behat/activity_completion_report.feature
report/stats/locallib.php
repository/contentbank/tests/browser_test.php
repository/contentbank/tests/search_test.php
repository/googledocs/classes/googledocs_content.php [new file with mode: 0644]
repository/googledocs/classes/googledocs_content_search.php [new file with mode: 0644]
repository/googledocs/classes/helper.php [new file with mode: 0644]
repository/googledocs/classes/local/browser/googledocs_drive_content.php [new file with mode: 0644]
repository/googledocs/classes/local/browser/googledocs_root_content.php [new file with mode: 0644]
repository/googledocs/classes/local/browser/googledocs_shared_drives_content.php [new file with mode: 0644]
repository/googledocs/classes/local/node/file_node.php [new file with mode: 0644]
repository/googledocs/classes/local/node/folder_node.php [new file with mode: 0644]
repository/googledocs/classes/local/node/node.php [new file with mode: 0644]
repository/googledocs/classes/rest.php
repository/googledocs/lang/en/repository_googledocs.php
repository/googledocs/lib.php
repository/googledocs/tests/googledocs_content_testcase.php [new file with mode: 0644]
repository/googledocs/tests/googledocs_search_content_test.php [new file with mode: 0644]
repository/googledocs/tests/helper_test.php [new file with mode: 0644]
repository/googledocs/tests/local/browser/googledocs_drive_content_test.php [new file with mode: 0644]
repository/googledocs/tests/local/browser/googledocs_root_content_test.php [new file with mode: 0644]
repository/googledocs/tests/local/browser/googledocs_shared_drives_content_test.php [new file with mode: 0644]
repository/googledocs/tests/local/node/file_node_test.php [new file with mode: 0644]
repository/googledocs/tests/local/node/folder_node_test.php [new file with mode: 0644]
repository/googledocs/tests/repository_googledocs_testcase.php [new file with mode: 0644]
repository/googledocs/version.php
repository/upgrade.txt
search/classes/engine.php
security.txt
tag/classes/manage_table.php
theme/boost/amd/build/bootstrap/alert.min.js
theme/boost/amd/build/bootstrap/alert.min.js.map
theme/boost/amd/build/bootstrap/button.min.js
theme/boost/amd/build/bootstrap/button.min.js.map
theme/boost/amd/build/bootstrap/carousel.min.js
theme/boost/amd/build/bootstrap/carousel.min.js.map
theme/boost/amd/build/bootstrap/collapse.min.js
theme/boost/amd/build/bootstrap/collapse.min.js.map
theme/boost/amd/build/bootstrap/dropdown.min.js
theme/boost/amd/build/bootstrap/dropdown.min.js.map
theme/boost/amd/build/bootstrap/index.min.js [deleted file]
theme/boost/amd/build/bootstrap/index.min.js.map [deleted file]
theme/boost/amd/build/bootstrap/modal.min.js
theme/boost/amd/build/bootstrap/modal.min.js.map
theme/boost/amd/build/bootstrap/popover.min.js
theme/boost/amd/build/bootstrap/popover.min.js.map
theme/boost/amd/build/bootstrap/scrollspy.min.js
theme/boost/amd/build/bootstrap/scrollspy.min.js.map
theme/boost/amd/build/bootstrap/tab.min.js
theme/boost/amd/build/bootstrap/tab.min.js.map
theme/boost/amd/build/bootstrap/toast.min.js
theme/boost/amd/build/bootstrap/toast.min.js.map
theme/boost/amd/build/bootstrap/tools/sanitizer.min.js.map
theme/boost/amd/build/bootstrap/tooltip.min.js
theme/boost/amd/build/bootstrap/tooltip.min.js.map
theme/boost/amd/build/bootstrap/util.min.js.map
theme/boost/amd/build/index.min.js [new file with mode: 0644]
theme/boost/amd/build/index.min.js.map [new file with mode: 0644]
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/src/bootstrap/alert.js
theme/boost/amd/src/bootstrap/button.js
theme/boost/amd/src/bootstrap/carousel.js
theme/boost/amd/src/bootstrap/collapse.js
theme/boost/amd/src/bootstrap/dropdown.js
theme/boost/amd/src/bootstrap/index.js [deleted file]
theme/boost/amd/src/bootstrap/modal.js
theme/boost/amd/src/bootstrap/popover.js
theme/boost/amd/src/bootstrap/scrollspy.js
theme/boost/amd/src/bootstrap/tab.js
theme/boost/amd/src/bootstrap/toast.js
theme/boost/amd/src/bootstrap/tools/sanitizer.js
theme/boost/amd/src/bootstrap/tooltip.js
theme/boost/amd/src/bootstrap/util.js
theme/boost/amd/src/index.js [new file with mode: 0644]
theme/boost/amd/src/loader.js
theme/boost/readme_moodle.txt
theme/boost/scss/bootstrap/_alert.scss
theme/boost/scss/bootstrap/_breadcrumb.scss
theme/boost/scss/bootstrap/_card.scss
theme/boost/scss/bootstrap/_carousel.scss
theme/boost/scss/bootstrap/_custom-forms.scss
theme/boost/scss/bootstrap/_dropdown.scss
theme/boost/scss/bootstrap/_functions.scss
theme/boost/scss/bootstrap/_grid.scss
theme/boost/scss/bootstrap/_input-group.scss
theme/boost/scss/bootstrap/_list-group.scss
theme/boost/scss/bootstrap/_modal.scss
theme/boost/scss/bootstrap/_nav.scss
theme/boost/scss/bootstrap/_navbar.scss
theme/boost/scss/bootstrap/_pagination.scss
theme/boost/scss/bootstrap/_progress.scss
theme/boost/scss/bootstrap/_reboot.scss
theme/boost/scss/bootstrap/_root.scss
theme/boost/scss/bootstrap/_spinners.scss
theme/boost/scss/bootstrap/_toasts.scss
theme/boost/scss/bootstrap/_type.scss
theme/boost/scss/bootstrap/_variables.scss
theme/boost/scss/bootstrap/bootstrap-grid.scss
theme/boost/scss/bootstrap/bootstrap-reboot.scss
theme/boost/scss/bootstrap/bootstrap.scss
theme/boost/scss/bootstrap/mixins/_border-radius.scss
theme/boost/scss/bootstrap/mixins/_forms.scss
theme/boost/scss/bootstrap/mixins/_grid-framework.scss
theme/boost/scss/bootstrap/mixins/_grid.scss
theme/boost/scss/bootstrap/mixins/_image.scss
theme/boost/scss/bootstrap/mixins/_screen-reader.scss
theme/boost/scss/bootstrap/mixins/_transition.scss
theme/boost/scss/bootstrap/utilities/_borders.scss
theme/boost/scss/bootstrap/utilities/_text.scss
theme/boost/scss/moodle.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/tables.scss
theme/boost/scss/moodle/toasts.scss [new file with mode: 0644]
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/thirdpartylibs.xml
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
user/action_redir.php
user/amd/build/participants.min.js
user/amd/build/participants.min.js.map
user/amd/src/participants.js
user/classes/external/search_identity.php
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_search.php
user/editlib.php
user/index.php
user/lib.php
user/selector/lib.php
user/tests/behat/filter_participants.feature
user/tests/fields_test.php [moved from lib/tests/user_fields_test.php with 84% similarity]
userpix/index.php
version.php
webservice/classes/token_filter.php
webservice/classes/token_form.php
webservice/classes/token_table.php
webservice/lib.php
webservice/renderer.php

index 0d0a6ea..2262b1f 100644 (file)
@@ -1,4 +1,5 @@
 # Generated by "grunt ignorefiles"
+!/.grunt
 */**/yui/src/*/meta/
 */**/build/
 node_modules/
@@ -83,7 +84,6 @@ theme/boost/amd/src/bootstrap/button.js
 theme/boost/amd/src/bootstrap/carousel.js
 theme/boost/amd/src/bootstrap/collapse.js
 theme/boost/amd/src/bootstrap/dropdown.js
-theme/boost/amd/src/bootstrap/index.js
 theme/boost/amd/src/bootstrap/modal.js
 theme/boost/amd/src/bootstrap/popover.js
 theme/boost/amd/src/bootstrap/tools/sanitizer.js
@@ -92,4 +92,5 @@ theme/boost/amd/src/bootstrap/tab.js
 theme/boost/amd/src/bootstrap/toast.js
 theme/boost/amd/src/bootstrap/tooltip.js
 theme/boost/amd/src/bootstrap/util.js
-theme/boost/scss/fontawesome/
\ No newline at end of file
+theme/boost/amd/src/index.js
+theme/boost/scss/fontawesome/
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 35fd4a7..0c4f1ba 100644 (file)
@@ -1,6 +1,12 @@
 name: Core
 
-on: [push]
+on:
+  push:
+    branches-ignore:
+      - master
+      - MOODLE_[0-9]+_STABLE
+    tags-ignore:
+      - v[0-9]+.[0-9]+.[0-9]+*
 
 env:
   php: 7.4
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 0207e41..2d28f80 100644 (file)
@@ -84,7 +84,6 @@ theme/boost/amd/src/bootstrap/button.js
 theme/boost/amd/src/bootstrap/carousel.js
 theme/boost/amd/src/bootstrap/collapse.js
 theme/boost/amd/src/bootstrap/dropdown.js
-theme/boost/amd/src/bootstrap/index.js
 theme/boost/amd/src/bootstrap/modal.js
 theme/boost/amd/src/bootstrap/popover.js
 theme/boost/amd/src/bootstrap/tools/sanitizer.js
@@ -93,4 +92,5 @@ theme/boost/amd/src/bootstrap/tab.js
 theme/boost/amd/src/bootstrap/toast.js
 theme/boost/amd/src/bootstrap/tooltip.js
 theme/boost/amd/src/bootstrap/util.js
-theme/boost/scss/fontawesome/
\ No newline at end of file
+theme/boost/amd/src/index.js
+theme/boost/scss/fontawesome/
index 0c565f3..6d52469 100644 (file)
@@ -19,6 +19,12 @@ services:
 addons:
   postgresql: "9.6"
 
+branches:
+  except:
+    - master
+    - /MOODLE_[0-9]+_STABLE/
+    - /^v[0-9]+\.[0-9]+\.[0-9]+.*/
+
 jobs:
     # Enable fast finish.
     # This will fail the build if a single job fails (except those in allow_failures).
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 709e28c..c0b5572 100644 (file)
@@ -34,11 +34,12 @@ $error = optional_param('error', '', PARAM_RAW);
 if ($error) {
     $message = optional_param('error_description', '', PARAM_RAW);
     if ($message) {
-        print_error($message);
+        $SESSION->loginerrormsg = $message;
+        redirect(new moodle_url(get_login_url()));
     } else {
-        print_error($error);
+        $SESSION->loginerrormsg = $error;
+        redirect(new moodle_url(get_login_url()));
     }
-    die();
 }
 
 // The authorization code generated by the authorization server.
@@ -53,5 +54,6 @@ if (isset($params['sesskey']) and confirm_sesskey($params['sesskey'])) {
     $redirecturl->param('oauth2code', $code);
     redirect($redirecturl);
 } else {
-    print_error('invalidsesskey');
+    $SESSION->loginerrormsg = get_string('invalidsesskey', 'error');
+    redirect(new moodle_url(get_login_url()));
 }
index b4401c4..b43aa77 100644 (file)
@@ -52,8 +52,14 @@ if ($data = $form->get_data()) {
     $message = get_string('purgecachesfinished', 'admin');
 }
 
+// Redirect and/or show notification message confirming cache(s) were purged.
 if (isset($message)) {
-    redirect($returnurl, $message);
+    if (!$PAGE->url->compare($returnurl, URL_MATCH_BASE)) {
+        redirect($returnurl, $message);
+    }
+
+    // We are already on the purge caches page, add the notification.
+    \core\notification::add($message, \core\output\notification::NOTIFY_INFO);
 }
 
 // Otherwise, show a form to actually purge the caches.
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();
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"
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 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();
diff --git a/admin/tool/dataprivacy/amd/build/contactdpo.min.js b/admin/tool/dataprivacy/amd/build/contactdpo.min.js
new file mode 100644 (file)
index 0000000..7351478
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/contactdpo.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/contactdpo.min.js.map b/admin/tool/dataprivacy/amd/build/contactdpo.min.js.map
new file mode 100644 (file)
index 0000000..6445af7
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/contactdpo.min.js.map differ
index a6ba759..b1b7317 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/myrequestactions.min.js and b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js differ
index c9f78ed..4dfdc98 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map and b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map differ
diff --git a/admin/tool/dataprivacy/amd/src/contactdpo.js b/admin/tool/dataprivacy/amd/src/contactdpo.js
new file mode 100644 (file)
index 0000000..50417a8
--- /dev/null
@@ -0,0 +1,67 @@
+// 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/>.
+
+/**
+ * Javascript module for contacting the site DPO
+ *
+ * @module      tool_dataprivacy/contactdpo
+ * @package     tool_dataprivacy
+ * @copyright   2021 Paul Holden <paulh@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import ModalForm from 'core_form/modalform';
+import Notification from 'core/notification';
+import {get_string as getString} from 'core/str';
+import {add as addToast} from 'core/toast';
+
+const SELECTORS = {
+    CONTACT_DPO: '[data-action="contactdpo"]',
+};
+
+/**
+ * Initialize module
+ */
+export const init = () => {
+    const triggerElement = document.querySelector(SELECTORS.CONTACT_DPO);
+
+    triggerElement.addEventListener('click', event => {
+        event.preventDefault();
+
+        const modalForm = new ModalForm({
+            modalConfig: {
+                title: getString('contactdataprotectionofficer', 'tool_dataprivacy'),
+            },
+            formClass: 'tool_dataprivacy\\form\\contactdpo',
+            saveButtonText: getString('send', 'tool_dataprivacy'),
+            returnFocus: triggerElement,
+        });
+
+        // Show a toast notification when the form is submitted.
+        modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {
+            if (event.detail.result) {
+                getString('requestsubmitted', 'tool_dataprivacy').then(addToast).catch();
+            } else {
+                const warningMessages = event.detail.warnings.map(warning => warning.message);
+                Notification.addNotification({
+                    type: 'error',
+                    message: warningMessages.join('<br>')
+                });
+            }
+        });
+
+        modalForm.show();
+    });
+};
index 54f94fb..b75b386 100644 (file)
  * @copyright  2018 Jun Pataleta
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define([
-    'jquery',
-    'core/ajax',
-    'core/notification',
-    'core/str',
-    'core/modal_factory',
-    'core/modal_events',
-    'core/templates',
-    'core/pending'],
-function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Pending) {
 
-    /**
-     * List of action selectors.
-     *
-     * @type {{CANCEL_REQUEST: string}}
-     * @type {{CONTACT_DPO: string}}
-     */
-    var ACTIONS = {
-        CANCEL_REQUEST: '[data-action="cancel"]',
-        CONTACT_DPO: '[data-action="contactdpo"]',
-    };
+import Ajax from 'core/ajax';
+import Notification from 'core/notification';
+import Pending from 'core/pending';
+import {get_strings as getStrings} from 'core/str';
 
-    /**
-     * MyRequestActions class.
-     */
-    var MyRequestActions = function() {
-        this.registerEvents();
-    };
+const SELECTORS = {
+    CANCEL_REQUEST: '[data-action="cancel"][data-requestid]',
+};
 
-    /**
-     * Register event listeners.
-     */
-    MyRequestActions.prototype.registerEvents = function() {
-        $(ACTIONS.CANCEL_REQUEST).click(function(e) {
-            e.preventDefault();
-
-            var requestId = $(this).data('requestid');
-            var stringkeys = [
-                {
-                    key: 'cancelrequest',
-                    component: 'tool_dataprivacy'
-