Merge branch 'MDL-71090' of git://github.com/paulholden/moodle
authorSara Arjona <sara@moodle.com>
Thu, 15 Apr 2021 09:56:40 +0000 (11:56 +0200)
committerSara Arjona <sara@moodle.com>
Thu, 15 Apr 2021 09:56:40 +0000 (11:56 +0200)
729 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/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/classes/api.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/createdatarequest_form.php
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
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/files_form.php [deleted file]
contentbank/index.php
contentbank/templates/bankcontent/toolbar.mustache
contentbank/tests/behat/edit_content.feature
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/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
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/course.php
lang/en/deprecated.txt
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/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/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/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/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/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 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();
index ceaee01..4547319 100644 (file)
@@ -189,7 +189,7 @@ class api {
         $dpos = [];
         $context = context_system::instance();
         foreach ($dporoles as $roleid) {
-            $userfieldsapi = \core\user_fields::for_name();
+            $userfieldsapi = \core_user\fields::for_name();
             $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
             $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
                       'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
index d9e5a41..30d2e6d 100644 (file)
@@ -701,7 +701,7 @@ class external extends external_api {
         self::validate_context($context);
         require_capability('tool/dataprivacy:managedatarequests', $context);
 
-        $userfieldsapi = \core\user_fields::for_name();
+        $userfieldsapi = \core_user\fields::for_name();
         $allusernames = $userfieldsapi->get_sql('', false, '', '', false)->selects;
         // Exclude admins and guest user.
         $excludedusers = array_keys(get_admins()) + [guest_user()->id];
@@ -709,7 +709,7 @@ class external extends external_api {
         $fields = 'id,' . $allusernames;
 
         // TODO Does not support custom user profile fields (MDL-70456).
-        $extrafields = \core\user_fields::get_identity_fields($context, false);
+        $extrafields = \core_user\fields::get_identity_fields($context, false);
         if (!empty($extrafields)) {
             $fields .= ',' . implode(',', $extrafields);
         }
index b34cb8f..d05087e 100644 (file)
@@ -187,7 +187,7 @@ class helper {
         global $DB;
 
         // Get users that the user has role assignments to.
-        $userfieldsapi = \core\user_fields::for_name();
+        $userfieldsapi = \core_user\fields::for_name();
         $allusernames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
         $sql = "SELECT u.id, $allusernames
                   FROM {role_assignments} ra, {context} c, {user} u
index 39c3e3c..af2f3aa 100644 (file)
@@ -62,7 +62,7 @@ class tool_dataprivacy_data_request_form extends \core\form\persistent {
                 'valuehtmlcallback' => function($value) {
                     global $OUTPUT;
 
-                    $userfieldsapi = \core\user_fields::for_name();
+                    $userfieldsapi = \core_user\fields::for_name();
                     $allusernames = $userfieldsapi->get_sql('', false, '', '', false)->selects;
                     $fields = 'id, email, ' . $allusernames;
                     $user = \core_user::get_user($value, $fields);
index fb60090..afc5cac 100644 (file)
@@ -145,7 +145,7 @@ class httpsreplace_test extends \advanced_testcase {
         $this->resetAfterTest();
         $this->expectOutputRegex($ouputregex);
 
-        $finder = new tool_httpreplace_url_finder_test();
+        $finder = new tool_httpreplace_url_finder_mock();
 
         $generator = $this->getDataGenerator();
         $course = $generator->create_course((object) [
@@ -216,7 +216,7 @@ class httpsreplace_test extends \advanced_testcase {
     public function test_http_link_stats($content, $domain, $expectedcount) {
         $this->resetAfterTest();
 
-        $finder = new tool_httpreplace_url_finder_test();
+        $finder = new tool_httpreplace_url_finder_mock();
 
         $generator = $this->getDataGenerator();
         $course = $generator->create_course((object) [
@@ -237,7 +237,7 @@ class httpsreplace_test extends \advanced_testcase {
         $this->resetAfterTest();
         $this->expectOutputRegex('/^$/');
 
-        $finder = new tool_httpreplace_url_finder_test();
+        $finder = new tool_httpreplace_url_finder_mock();
 
         $generator = $this->getDataGenerator();
         $course = $generator->create_course((object) [
@@ -269,7 +269,7 @@ class httpsreplace_test extends \advanced_testcase {
         $CFG->wwwroot = preg_replace('/^https:/', 'http:', $CFG->wwwroot);
         $this->expectOutputRegex('/^$/');
 
-        $finder = new tool_httpreplace_url_finder_test();
+        $finder = new tool_httpreplace_url_finder_mock();
 
         $generator = $this->getDataGenerator();
         $course = $generator->create_course((object) [
@@ -292,7 +292,7 @@ class httpsreplace_test extends \advanced_testcase {
 
         set_config('test_upgrade_http_links', '<img src="http://somesite/someimage.png" />');
 
-        $finder = new tool_httpreplace_url_finder_test();
+        $finder = new tool_httpreplace_url_finder_mock();
         ob_start();
         $results = $finder->upgrade_http_links();
         $output = ob_get_contents();
@@ -318,7 +318,7 @@ class httpsreplace_test extends \advanced_testcase {
 
         set_config('renames', json_encode($renames), 'tool_httpsreplace');
 
-        $finder = new tool_httpreplace_url_finder_test();
+        $finder = new tool_httpreplace_url_finder_mock();
 
         $generator = $this->getDataGenerator();
         $course = $generator->create_course((object) [
@@ -353,7 +353,7 @@ class httpsreplace_test extends \advanced_testcase {
             $original2 .= '<img src="http://example.com/image' . ($i + 15 ) . '.png">';
             $expected2 .= '<img src="https://example.com/image' . ($i + 15) . '.png">';
         }
-        $finder = new tool_httpreplace_url_finder_test();
+        $finder = new tool_httpreplace_url_finder_mock();
 
         $generator = $this->getDataGenerator();
         $course1 = $generator->create_course((object) ['summary' => $original1]);
@@ -397,7 +397,7 @@ class httpsreplace_test extends \advanced_testcase {
         $columnamequoted = $dbman->generator->getEncQuoted('where');
         $DB->execute("INSERT INTO {reserved_words_temp} ($columnamequoted) VALUES (?)", [$content]);
 
-        $finder = new tool_httpreplace_url_finder_test();
+        $finder = new tool_httpreplace_url_finder_mock();
         $finder->upgrade_http_links();
 
         $record = $DB->get_record('reserved_words_temp', []);
@@ -408,13 +408,13 @@ class httpsreplace_test extends \advanced_testcase {
 }
 
 /**
- * Class tool_httpreplace_url_finder_test for testing replace tool without calling curl
+ * Class tool_httpreplace_url_finder_mock for testing replace tool without calling curl
  *
  * @package   tool_httpsreplace
  * @copyright 2017 Marina Glancy
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class tool_httpreplace_url_finder_test extends \tool_httpsreplace\url_finder {
+class tool_httpreplace_url_finder_mock extends \tool_httpsreplace\url_finder {
     /**
      * Check if url is available (check hardcoded for unittests)
      *
index 0264bd0..b04ab76 100644 (file)
@@ -879,9 +879,9 @@ class external extends external_api {
             $USER->id, SQL_PARAMS_NAMED);
 
         // TODO Does not support custom user profile fields (MDL-70456).
-        $userfieldsapi = \core\user_fields::for_identity($context, false)->with_userpic();
+        $userfieldsapi = \core_user\fields::for_identity($context, false)->with_userpic();
         $fields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
-        $extrasearchfields = $userfieldsapi->get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);
+        $extrasearchfields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
 
         list($wheresql, $whereparams) = users_search_sql($query, 'u', true, $extrasearchfields);
         list($sortsql, $sortparams) = users_order_by_sql('u', $query, $context);
index ba17116..83829f6 100644 (file)
@@ -93,7 +93,7 @@ class template_plans_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(
@@ -137,7 +137,7 @@ class template_plans_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) {
index cc5da40..6383ef2 100644 (file)
@@ -53,7 +53,7 @@ if ($action == 'edit') {
     if ($issuer) {
         $PAGE->navbar->add(get_string('editissuer', 'tool_oauth2', s($issuer->get('name'))));
     } else {
-        $PAGE->navbar->add(get_string('createnewservice', 'tool_oauth2') . get_string('custom_service', 'tool_oauth2'));
+        $PAGE->navbar->add(get_string('createnewservice', 'tool_oauth2') . ' ' . get_string('custom_service', 'tool_oauth2'));
     }
 
     $showrequireconfirm = false;
@@ -96,7 +96,7 @@ if ($mform && $mform->is_cancelled()) {
         if ($issuer) {
             echo $OUTPUT->heading(get_string('editissuer', 'tool_oauth2', s($issuer->get('name'))));
         } else {
-            echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . get_string('custom_service', 'tool_oauth2'));
+            echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . ' ' . get_string('custom_service', 'tool_oauth2'));
         }
         $mform->display();
         echo $OUTPUT->footer();
@@ -115,7 +115,7 @@ if ($mform && $mform->is_cancelled()) {
         redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
     } else {
         echo $OUTPUT->header();
-        echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . get_string($type . '_service', 'tool_oauth2'));
+        echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . ' ' . get_string($type . '_service', 'tool_oauth2'));
         $mform->display();
         echo $OUTPUT->footer();
     }
@@ -130,9 +130,9 @@ if ($mform && $mform->is_cancelled()) {
     $mform = new \tool_oauth2\form\issuer(null, ['persistent' => $issuer, 'type' => $type,
         'showrequireconfirm' => $showrequireconfirm]);
 
-    $PAGE->navbar->add(get_string('createnewservice', 'tool_oauth2') . get_string($type . '_service', 'tool_oauth2'));
+    $PAGE->navbar->add(get_string('createnewservice', 'tool_oauth2') . ' ' . get_string($type . '_service', 'tool_oauth2'));
     echo $OUTPUT->header();
-    echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . get_string($type . '_service', 'tool_oauth2'));
+    echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . ' ' . get_string($type . '_service', 'tool_oauth2'));
     $mform->display();
     echo $OUTPUT->footer();
 
@@ -199,7 +199,7 @@ if ($mform && $mform->is_cancelled()) {
     echo $renderer->issuers_table($issuers);
 
     echo $renderer->container_start();
-    echo get_string('createnewservice', 'tool_oauth2');
+    echo get_string('createnewservice', 'tool_oauth2') . ' ';
 
     // Google template.
     $docs = 'admin/tool/oauth2/issuers/google';
index 938c576..9ff13af 100644 (file)
@@ -31,7 +31,7 @@ $string['connectsystemaccount'] = 'Connect to a system account';
 $string['createfromtemplate'] = 'Create an OAuth 2 service from a template';
 $string['createfromtemplatedesc'] = 'Choose one of the OAuth 2 service templates below to create an OAuth service with a valid configuration for one of the known service types. This will create the OAuth 2 service, with all the correct end points and parameters required for authentication, though you will still need to enter the client ID and secret for the new service before it can be used.';
 $string['createnewendpoint'] = 'Create new endpoint for issuer "{$a}"';
-$string['createnewservice'] = 'Create new service: ';
+$string['createnewservice'] = 'Create new service:';
 $string['createnewuserfieldmapping'] = 'Create new user field mapping for issuer "{$a}"';
 $string['custom_service'] = 'Custom';
 $string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.';
@@ -53,7 +53,7 @@ $string['endpointurl_help'] = 'URL for this endpoint. Must use https:// protocol
 $string['endpointurl'] = 'URL';
 $string['facebook_service'] = 'Facebook';
 $string['google_service'] = 'Google';
-$string['imsobv2p1_service'] = 'IMS OBv2.1';
+$string['imsobv2p1_service'] = 'OpenBadges';
 $string['issuersetup'] = 'Detailed instructions on configuring the common OAuth 2 services';
 $string['issuersetuptype'] = 'Detailed instructions on setting up the {$a} OAuth 2 provider';
 $string['issueralloweddomains_help'] = 'If set, this setting is a comma separated list of domains that logins will be restricted to when using this provider.';
index 2eb3345..429d371 100644 (file)
@@ -142,29 +142,29 @@ Feature: Basic OAuth2 functionality
     And I should see "Identity issuer deleted"
     And I should not see "Testing service modified"
 
-  Scenario: Create, edit and delete standard service for IMS OBv2.1
-    Given I press "IMS OBv2.1"
-    And I should see "Create new service: IMS OBv2.1"
+  Scenario: Create, edit and delete standard service for OpenBadges
+    Given I press "OpenBadges"
+    And I should see "Create new service: OpenBadges"
     And I set the following fields to these values:
       | Client ID                  | thisistheclientid                         |
       | Client secret              | supersecret                               |
       | Service base URL           | https://dc.imsglobal.org/                 |
     When I press "Save changes"
     Then I should see "Changes saved"
-    And I should see "IMS OBv2.1"
-    And "Configured" "icon" should exist in the "IMS OBv2.1" "table_row"
-    And "Do not allow login" "icon" should exist in the "IMS OBv2.1" "table_row"
-    And "Service discovery successful" "icon" should exist in the "IMS OBv2.1" "table_row"
+    And I should see "OpenBadges"
+    And "Configured" "icon" should exist in the "OpenBadges" "table_row"
+    And "Do not allow login" "icon" should exist in the "OpenBadges" "table_row"
+    And "Service discovery successful" "icon" should exist in the "OpenBadges" "table_row"
     And the "src" attribute of "table.admintable th img" "css_element" should contain "IMS-Global-Logo.png"
-    And I click on "Configure endpoints" "link" in the "IMS OBv2.1" "table_row"
+    And I click on "Configure endpoints" "link" in the "OpenBadges" "table_row"
     And I should see "https://dc.imsglobal.org/.well-known/badgeconnect.json" in the "discovery_endpoint" "table_row"
     And I should see "authorization_endpoint"
     And I follow "OAuth 2 services"
-    And I click on "Configure user field mappings" "link" in the "IMS OBv2.1" "table_row"
+    And I click on "Configure user field mappings" "link" in the "OpenBadges" "table_row"
     And I should not see "given_name"
     And I should not see "middle_name"
     And I follow "OAuth 2 services"
-    And I click on "Edit" "link" in the "IMS OBv2.1" "table_row"
+    And I click on "Edit" "link" in the "OpenBadges" "table_row"
     And I set the following fields to these values:
       | Name                       | IMS Global                                |
     And I press "Save changes"
index b4492bc..135f5bc 100644 (file)
@@ -92,9 +92,9 @@ class acceptances_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, '', '', false)->selects;
-        $extrafields = $userfieldsapi->get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);
+        $extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
 
         $this->set_sql("$userfields",
             "{user} u",
@@ -105,7 +105,7 @@ class acceptances_table extends \table_sql {
         }
         $this->add_column_header('fullname', get_string('fullnameuser', 'core'));
         foreach ($extrafields as $field) {
-            $this->add_column_header($field, \core\user_fields::get_display_name($field));
+            $this->add_column_header($field, \core_user\fields::get_display_name($field));
         }
 
         if (!$this->is_downloading() && !has_capability('tool/policy:acceptbehalf', \context_system::instance())) {
@@ -170,7 +170,7 @@ class acceptances_table extends \table_sql {
      * Helper configuration method.
      */
     protected function configure_for_single_version() {
-        $userfieldsapi = \core\user_fields::for_name();
+        $userfieldsapi = \core_user\fields::for_name();
         $userfieldsmod = $userfieldsapi->get_sql('m', false, 'mod', '', false)->selects;
&nbs