Merge branch 'MDL-67378-master' of git://github.com/rezaies/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 8 Jan 2020 03:11:45 +0000 (11:11 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 8 Jan 2020 03:11:45 +0000 (11:11 +0800)
500 files changed:
.travis.yml
Gruntfile.js
GruntfileComponents.js [new file with mode: 0644]
admin/category.php
admin/environment.xml
admin/mnet/peer_forms.php
admin/qbehaviours.php
admin/qtypes.php
admin/renderer.php
admin/roles/classes/capability_table_base.php
admin/roles/classes/check_capability_table.php
admin/roles/classes/define_role_table_advanced.php
admin/roles/classes/override_permissions_table_advanced.php
admin/templates/setting.mustache
admin/templates/setting_configpasswordunmask.mustache
admin/templates/settings_search_results.mustache
admin/tests/behat/filter_users.feature
admin/tool/capability/classes/settings_form.php
admin/tool/capability/index.php
admin/tool/capability/lang/en/tool_capability.php
admin/tool/capability/renderer.php
admin/tool/capability/tests/behat/show_capabilies.feature [new file with mode: 0644]
admin/tool/capability/tests/behat/show_differences.feature [new file with mode: 0644]
admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-debug.js
admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js
admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search.js
admin/tool/capability/yui/src/search/js/search.js
admin/tool/customlang/db/upgrade.php
admin/tool/customlang/locallib.php
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/task/process_data_request_task.php
admin/tool/dataprivacy/templates/component_status.mustache
admin/tool/dataprivacy/tests/api_test.php
admin/tool/filetypes/renderer.php
admin/tool/filetypes/styles.css
admin/tool/log/db/upgrade.php
admin/tool/log/store/database/db/upgrade.php
admin/tool/log/store/standard/db/upgrade.php
admin/tool/lp/db/renamedclasses.php [deleted file]
admin/tool/monitor/db/upgrade.php
admin/tool/task/cli/adhoc_task.php
admin/tool/task/lang/en/tool_task.php
admin/tool/task/schedule_task.php
admin/tool/uploadcourse/lang/en/tool_uploadcourse.php
admin/tool/usertours/amd/build/tour.min.js
admin/tool/usertours/amd/build/tour.min.js.map
admin/tool/usertours/amd/src/tour.js
admin/tool/usertours/db/upgrade.php
admin/tool/xmldb/actions/main_view/main_view.class.php
admin/upgrade.txt
admin/user.php
auth/cas/db/upgrade.php
auth/db/db/upgrade.php
auth/email/db/upgrade.php
auth/ldap/db/upgrade.php
auth/manual/db/upgrade.php
auth/mnet/db/upgrade.php
auth/none/db/upgrade.php
auth/oauth2/db/upgrade.php
auth/shibboleth/db/upgrade.php
auth/upgrade.txt
babel-plugin-add-module-to-define.js
backup/util/ui/renderer.php
badges/renderer.php
blocks/badges/db/upgrade.php
blocks/calendar_month/db/upgrade.php
blocks/calendar_upcoming/db/upgrade.php
blocks/completionstatus/db/upgrade.php
blocks/course_summary/db/upgrade.php
blocks/html/db/upgrade.php
blocks/navigation/db/upgrade.php
blocks/quiz_results/db/upgrade.php
blocks/recent_activity/db/upgrade.php
blocks/rss_client/db/upgrade.php
blocks/section_links/db/upgrade.php
blocks/selfcompletion/db/upgrade.php
blocks/settings/db/upgrade.php
blocks/timeline/amd/build/event_list.min.js
blocks/timeline/amd/build/event_list.min.js.map
blocks/timeline/amd/src/event_list.js
cache/stores/mongodb/LICENSE [moved from cache/stores/mongodb/MongoDB/LICENSE with 100% similarity]
cache/stores/mongodb/MongoDB/BulkWriteResult.php
cache/stores/mongodb/MongoDB/ChangeStream.php
cache/stores/mongodb/MongoDB/Client.php
cache/stores/mongodb/MongoDB/Collection.php
cache/stores/mongodb/MongoDB/Database.php
cache/stores/mongodb/MongoDB/DeleteResult.php
cache/stores/mongodb/MongoDB/Exception/BadMethodCallException.php
cache/stores/mongodb/MongoDB/Exception/Exception.php
cache/stores/mongodb/MongoDB/Exception/InvalidArgumentException.php
cache/stores/mongodb/MongoDB/Exception/ResumeTokenException.php
cache/stores/mongodb/MongoDB/Exception/RuntimeException.php
cache/stores/mongodb/MongoDB/Exception/UnexpectedValueException.php
cache/stores/mongodb/MongoDB/Exception/UnsupportedException.php
cache/stores/mongodb/MongoDB/GridFS/Bucket.php
cache/stores/mongodb/MongoDB/GridFS/CollectionWrapper.php
cache/stores/mongodb/MongoDB/GridFS/Exception/CorruptFileException.php
cache/stores/mongodb/MongoDB/GridFS/Exception/FileNotFoundException.php
cache/stores/mongodb/MongoDB/GridFS/ReadableStream.php
cache/stores/mongodb/MongoDB/GridFS/StreamWrapper.php
cache/stores/mongodb/MongoDB/GridFS/WritableStream.php
cache/stores/mongodb/MongoDB/InsertManyResult.php
cache/stores/mongodb/MongoDB/InsertOneResult.php
cache/stores/mongodb/MongoDB/MapReduceResult.php
cache/stores/mongodb/MongoDB/Model/BSONArray.php
cache/stores/mongodb/MongoDB/Model/BSONDocument.php
cache/stores/mongodb/MongoDB/Model/BSONIterator.php
cache/stores/mongodb/MongoDB/Model/CachingIterator.php
cache/stores/mongodb/MongoDB/Model/ChangeStreamIterator.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/Model/CollectionInfo.php
cache/stores/mongodb/MongoDB/Model/DatabaseInfo.php
cache/stores/mongodb/MongoDB/Model/DatabaseInfoLegacyIterator.php
cache/stores/mongodb/MongoDB/Model/IndexInfo.php
cache/stores/mongodb/MongoDB/Model/IndexInput.php
cache/stores/mongodb/MongoDB/Model/TypeMapArrayIterator.php [deleted file]
cache/stores/mongodb/MongoDB/Operation/Aggregate.php
cache/stores/mongodb/MongoDB/Operation/BulkWrite.php
cache/stores/mongodb/MongoDB/Operation/Count.php
cache/stores/mongodb/MongoDB/Operation/CountDocuments.php
cache/stores/mongodb/MongoDB/Operation/CreateCollection.php
cache/stores/mongodb/MongoDB/Operation/CreateIndexes.php
cache/stores/mongodb/MongoDB/Operation/DatabaseCommand.php
cache/stores/mongodb/MongoDB/Operation/Delete.php
cache/stores/mongodb/MongoDB/Operation/DeleteMany.php
cache/stores/mongodb/MongoDB/Operation/DeleteOne.php
cache/stores/mongodb/MongoDB/Operation/Distinct.php
cache/stores/mongodb/MongoDB/Operation/DropCollection.php
cache/stores/mongodb/MongoDB/Operation/DropDatabase.php
cache/stores/mongodb/MongoDB/Operation/DropIndexes.php
cache/stores/mongodb/MongoDB/Operation/EstimatedDocumentCount.php
cache/stores/mongodb/MongoDB/Operation/Explain.php
cache/stores/mongodb/MongoDB/Operation/Explainable.php
cache/stores/mongodb/MongoDB/Operation/Find.php
cache/stores/mongodb/MongoDB/Operation/FindAndModify.php
cache/stores/mongodb/MongoDB/Operation/FindOne.php
cache/stores/mongodb/MongoDB/Operation/FindOneAndDelete.php
cache/stores/mongodb/MongoDB/Operation/FindOneAndReplace.php
cache/stores/mongodb/MongoDB/Operation/FindOneAndUpdate.php
cache/stores/mongodb/MongoDB/Operation/InsertMany.php
cache/stores/mongodb/MongoDB/Operation/InsertOne.php
cache/stores/mongodb/MongoDB/Operation/ListCollections.php
cache/stores/mongodb/MongoDB/Operation/ListDatabases.php
cache/stores/mongodb/MongoDB/Operation/ListIndexes.php
cache/stores/mongodb/MongoDB/Operation/MapReduce.php
cache/stores/mongodb/MongoDB/Operation/ModifyCollection.php
cache/stores/mongodb/MongoDB/Operation/ReplaceOne.php
cache/stores/mongodb/MongoDB/Operation/Update.php
cache/stores/mongodb/MongoDB/Operation/UpdateMany.php
cache/stores/mongodb/MongoDB/Operation/UpdateOne.php
cache/stores/mongodb/MongoDB/Operation/Watch.php
cache/stores/mongodb/MongoDB/Operation/WithTransaction.php [new file with mode: 0644]
cache/stores/mongodb/MongoDB/UpdateResult.php
cache/stores/mongodb/MongoDB/functions.php
cache/stores/mongodb/MongoDB/readme_moodle.txt [deleted file]
cache/stores/mongodb/readme_moodle.txt [new file with mode: 0644]
cache/stores/mongodb/thirdpartylibs.xml
calendar/lib.php
calendar/templates/event_details.mustache
calendar/tests/externallib_test.php
calendar/tests/lib_test.php
competency/tests/privacy_test.php
composer.json
composer.lock
config-dist.php
course/format/topics/db/upgrade.php
course/format/topics/db/upgradelib.php [deleted file]
course/format/topics/tests/format_topics_upgrade_test.php [deleted file]
course/format/upgrade.txt
course/format/weeks/db/upgrade.php
course/format/weeks/db/upgradelib.php [deleted file]
course/format/weeks/tests/format_weeks_upgrade_test.php [deleted file]
course/modlib.php
course/templates/activityinstance.mustache
course/templates/bulkactivitycompletion.mustache
course/templates/defaultactivitycompletion.mustache
enrol/database/db/upgrade.php
enrol/flatfile/db/upgrade.php
enrol/guest/db/upgrade.php
enrol/imsenterprise/db/upgrade.php
enrol/lti/db/upgrade.php
enrol/manual/amd/build/form-potential-user-selector.min.js
enrol/manual/amd/build/form-potential-user-selector.min.js.map
enrol/manual/amd/src/form-potential-user-selector.js
enrol/manual/db/upgrade.php
enrol/meta/lib.php
enrol/mnet/db/upgrade.php
enrol/paypal/db/upgrade.php
enrol/renderer.php
enrol/self/db/upgrade.php
files/converter/unoconv/classes/converter.php
filter/displayh5p/tests/behat/h5p_filter.feature
filter/mathjaxloader/db/upgrade.php
filter/mathjaxloader/db/upgradelib.php [deleted file]
filter/mathjaxloader/tests/upgradelib_test.php [deleted file]
filter/mediaplugin/db/upgrade.php
filter/mediaplugin/styles.css
filter/tex/db/upgrade.php
filter/upgrade.txt
grade/amd/build/edittree_index.min.js
grade/amd/build/edittree_index.min.js.map
grade/amd/src/edittree_index.js
grade/classes/grades/grader/gradingpanel/point/external/fetch.php
grade/classes/grades/grader/gradingpanel/point/external/store.php
grade/classes/grades/grader/gradingpanel/scale/external/fetch.php
grade/classes/grades/grader/gradingpanel/scale/external/store.php
grade/edit/tree/lib.php
grade/grading/form/guide/classes/grades/grader/gradingpanel/external/fetch.php
grade/grading/form/guide/db/upgrade.php
grade/grading/form/guide/tests/grades_grader_gradingpanel_guide_external_fetch_test.php
grade/grading/form/guide/tests/grades_grader_gradingpanel_guide_external_store_test.php
grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/fetch.php
grade/grading/form/rubric/db/upgrade.php
grade/grading/form/rubric/lang/en/gradingform_rubric.php
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/tests/behat/grade_calculation.feature
grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_fetch_test.php
grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_store_test.php
grade/grading/renderer.php
grade/grading/tests/behat/behat_grading.php
grade/report/overview/db/upgrade.php
grade/report/user/db/upgrade.php
grade/templates/edit_tree.mustache
grade/tests/grades_grader_gradingpanel_point_external_fetch_test.php
grade/tests/grades_grader_gradingpanel_point_external_store_test.php
grade/tests/grades_grader_gradingpanel_scale_external_fetch_test.php
grade/tests/grades_grader_gradingpanel_scale_external_store_test.php
group/templates/group_details.mustache
install/lang/el/error.php
install/lang/fr_incl/langconfig.php [moved from mod/lti/service/toolproxy/db/renamedclasses.php with 61% similarity]
install/lang/se/install.php [new file with mode: 0644]
install/lang/tpi/langconfig.php [moved from mod/lti/service/profile/db/renamedclasses.php with 62% similarity]
iplookup/tests/fixtures/GeoIP2-City-Test.mmdb [new file with mode: 0644]
iplookup/tests/fixtures/README.txt [new file with mode: 0644]
iplookup/tests/geoip_test.php
lang/en/admin.php
lang/en/antivirus.php
lang/en/course.php
lang/en/deprecated.txt
lang/en/error.php
lang/en/grades.php
lang/en/h5p.php
lang/en/moodle.php
lang/en/user.php
lib/adminlib.php
lib/amd/build/adapter.min.js
lib/amd/build/adapter.min.js.map
lib/amd/build/loglevel.min.js
lib/amd/build/loglevel.min.js.map
lib/amd/build/notification.min.js
lib/amd/build/notification.min.js.map
lib/amd/build/tag.min.js
lib/amd/build/tag.min.js.map
lib/amd/src/adapter.js
lib/amd/src/loglevel.js
lib/amd/src/notification.js
lib/amd/src/tag.js
lib/antivirus/clamav/db/upgrade.php
lib/babel-polyfill/polyfill.js
lib/babel-polyfill/polyfill.min.js
lib/behat/form_field/behat_form_autocomplete.php
lib/classes/filetypes.php
lib/classes/lock/postgres_lock_factory.php
lib/classes/task/adhoc_task.php
lib/classes/task/course_backup_task.php
lib/classes/task/manager.php
lib/classes/useragent.php
lib/cronlib.php
lib/db/renamedclasses.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/deprecatedlib.php
lib/editor/atto/db/upgrade.php
lib/editor/atto/plugins/equation/db/upgrade.php
lib/editor/atto/plugins/h5p/tests/behat/h5p.feature
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button-debug.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button-min.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-debug.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording.js
lib/editor/atto/plugins/recordrtc/yui/src/button/js/button.js
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/commonmodule.js
lib/editor/tests/fixtures/editor_form.php
lib/editor/tinymce/db/upgrade.php
lib/editor/tinymce/plugins/spellchecker/db/upgrade.php
lib/filelib.php
lib/filestorage/file_system.php
lib/form/duration.php
lib/form/filetypes.php
lib/form/float.php
lib/maxmind/GeoIp2/Compat/JsonSerializable.php [deleted file]
lib/maxmind/GeoIp2/Database/Reader.php
lib/maxmind/GeoIp2/Model/AnonymousIp.php
lib/maxmind/GeoIp2/Model/Asn.php
lib/maxmind/GeoIp2/Model/City.php
lib/maxmind/GeoIp2/Model/ConnectionType.php
lib/maxmind/GeoIp2/Model/Country.php
lib/maxmind/GeoIp2/Model/Domain.php
lib/maxmind/GeoIp2/Model/Enterprise.php
lib/maxmind/GeoIp2/Model/Insights.php
lib/maxmind/GeoIp2/Model/Isp.php
lib/maxmind/GeoIp2/Record/AbstractRecord.php
lib/maxmind/GeoIp2/Record/Country.php
lib/maxmind/GeoIp2/Record/Location.php
lib/maxmind/GeoIp2/Record/RepresentedCountry.php
lib/maxmind/GeoIp2/Record/Subdivision.php
lib/maxmind/GeoIp2/Record/Traits.php
lib/maxmind/GeoIp2/Util.php [new file with mode: 0644]
lib/maxmind/GeoIp2/WebService/Client.php
lib/maxmind/MaxMind/CHANGELOG.md [new file with mode: 0644]
lib/maxmind/MaxMind/Db/Reader.php
lib/maxmind/MaxMind/Db/Reader/Decoder.php
lib/maxmind/MaxMind/Db/Reader/InvalidDatabaseException.php
lib/maxmind/MaxMind/Db/Reader/Metadata.php
lib/maxmind/MaxMind/LICENSE [new file with mode: 0644]
lib/maxmind/MaxMind/README.md [new file with mode: 0644]
lib/maxmind/MaxMind/autoload.php [new file with mode: 0644]
lib/maxmind/MaxMind/composer.json [new file with mode: 0644]
lib/maxmind/readme_moodle.txt
lib/moodlelib.php
lib/mustache/readme_moodle.txt
lib/mustache/src/Mustache/Parser.php
lib/plagiarismlib.php
lib/questionlib.php
lib/requirejs.php
lib/scssphp/Compiler.php
lib/scssphp/moodle_readme.txt
lib/setuplib.php
lib/simplepie/README.markdown
lib/simplepie/library/SimplePie.php
lib/simplepie/library/SimplePie/Locator.php
lib/simplepie/library/SimplePie/Parse/Date.php
lib/simplepie/readme_moodle.txt
lib/tablelib.php
lib/tests/adhoc_task_test.php
lib/tests/behat/behat_permissions.php
lib/tests/fixtures/repeated_events.ics [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/session_manager_test.php
lib/tests/task_manager_test.php
lib/tests/upgradelib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
message/classes/api.php
message/output/email/db/upgrade.php
message/output/jabber/db/upgrade.php
message/output/popup/db/upgrade.php
message/templates/message_drawer_view_contacts_body.mustache
message/templates/message_drawer_view_overview_header.mustache
message/templates/notification_preferences_component.mustache
message/templates/notification_preferences_component_notification.mustache
message/tests/api_test.php
message/tests/messagelib_test.php
message/upgrade.txt
mod/assign/db/upgrade.php
mod/assign/feedback/comments/db/upgrade.php
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/file/db/upgrade.php
mod/assign/mod_form.php
mod/assign/submission/comments/db/upgrade.php
mod/assign/submission/file/db/upgrade.php
mod/assign/submission/onlinetext/db/upgrade.php
mod/assign/tests/behat/assign_group_override.feature
mod/assign/tests/behat/assign_user_override.feature
mod/assign/tests/behat/steps_blind_marking.feature
mod/assign/tests/locallib_test.php
mod/assign/upgrade.txt
mod/assign/upgradelib.php
mod/assignment/db/upgrade.php
mod/book/db/upgrade.php
mod/book/locallib.php
mod/book/styles.css
mod/book/tests/behat/show_hide_chapters.feature
mod/book/view.php
mod/chat/db/upgrade.php
mod/choice/db/upgrade.php
mod/data/db/upgrade.php
mod/feedback/classes/responses_table.php
mod/feedback/db/upgrade.php
mod/folder/db/upgrade.php
mod/forum/classes/local/exporters/author.php
mod/forum/classes/local/exporters/discussion_summary.php
mod/forum/classes/local/factories/url.php
mod/forum/db/upgrade.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/mod_form.php
mod/forum/report/summary/lang/en/forumreport_summary.php
mod/forum/templates/local/grades/view_grade.mustache
mod/forum/tests/exporters_author_test.php
mod/forum/tests/externallib_test.php
mod/forum/tests/mail_test.php
mod/glossary/db/upgrade.php
mod/glossary/import_form.php
mod/imscp/db/upgrade.php
mod/label/db/upgrade.php
mod/lesson/db/upgrade.php
mod/lesson/tests/behat/date_availability.feature
mod/lesson/tests/behat/lesson_group_override.feature
mod/lesson/tests/behat/lesson_user_override.feature
mod/lti/db/upgrade.php
mod/lti/service/memberships/db/renamedclasses.php [deleted file]
mod/lti/service/toolsettings/db/renamedclasses.php [deleted file]
mod/page/db/upgrade.php
mod/quiz/db/upgrade.php
mod/quiz/lang/en/quiz.php
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/statistics/db/upgrade.php
mod/resource/db/upgrade.php
mod/scorm/db/upgrade.php
mod/scorm/report/graphs/db/renamedclasses.php [deleted file]
mod/scorm/report/interactions/db/renamedclasses.php [deleted file]
mod/scorm/report/objectives/db/renamedclasses.php [deleted file]
mod/survey/db/upgrade.php
mod/url/db/upgrade.php
mod/wiki/comments_form.php
mod/wiki/db/upgrade.php
mod/wiki/parser/parser.php
mod/workshop/db/upgrade.php
mod/workshop/form/accumulative/db/upgrade.php
mod/workshop/form/comments/db/upgrade.php
mod/workshop/form/numerrors/db/upgrade.php
mod/workshop/form/rubric/db/upgrade.php
mod/workshop/lang/en/workshop.php
mod/workshop/mod_form.php
mod/workshop/renderer.php
mod/workshop/tests/behat/file_type_restriction.feature
mod/workshop/tests/behat/submission_types.feature
pix/i/incorrect.png [new file with mode: 0644]
pix/i/incorrect.svg [new file with mode: 0644]
plagiarism/lib.php
plagiarism/upgrade.txt
portfolio/boxnet/db/upgrade.php
portfolio/googledocs/db/upgrade.php
portfolio/picasa/db/upgrade.php
privacy/classes/local/request/moodle_content_writer.php
question/behaviour/adaptive/renderer.php
question/behaviour/adaptive/tests/mark_display_test.php
question/behaviour/manualgraded/db/upgrade.php
question/classes/bank/view.php
question/engine/questionusage.php
question/engine/renderer.php
question/tests/behat/preview_question.feature
question/type/calculated/datasetitems_form.php
question/type/calculated/db/upgrade.php
question/type/calculatedsimple/edit_calculatedsimple_form.php
question/type/ddmarker/db/upgrade.php
question/type/essay/db/upgrade.php
question/type/match/db/upgrade.php
question/type/multianswer/db/upgrade.php
question/type/multichoice/db/upgrade.php
question/type/numerical/db/upgrade.php
question/type/numerical/edit_numerical_form.php
question/type/numerical/questiontype.php
question/type/numerical/tests/behat/edit.feature
question/type/random/db/upgrade.php
question/type/randomsamatch/db/upgrade.php
question/type/shortanswer/db/upgrade.php
report/backups/index.php
report/insights/templates/insight.mustache
report/insights/templates/insight_details.mustache
report/log/classes/table_log.php
report/loglive/classes/table_log.php
report/performance/locallib.php
report/progress/index.php
report/security/index.php
repository/boxnet/db/upgrade.php
repository/dropbox/classes/dropbox.php
repository/dropbox/db/upgrade.php
repository/flickr/db/upgrade.php
repository/googledocs/db/upgrade.php
repository/onedrive/db/upgrade.php
repository/picasa/db/upgrade.php
theme/boost/scss/moodle.scss
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/backup-restore.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/bs2-compat.scss
theme/boost/scss/moodle/bs4alphacompat.scss
theme/boost/scss/moodle/buttons.scss
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/chat.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/expendable.scss
theme/boost/scss/moodle/filemanager.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/moodle/tables.scss
theme/boost/scss/moodle/user.scss
theme/boost/style/moodle.css
theme/boost/templates/embedded.mustache
theme/classic/style/moodle.css
user/classes/output/myprofile/renderer.php
user/filters/lib.php
version.php

index 90edc4f..321c133 100644 (file)
@@ -19,7 +19,7 @@ services:
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
     - 7.3
-    - 7.1.30 # Make this sticky because current default version (7.1.11) has a bug with redis-extension output (MDL-66062)
+    - 7.2
 
 addons:
   postgresql: "9.6"
index f05bb19..f45c261 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+/* eslint-env node */
+
 /**
- * Grunt configuration
+ * 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 => {
+    const fs = require('fs');
+    const path = require('path');
 
-/* eslint-env node */
-module.exports = function(grunt) {
-    var path = require('path'),
-        tasks = {},
-        cwd = process.env.PWD || process.cwd(),
-        async = require('async'),
-        DOMParser = require('xmldom').DOMParser,
-        xpath = require('xpath'),
-        semver = require('semver'),
-        watchman = require('fb-watchman'),
-        watchmanClient = new watchman.Client(),
-        gruntFilePath = process.cwd();
-
-    // 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);
-    }
+    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')) {
-        var root = grunt.option('root');
+        const root = grunt.option('root');
         if (grunt.file.exists(__dirname, root)) {
-            cwd = path.join(__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');
         }
     }
 
+    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,
+                            }
+                        }
+                    }
+                },
+            },
+        };
+    };
+
+    const getScssConfigForFiles = files => {
+        return {
+            stylelint: {
+                scss: {
+                    options: {syntax: 'scss'},
+                    src: files,
+                },
+            },
+        };
+    };
+
+    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));
+
+        if (scssSrc.length) {
+            grunt.config.merge(getScssConfigForFiles(scssSrc));
+        } else {
+            hasScss = false;
+        }
+
+        const cssSrc = [];
+        glob.sync(`${fullRunDir}/**/*.css`).forEach(path => cssSrc.push(path));
+
+        if (cssSrc.length) {
+            grunt.config.merge(getCssConfigForFiles(cssSrc));
+        } else {
+            hasCss = false;
+        }
+    }
+
+    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'));
+
+    // 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);
+    }
+
+    // Detect directories:
+    // * gruntFilePath          The real path on disk to this Gruntfile.js
+    // * cwd                    The current working directory, which can be overridden by the `root` option
+    // * relativeCwd            The cwd, relative to the Gruntfile.js
+    // * componentDirectory     The root directory of the component if the cwd is in a valid component
+    // * inComponent            Whether the cwd is in a valid component
+    // * runDir                 The componentDirectory or cwd if not in a component, relative to Gruntfile.js
+    // * fullRunDir             The full path to the runDir
+    const gruntFilePath = fs.realpathSync(process.cwd());
+    const cwd = getCwd(grunt);
+    const relativeCwd = cwd.replace(new RegExp(`${gruntFilePath}/?`), '');
+    const componentDirectory = ComponentList.getOwningComponentDirectory(relativeCwd);
+    const inComponent = !!componentDirectory;
+    const runDir = inComponent ? componentDirectory : relativeCwd;
+    const fullRunDir = fs.realpathSync(gruntFilePath + path.sep + runDir);
+    grunt.log.debug(`The cwd was detected as ${cwd} with a fullRunDir of ${fullRunDir}`);
+
+    if (inComponent) {
+        grunt.log.ok(`Running tasks for component directory ${componentDirectory}`);
+    }
+
     var files = null;
     if (grunt.option('files')) {
         // Accept a comma separated list of files to process.
         files = grunt.option('files').split(',');
     }
 
-    var inAMD = path.basename(cwd) == 'amd';
+    const inAMD = path.basename(cwd) == 'amd';
 
     // Globbing pattern for matching all AMD JS source files.
-    var amdSrc = [];
-    if (inAMD) {
-        amdSrc.push(cwd + "/src/*.js");
-        amdSrc.push(cwd + "/src/**/*.js");
+    let amdSrc = [];
+    if (inComponent) {
+        amdSrc.push(componentDirectory + "/amd/src/*.js");
+        amdSrc.push(componentDirectory + "/amd/src/**/*.js");
     } else {
-        amdSrc.push("**/amd/src/*.js");
-        amdSrc.push("**/amd/src/**/*.js");
+        amdSrc = ComponentList.getAmdSrcGlobList();
+    }
+
+    let yuiSrc = [];
+    if (inComponent) {
+        yuiSrc.push(componentDirectory + "/yui/src/**/*.js");
+    } else {
+        yuiSrc = ComponentList.getYuiSrcGlobList(gruntFilePath + '/');
     }
 
     /**
@@ -97,28 +225,29 @@ module.exports = function(grunt) {
      * @return {array} The list of thirdparty paths.
      */
     var getThirdPartyPathsFromXML = function() {
-        var thirdpartyfiles = grunt.file.expand('*/**/thirdpartylibs.xml');
-        var libs = ['node_modules/', 'vendor/'];
+        const thirdpartyfiles = ComponentList.getThirdPartyLibsList(gruntFilePath + '/');
+        const libs = ['node_modules/', 'vendor/'];
 
         thirdpartyfiles.forEach(function(file) {
-          var dirname = path.dirname(file);
+            const dirname = path.dirname(file);
 
-          var doc = new DOMParser().parseFromString(grunt.file.read(file));
-          var nodes = xpath.select("/libraries/library/location/text()", doc);
+            const doc = new DOMParser().parseFromString(grunt.file.read(file));
+            const nodes = xpath.select("/libraries/library/location/text()", doc);
 
-          nodes.forEach(function(node) {
-            var lib = path.join(dirname, node.toString());
-            if (grunt.file.isDir(lib)) {
-                // Ensure trailing slash on dirs.
-                lib = lib.replace(/\/?$/, '/');
-            }
+            nodes.forEach(function(node) {
+                let lib = path.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);
-            }
-          });
+                // Look for duplicate paths before adding to array.
+                if (libs.indexOf(lib) === -1) {
+                    libs.push(lib);
+                }
+            });
         });
+
         return libs;
     };
 
@@ -130,7 +259,7 @@ module.exports = function(grunt) {
             options: {quiet: !grunt.option('show-lint-warnings')},
             amd: {src: files ? files : amdSrc},
             // Check YUI module source files.
-            yui: {src: files ? files : ['**/yui/src/**/*.js', '!*/**/yui/src/*/meta/*.js']}
+            yui: {src: files ? files : yuiSrc},
         },
         babel: {
             options: {
@@ -198,30 +327,41 @@ module.exports = function(grunt) {
                 nospawn: true // We need not to spawn so config can be changed dynamically.
             },
             amd: {
-                files: ['**/amd/src/**/*.js'],
+                files: inComponent
+                    ? ['amd/src/*.js', 'amd/src/**/*.js']
+                    : ['**/amd/src/**/*.js'],
                 tasks: ['amd']
             },
             boost: {
-                files: ['**/theme/boost/scss/**/*.scss'],
+                files: [inComponent ? 'scss/**/*.scss' : 'theme/boost/scss/**/*.scss'],
                 tasks: ['scss']
             },
             rawcss: {
-                files: ['**/*.css', '**/theme/**/!(moodle.css|editor.css)'],
+                files: [
+                    '**/*.css',
+                ],
+                excludes: [
+                    '**/moodle.css',
+                    '**/editor.css',
+                ],
                 tasks: ['rawcss']
             },
             yui: {
-                files: ['**/yui/src/**/*.js'],
+                files: inComponent
+                    ? ['yui/src/*.json', 'yui/src/**/*.js']
+                    : ['**/yui/src/**/*.js'],
                 tasks: ['yui']
             },
             gherkinlint: {
-                files: ['**/tests/behat/*.feature'],
+                files: [inComponent ? 'tests/behat/*.feature' : '**/tests/behat/*.feature'],
                 tasks: ['gherkinlint']
             }
         },
         shifter: {
             options: {
                 recursive: true,
-                paths: files ? files : [cwd]
+                // Shifter takes a relative path.
+                paths: files ? files : [runDir]
             }
         },
         gherkinlint: {
@@ -229,42 +369,30 @@ module.exports = function(grunt) {
                 files: files ? files : ['**/tests/behat/*.feature'],
             }
         },
-        stylelint: {
-            scss: {
-                options: {syntax: 'scss'},
-                src: files ? files : ['*/**/*.scss']
-            },
-            css: {
-                src: files ? files : ['*/**/*.css'],
-                options: {
-                    configOverrides: {
-                        rules: {
-                            // These rules have to be disabled in .stylelintrc for scss compat.
-                            "at-rule-no-unknown": true,
-                        }
-                    }
-                }
-            }
-        }
     });
 
     /**
      * Generate ignore files (utilising thirdpartylibs.xml data)
      */
     tasks.ignorefiles = function() {
-      // An array of paths to third party directories.
-      var thirdPartyPaths = getThirdPartyPathsFromXML();
-      // Generate .eslintignore.
-      var eslintIgnores = ['# Generated by "grunt ignorefiles"', '*/**/yui/src/*/meta/', '*/**/build/'].concat(thirdPartyPaths);
-      grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
-      // Generate .stylelintignore.
-      var 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'));
+        // 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'));
     };
 
     /**
@@ -428,7 +556,7 @@ module.exports = function(grunt) {
                             grunt: true,
                             // Run from current working dir and inherit stdio from process.
                             opts: {
-                                cwd: cwd,
+                                cwd: fullRunDir,
                                 stdio: 'inherit'
                             },
                             args: [task, filesOption]
@@ -454,18 +582,26 @@ module.exports = function(grunt) {
             );
         };
 
-        var watchConfig = grunt.config.get(['watch']);
-        watchConfig = Object.keys(watchConfig).reduce(function(carry, key) {
+        const originalWatchConfig = grunt.config.get(['watch']);
+        const watchConfig = Object.keys(originalWatchConfig).reduce(function(carry, key) {
             if (key == 'options') {
                 return carry;
             }
 
-            var value = watchConfig[key];
-            var fileGlobs = value.files;
-            var taskNames = value.tasks;
+            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] = fileGlobs;
+                carry[taskName] = {
+                    files,
+                    excludes,
+                };
             });
 
             return carry;
@@ -499,12 +635,14 @@ module.exports = function(grunt) {
             resp.files.forEach(function(file) {
                 grunt.log.ok('File changed: ' + file.name);
 
-                var fullPath = cwd + '/' + file.name;
+                var fullPath = fullRunDir + '/' + file.name;
                 Object.keys(watchConfig).forEach(function(task) {
-                    var fileGlobs = watchConfig[task];
-                    var match = fileGlobs.every(function(fileGlob) {
-                        return grunt.file.isMatch(fileGlob, fullPath);
+
+                    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
@@ -536,7 +674,7 @@ module.exports = function(grunt) {
         });
 
         // Initiate the watch on the current directory.
-        watchmanClient.command(['watch-project', cwd], function(watchError, watchResponse) {
+        watchmanClient.command(['watch-project', fullRunDir], function(watchError, watchResponse) {
             if (watchError) {
                 grunt.log.error('Error initiating watch:', watchError);
                 watchTaskDone(1);
@@ -558,18 +696,40 @@ module.exports = function(grunt) {
                     return;
                 }
 
-                // Use the matching patterns specified in the watch config.
+                // 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) {
-                    var fileGlobs = watchConfig[task];
-                    var fileGlobMatches = fileGlobs.map(function(fileGlob) {
-                        return ['match', fileGlob, 'wholename'];
-                    });
+                    const matchAll = [];
+                    matchAll.push(['anyof'].concat(watchConfig[task].files.map(matchWholeName)));
 
-                    return ['allof'].concat(fileGlobMatches);
+                    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: ["anyof"].concat(matches),
+                    expression: matches,
                     // Which fields we're interested in.
                     fields: ["name", "size", "type"],
                     // Add our time constraint.
@@ -589,7 +749,7 @@ module.exports = function(grunt) {
                         return;
                     }
 
-                    grunt.log.ok('Listening for changes to files in ' + cwd);
+                    grunt.log.ok('Listening for changes to files in ' + fullRunDir);
                 });
             });
         });
@@ -634,10 +794,8 @@ module.exports = function(grunt) {
     grunt.registerTask('amd', ['eslint:amd', 'babel']);
     grunt.registerTask('js', ['amd', 'yui']);
 
-    // Register CSS taks.
-    grunt.registerTask('css', ['stylelint:scss', 'sass', 'stylelint:css']);
-    grunt.registerTask('scss', ['stylelint:scss', 'sass']);
-    grunt.registerTask('rawcss', ['stylelint:css']);
+    // Register CSS tasks.
+    registerStyleLintTasks(grunt, files, fullRunDir);
 
     // Register the startup task.
     grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
diff --git a/GruntfileComponents.js b/GruntfileComponents.js
new file mode 100644 (file)
index 0000000..06ed999
--- /dev/null
@@ -0,0 +1,185 @@
+// 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/>.
+
+/**
+ * Helper functions for working with Moodle component names, directories, and sources.
+ *
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+"use strict";
+/* eslint-env node */
+
+/** @var {Object} A list of subsystems in Moodle */
+const componentData = {};
+
+/**
+ * Load details of all moodle modules.
+ *
+ * @returns {object}
+ */
+const fetchComponentData = () => {
+    const fs = require('fs');
+    const path = require('path');
+    const glob = require('glob');
+    const gruntFilePath = process.cwd();
+
+    if (!Object.entries(componentData).length) {
+        componentData.subsystems = {};
+        componentData.pathList = [];
+
+        // Fetch the component definiitions from the distributed JSON file.
+        const components = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/components.json`));
+
+        // Build the list of moodle subsystems.
+        componentData.subsystems.lib = 'core';
+        componentData.pathList.push(process.cwd() + path.sep + 'lib');
+        for (const [component, thisPath] of Object.entries(components.subsystems)) {
+            if (thisPath) {
+                // Prefix "core_" to the front of the subsystems.
+                componentData.subsystems[thisPath] = `core_${component}`;
+                componentData.pathList.push(process.cwd() + path.sep + thisPath);
+            }
+        }
+
+        // The list of components incldues the list of subsystems.
+        componentData.components = componentData.subsystems;
+
+        // Go through each of the plugintypes.
+        Object.entries(components.plugintypes).forEach(([pluginType, pluginTypePath]) => {
+            // We don't allow any code in this place..?
+            glob.sync(`${pluginTypePath}/*/version.php`).forEach(versionPath => {
+                const componentPath = fs.realpathSync(path.dirname(versionPath));
+                const componentName = path.basename(componentPath);
+                const frankenstyleName = `${pluginType}_${componentName}`;
+                componentData.components[`${pluginTypePath}/${componentName}`] = frankenstyleName;
+                componentData.pathList.push(componentPath);
+
+                // Look for any subplugins.
+                const subPluginConfigurationFile = `${componentPath}/db/subplugins.json`;
+                if (fs.existsSync(subPluginConfigurationFile)) {
+                    const subpluginList = JSON.parse(fs.readFileSync(fs.realpathSync(subPluginConfigurationFile)));
+
+                    Object.entries(subpluginList.plugintypes).forEach(([subpluginType, subpluginTypePath]) => {
+                        glob.sync(`${subpluginTypePath}/*/version.php`).forEach(versionPath => {
+                            const componentPath = fs.realpathSync(path.dirname(versionPath));
+                            const componentName = path.basename(componentPath);
+                            const frankenstyleName = `${subpluginType}_${componentName}`;
+
+                            componentData.components[`${subpluginTypePath}/${componentName}`] = frankenstyleName;
+                            componentData.pathList.push(componentPath);
+                        });
+                    });
+                }
+            });
+        });
+
+    }
+
+    return componentData;
+};
+
+/**
+ * Get the list of paths to build AMD sources.
+ *
+ * @returns {Array}
+ */
+const getAmdSrcGlobList = () => {
+    const globList = [];
+    fetchComponentData().pathList.forEach(componentPath => {
+        globList.push(`${componentPath}/amd/src/*.js`);
+        globList.push(`${componentPath}/amd/src/**/*.js`);
+    });
+
+    return globList;
+};
+
+/**
+ * Get the list of paths to build YUI sources.
+ *
+ * @param {String} relativeTo
+ * @returns {Array}
+ */
+const getYuiSrcGlobList = relativeTo => {
+    const globList = [];
+    fetchComponentData().pathList.forEach(componentPath => {
+        const relativeComponentPath = componentPath.replace(relativeTo, '');
+        globList.push(`${relativeComponentPath}/yui/src/**/*.js`);
+    });
+
+    return globList;
+};
+
+/**
+ * Get the list of paths to thirdpartylibs.xml.
+ *
+ * @param {String} relativeTo
+ * @returns {Array}
+ */
+const getThirdPartyLibsList = relativeTo => {
+    const fs = require('fs');
+
+    return fetchComponentData().pathList
+        .map(componentPath => componentPath.replace(relativeTo, '') + '/thirdpartylibs.xml')
+        .filter(path => fs.existsSync(path))
+        .sort();
+};
+
+/**
+ * Find the name of the component matching the specified path.
+ *
+ * @param {String} path
+ * @returns {String|null} Name of matching component.
+ */
+const getComponentFromPath = path => {
+    const componentList = fetchComponentData().components;
+
+    if (componentList.hasOwnProperty(path)) {
+        return componentList[path];
+    }
+
+    return null;
+};
+
+/**
+ * Check whether the supplied path, relative to the Gruntfile.js, is in a known component.
+ *
+ * @param {String} checkPath The path to check
+ * @returns {String|null}
+ */
+const getOwningComponentDirectory = checkPath => {
+    const path = require('path');
+
+    const pathList = fetchComponentData().components;
+    for (const componentPath of Object.keys(pathList)) {
+        if (checkPath === componentPath) {
+            return componentPath;
+        }
+        if (checkPath.startsWith(componentPath + path.sep)) {
+            return componentPath;
+        }
+    }
+
+    return null;
+};
+
+module.exports = {
+    getAmdSrcGlobList,
+    getComponentFromPath,
+    getOwningComponentDirectory,
+    getYuiSrcGlobList,
+    getThirdPartyLibsList,
+};
index 39b74ab..9bfa2bb 100644 (file)
@@ -30,7 +30,7 @@ $category = required_param('category', PARAM_SAFEDIR);
 $return = optional_param('return','', PARAM_ALPHA);
 $adminediting = optional_param('adminedit', -1, PARAM_BOOL);
 
-require_admin();
+require_login(0, false);
 $PAGE->set_context(context_system::instance());
 $PAGE->set_url('/admin/category.php', array('category' => $category));
 $PAGE->set_pagetype('admin-setting-' . $category);
index 16b7402..6f6fb81 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.9" requires="3.5">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="10.2.29" />
+      <VENDOR name="mysql" version="5.6" />
+      <VENDOR name="postgres" version="9.5" />
+      <VENDOR name="mssql" version="11.0" />
+      <VENDOR name="oracle" version="11.2" />
+    </DATABASE>
+    <PHP version="7.2.0" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="mbstringrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="opensslrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlreader" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="intlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+      <PHP_EXTENSION name="fileinfo" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfileformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfilepertable" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddblargeprefix" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="ishttpswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="incompleteunicodesupport" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="sixtyfourbitswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index 5769e6d..cd24ec1 100644 (file)
@@ -156,7 +156,7 @@ class mnet_review_host_form extends moodleform {
         if ($mnet_peer && !empty($mnet_peer->deleted)) {
             $radioarray = array();
             $radioarray[] = $mform->createElement('static', 'deletedinfo', '',
-                $OUTPUT->container(get_string('deletedhostinfo', 'mnet'), 'deletedhostinfo'));
+                $OUTPUT->container(get_string('deletedhostinfo', 'mnet'), 'alert alert-warning'));
             $radioarray[] = $mform->createElement('radio', 'deleted', '', get_string('yes'), 1);
             $radioarray[] = $mform->createElement('radio', 'deleted', '', get_string('no'), 0);
             $mform->addGroup($radioarray, 'radioar', get_string('deleted'), array(' ', ' '), false);
index ae01d31..9edd3f0 100644 (file)
@@ -170,7 +170,7 @@ foreach ($sortedbehaviours as $behaviour => $behaviourname) {
     if ($version) {
         $row[] = $version;
     } else {
-        $row[] = html_writer::tag('span', get_string('nodatabase', 'admin'), array('class' => 'disabled'));
+        $row[] = html_writer::tag('span', get_string('nodatabase', 'admin'), array('class' => 'text-muted'));
     }
 
     // Other question types required by this one.
index a6bc06a..4704698 100644 (file)
@@ -172,7 +172,7 @@ foreach ($sortedqtypes as $qtypename => $localname) {
     if ($version) {
         $row[] = $version;
     } else {
-        $row[] = html_writer::tag('span', get_string('nodatabase', 'admin'), array('class' => 'disabled'));
+        $row[] = html_writer::tag('span', get_string('nodatabase', 'admin'), array('class' => 'text-muted'));
     }
 
     // Other question types required by this one.
index 5abd08e..5ba612e 100644 (file)
@@ -253,7 +253,7 @@ class core_admin_renderer extends plugin_renderer_base {
             $out .= $this->output->container(get_string('cancelinstallinfodir', 'core_plugin', $pluginfo->rootdir));
             if ($repotype = $pluginman->plugin_external_source($pluginfo->component)) {
                 $out .= $this->output->container(get_string('uninstalldeleteconfirmexternal', 'core_plugin', $repotype),
-                    'uninstalldeleteconfirmexternal');
+                    'alert alert-warning mt-2');
             }
         }
 
@@ -432,7 +432,7 @@ class core_admin_renderer extends plugin_renderer_base {
 
         if ($repotype = $pluginman->plugin_external_source($pluginfo->component)) {
             $confirm .= $this->output->container(get_string('uninstalldeleteconfirmexternal', 'core_plugin', $repotype),
-                'uninstalldeleteconfirmexternal');
+                'alert alert-warning mt-2');
         }
 
         // After any uninstall we must execute full upgrade to finish the cleanup!
@@ -511,7 +511,7 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string HTML to output.
      */
     protected function warning($message, $type = 'warning') {
-        return $this->box($message, 'generalbox admin' . $type);
+        return $this->box($message, 'generalbox alert alert-' . $type);
     }
 
     /**
@@ -526,7 +526,7 @@ class core_admin_renderer extends plugin_renderer_base {
             return $this->warning(get_string('datarootsecuritywarning', 'admin', $CFG->dataroot));
 
         } else if ($insecuredataroot == INSECURE_DATAROOT_ERROR) {
-            return $this->warning(get_string('datarootsecurityerror', 'admin', $CFG->dataroot), 'error');
+            return $this->warning(get_string('datarootsecurityerror', 'admin', $CFG->dataroot), 'danger');
 
         } else {
             return '';
@@ -544,7 +544,7 @@ class core_admin_renderer extends plugin_renderer_base {
         if ($devlibdir) {
             $moreinfo = new moodle_url('/report/security/index.php');
             $warning = get_string('devlibdirpresent', 'core_admin', ['moreinfourl' => $moreinfo->out()]);
-            return $this->warning($warning, 'error');
+            return $this->warning($warning, 'danger');
 
         } else {
             return '';
@@ -721,7 +721,7 @@ class core_admin_renderer extends plugin_renderer_base {
         return $this->warning(
                     $this->container(get_string('maturitycorewarning', 'admin', $maturitylevel)) .
                     $this->container($this->doc_link('admin/versions', get_string('morehelp'))),
-                'error');
+                'danger');
     }
 
     /*
@@ -737,7 +737,7 @@ class core_admin_renderer extends plugin_renderer_base {
         }
 
         $warning = (get_string('testsiteupgradewarning', 'admin', $testsite));
-        return $this->warning($warning, 'error');
+        return $this->warning($warning, 'danger');
     }
 
     /**
@@ -772,7 +772,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $level = 'warning';
 
         if ($maturity == MATURITY_ALPHA) {
-            $level = 'error';
+            $level = 'danger';
         }
 
         $maturitylevel = get_string('maturity' . $maturity, 'admin');
@@ -954,7 +954,7 @@ class core_admin_renderer extends plugin_renderer_base {
     protected function release_notes_link() {
         $releasenoteslink = get_string('releasenoteslink', 'admin', 'http://docs.moodle.org/dev/Releases');
         $releasenoteslink = str_replace('target="_blank"', 'onclick="this.target=\'_blank\'"', $releasenoteslink); // extremely ugly validation hack
-        return $this->box($releasenoteslink, 'generalbox releasenoteslink');
+        return $this->box($releasenoteslink, 'generalbox alert alert-info');
     }
 
     /**
@@ -1902,7 +1902,7 @@ class core_admin_renderer extends plugin_renderer_base {
             get_string('status'),
         );
         $servertable->colclasses = array('centeralign name', 'centeralign info', 'leftalign report', 'leftalign plugin', 'centeralign status');
-        $servertable->attributes['class'] = 'admintable environmenttable generaltable';
+        $servertable->attributes['class'] = 'admintable environmenttable generaltable table-sm';
         $servertable->id = 'serverstatus';
 
         $serverdata = array('ok'=>array(), 'warn'=>array(), 'error'=>array());
@@ -1915,7 +1915,7 @@ class core_admin_renderer extends plugin_renderer_base {
             get_string('status'),
         );
         $othertable->colclasses = array('aligncenter info', 'alignleft report', 'alignleft plugin', 'aligncenter status');
-        $othertable->attributes['class'] = 'admintable environmenttable generaltable';
+        $othertable->attributes['class'] = 'admintable environmenttable generaltable table-sm';
         $othertable->id = 'otherserverstatus';
 
         $otherdata = array('ok'=>array(), 'warn'=>array(), 'error'=>array());
index 2765659..ab45e08 100644 (file)
@@ -43,7 +43,7 @@ abstract class core_role_capability_table_base {
     protected $id;
 
     /** Added to the class="" attribute on output. */
-    protected $classes = array('rolecap');
+    protected $classes = array('rolecap table-hover');
 
     /** Default number of capabilities in the table for the search UI to be shown. */
     const NUM_CAPS_FOR_SEARCH = 12;
index b716ae5..99e8c2e 100644 (file)
@@ -50,6 +50,7 @@ class core_role_check_capability_table extends core_role_capability_table_base {
         $this->contextname = $contextname;
         $this->stryes = get_string('yes');
         $this->strno = get_string('no');
+        $this->add_classes(['table-striped']);
     }
 
     protected function add_header_cells() {
index f4ab562..6fb58c6 100644 (file)
@@ -55,6 +55,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         foreach ($levels as $level => $classname) {
             $this->allcontextlevels[$level] = context_helper::get_level_name($level);
         }
+        $this->add_classes(['table-striped']);
     }
 
     protected function load_current_permissions() {
@@ -157,19 +158,19 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         // Allowed roles.
         $allow = optional_param_array('allowassign', null, PARAM_INT);
         if (!is_null($allow)) {
-            $this->allowassign = $allow;
+            $this->allowassign = array_filter($allow);
         }
         $allow = optional_param_array('allowoverride', null, PARAM_INT);
         if (!is_null($allow)) {
-            $this->allowoverride = $allow;
+            $this->allowoverride = array_filter($allow);
         }
         $allow = optional_param_array('allowswitch', null, PARAM_INT);
         if (!is_null($allow)) {
-            $this->allowswitch = $allow;
+            $this->allowswitch = array_filter($allow);
         }
         $allow = optional_param_array('allowview', null, PARAM_INT);
         if (!is_null($allow)) {
-            $this->allowview = $allow;
+            $this->allowview = array_filter($allow);
         }
 
         // Now read the permissions for each capability.
@@ -618,7 +619,9 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         if ($this->roleid == 0) {
             $options[-1] = get_string('thisnewrole', 'core_role');
         }
-        return html_writer::select($options, 'allow'.$type.'[]', $selected, false, array('multiple' => 'multiple',
+        return
+            html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'allow'.$type.'[]', 'value' => "")) .
+            html_writer::select($options, 'allow'.$type.'[]', $selected, false, array('multiple' => 'multiple',
             'size' => 10, 'class' => 'form-control'));
     }
 
index 1a119d8..3440758 100644 (file)
@@ -66,9 +66,9 @@ class core_role_override_permissions_table_advanced extends core_role_capability
         $rowattributes = parent::get_row_attributes($capability);
         if ($this->permissions[$capability->name] !== 0) {
             if (empty($rowattributes['class'])) {
-                $rowattributes['class'] = "overriddenpermission";
+                $rowattributes['class'] = "overriddenpermission table-warning";
             } else {
-                $rowattributes['class'] .= " overriddenpermission";
+                $rowattributes['class'] .= " overriddenpermission table-warning";
             }
         }
         return $rowattributes;
index af78e7e..a3a3a22 100644 (file)
         <label {{#labelfor}}for="{{labelfor}}"{{/labelfor}}>
             {{{title}}}
             {{#override}}
-                <div class="form-overridden">{{override}}</div>
+                <div class="alert alert-info">{{override}}</div>
             {{/override}}
             {{#warning}}
-                <div class="form-warning">{{warning}}</div>
+                <div class="alert alert-warning">{{warning}}</div>
             {{/warning}}
         </label>
         <span class="form-shortname d-block small text-muted">{{{name}}}</span>
index 8cb2d66..7092e82 100644 (file)
     * size - form element size
     * value - form element value
     * id - element id
+    * forced - has value been defined in config.php
 
     Example context (json):
     {
         "name": "test",
         "id": "test0",
         "size": "8",
-        "value": "secret"
+        "value": "secret",
+        "forced": false
     }
 }}
+{{#forced}}
+    <div class="form-password">
+        <input type="text"
+               name = "{{ name }}"
+               id="{{ id }}"
+               value="********"
+               size="{{ size }}"
+               class="form-control"
+               disabled
+        >
+    </div>
+{{/forced}}
+{{^forced}}
 <div class="form-password">
     <span data-passwordunmask="wrapper" data-passwordunmaskid="{{ id }}">
         <span data-passwordunmask="editor">
@@ -61,3 +76,4 @@ require(['core_form/passwordunmask'], function(PasswordUnmask) {
     new PasswordUnmask("{{ id }}");
 });
 {{/js}}
+{{/forced}}
index 6653154..2a76659 100644 (file)
@@ -47,7 +47,7 @@
                 <h3 class="adminpagetitle"><a href="{{url}}">{{{title}}}</a></h3>
                 <ul class="adminpagepath" aria-label="{{#str}} pagepath, core {{/str}}">
                     {{#path}}
-                    <li>{{.}}</li>
+                    <li class="small text-muted">{{.}}</li>
                     {{/path}}
                 </ul>
                 <fieldset class="adminsettings">
index 34088d5..51e7960 100644 (file)
@@ -6,11 +6,11 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email | auth | confirmed | lastip |
-      | user1 | User | One | one@example.com | manual | 0 | 127.0.1.1 |
-      | user2 | User | Two | two@example.com | ldap | 1 | 0.0.0.0 |
-      | user3 | User | Three | three@example.com | manual | 1 | 0.0.0.0 |
-      | user4 | User | Four | four@example.com | ldap | 0 | 127.0.1.2 |
+      | username | firstname | lastname | email | auth | confirmed | lastip | institution | department |
+      | user1 | User | One | one@example.com | manual | 0 | 127.0.1.1       | moodle      | red        |
+      | user2 | User | Two | two@example.com | ldap | 1 | 0.0.0.0           | moodle      | blue       |
+      | user3 | User | Three | three@example.com | manual | 1 | 0.0.0.0 |                 |            |
+      | user4 | User | Four | four@example.com | ldap | 0 | 127.0.1.2 |                   |            |
     And the following "cohorts" exist:
       | name | idnumber |
       | Cohort 1 | CH1 |
@@ -104,3 +104,15 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
     And I should see "User Two"
     And I should see "User Three"
     And I should see "User Four"
+
+  Scenario: Filter users by institution and department
+    When I set the field "id_institution" to "moodle"
+    And I press "Add filter"
+    Then I should see "User One"
+    And I should see "User Two"
+    And I should not see "User Three"
+    And I should not see "User Four"
+    And I set the field "id_department" to "red"
+    And I press "Add filter"
+    And I should see "User One"
+    And I should not see "User Two"
\ No newline at end of file
index cf5fa3e..1bdf548 100644 (file)
@@ -60,6 +60,11 @@ class tool_capability_settings_form extends moodleform {
         $form->addElement('select', 'roles', get_string('roleslabel', 'tool_capability'), $roles, $attributes);
         $form->setType('roles', PARAM_TEXT);
 
+        $form->addElement('checkbox', 'onlydiff',
+                get_string('filters', 'tool_capability'),
+                get_string('onlydiff', 'tool_capability'));
+        $form->setType('onlydiff', PARAM_BOOL);
+
         $form->addElement('submit', 'submitbutton', get_string('getreport', 'tool_capability'));
     }
 
index 57f0716..7f01576 100644 (file)
@@ -66,6 +66,7 @@ $capabilities = array();
 $rolestoshow = array();
 $roleids = array('0');
 $cleanedroleids = array();
+$onlydiff = false;
 if ($data = $form->get_data()) {
 
     $roleids = array();
@@ -90,6 +91,10 @@ if ($data = $form->get_data()) {
             }
         }
     }
+
+    if (isset($data->onlydiff)) {
+        $onlydiff = $data->onlydiff;
+    }
 }
 
 \tool_capability\event\report_viewed::create()->trigger();
@@ -103,7 +108,7 @@ $form->display();
 // If we have a capability, generate the report.
 if (count($capabilities) && count($rolestoshow)) {
     /* @var tool_capability_renderer $renderer */
-    echo $renderer->capability_comparison_table($capabilities, $context->id, $rolestoshow);
+    echo $renderer->capability_comparison_table($capabilities, $context->id, $rolestoshow, $onlydiff);
 }
 
 // Footer.
@@ -138,7 +143,7 @@ function print_report_tree($contextid, $contexts, $allroles) {
     // If there are any role overrides here, print them.
     if (!empty($contexts[$contextid]->rolecapabilities)) {
         $rowcounter = 0;
-        echo '<table class="generaltable rolecaps"><tbody>';
+        echo '<table class="generaltable table-striped"><tbody>';
         foreach ($allroles as $role) {
             if (isset($contexts[$contextid]->rolecapabilities[$role->id])) {
                 $permission = $contexts[$contextid]->rolecapabilities[$role->id];
index b44db22..05a4534 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['onlydiff'] = 'Show differences only';
 $string['capabilitylabel'] = 'Capability:';
 $string['capabilityreport'] = 'Capability overview';
 $string['eventreportviewed'] = 'Report viewed';
+$string['filters'] = 'Filter results';
 $string['forroles'] = 'For roles {$a}';
 $string['getreport'] = 'Get the overview';
 $string['changeoverrides'] = 'Change overrides in this context';
 $string['changeroles'] = 'Change role definitions';
 $string['intro'] = 'This report shows, for a particular capability, what permission that capability has in the definition of every role (or a selection of roles), and everywhere in the site where that capability is overridden.';
 $string['pluginname'] = 'Capability overview';
+$string['nodifferences'] = 'There are no differences to show between selected roles in this context';
 $string['reportforcapability'] = 'Report for capability \'{$a}\'';
 $string['reportsettings'] = 'Report settings';
 $string['roleslabel'] = 'Roles:';
index 30a9628..d083f28 100644 (file)
@@ -72,9 +72,10 @@ class tool_capability_renderer extends plugin_renderer_base {
      * @param array $capabilities An array of capabilities to show comparison for.
      * @param int $contextid The context we are displaying for.
      * @param array $roles An array of roles to show comparison for.
+     * @param bool $onlydiff show only different permissions
      * @return string
      */
-    public function capability_comparison_table(array $capabilities, $contextid, array $roles) {
+    public function capability_comparison_table(array $capabilities, $contextid, array $roles, $onlydiff=false) {
 
         $strpermissions = $this->get_permission_strings();
         $permissionclasses = $this->get_permission_classes();
@@ -99,18 +100,23 @@ class tool_capability_renderer extends plugin_renderer_base {
 
             $row = new html_table_row(array($captitle));
 
+            $permissiontypes = array();
             foreach ($roles as $role) {
                 if (isset($contexts[$contextid]->rolecapabilities[$role->id])) {
                     $permission = $contexts[$contextid]->rolecapabilities[$role->id];
                 } else {
                     $permission = CAP_INHERIT;
                 }
+                if (!in_array($permission, $permissiontypes)) {
+                    $permissiontypes[] = $permission;
+                }
                 $cell = new html_table_cell($strpermissions[$permission]);
                 $cell->attributes['class'] = $permissionclasses[$permission];
                 $row->cells[] = $cell;
             }
-
-            $table->data[] = $row;
+            if (!$onlydiff || count($permissiontypes) > 1) {
+                $table->data[] = $row;
+            }
         }
 
         // Start the list item, and print the context name as a link to the place to make changes.
@@ -125,11 +131,15 @@ class tool_capability_renderer extends plugin_renderer_base {
         $title = get_string('permissionsincontext', 'core_role', $context->get_context_name());
 
         $html = $this->output->heading(html_writer::link($url, $title), 3);
-        $html .= html_writer::table($table);
+        if (!empty($table->data)) {
+            $html .= html_writer::table($table);
+        } else {
+            $html .= html_writer::tag('p', get_string('nodifferences', 'tool_capability'));
+        }
         // If there are any child contexts, print them recursively.
         if (!empty($contexts[$contextid]->children)) {
             foreach ($contexts[$contextid]->children as $childcontextid) {
-                $html .= $this->capability_comparison_table($capabilities, $childcontextid, $roles, true);
+                $html .= $this->capability_comparison_table($capabilities, $childcontextid, $roles, $onlydiff);
             }
         }
         return $html;
diff --git a/admin/tool/capability/tests/behat/show_capabilies.feature b/admin/tool/capability/tests/behat/show_capabilies.feature
new file mode 100644 (file)
index 0000000..d0ec7c6
--- /dev/null
@@ -0,0 +1,104 @@
+@tool @tool_capability
+Feature: show capabilities for selected roles
+  In order to check roles capabilities
+  As an admin
+  I need to be able to customize capabilities report viewing only specific roles and capabilities
+
+  Background:
+    Given the following "roles" exist:
+      | shortname     | name      | archetype |
+      | studenteq     | Studenteq | student   |
+      | studentdf     | Studentdf | student   |
+    And the following "permission overrides" exist:
+      | capability                    | permission | role        | contextlevel | reference |
+      | moodle/course:changefullname  | Allow      | studentdf   | System       |           |
+      | moodle/course:changeshortname | Prohibit   | studentdf   | System       |           |
+      | moodle/course:changeidnumber  | Prevent    | studentdf   | System       |           |
+    And I log in as "admin"
+    And I navigate to "Users > Permissions > Capability overview" in site administration
+
+  Scenario: visualize capabilities table with a limited number of capabilities
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changefullname, moodle/course:changeshortname |
+      | Roles:      | Studentdf                                                                                 |
+    And I click on "Get the overview" "button"
+    Then I should see "moodle/course:changefullname" in the "comparisontable" "table"
+    And I should see "moodle/course:changeshortname" in the "comparisontable" "table"
+    And I should not see "moodle/course:changecategory" in the "comparisontable" "table"
+
+  Scenario: visualize an allow capability
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changefullname |
+      | Roles:      | Studentdf                                                                                                     |
+    And I click on "Get the overview" "button"
+    Then I should see "Allow" in the "comparisontable" "table"
+    And I should not see "Prevent" in the "comparisontable" "table"
+    And I should not see "Prohibit" in the "comparisontable" "table"
+    And I should not see "Not set" in the "comparisontable" "table"
+
+  Scenario: visualize a prohibit capability
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changeshortname |
+      | Roles:      | Studentdf                                                                                                     |
+    And I click on "Get the overview" "button"
+    Then I should not see "Allow" in the "comparisontable" "table"
+    And I should not see "Prevent" in the "comparisontable" "table"
+    And I should see "Prohibit" in the "comparisontable" "table"
+    And I should not see "Not set" in the "comparisontable" "table"
+
+  Scenario: visualize a not set capability
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changecategory |
+      | Roles:      | Studentdf                    |
+    And I click on "Get the overview" "button"
+    Then I should not see "Allow" in the "comparisontable" "table"
+    And I should not see "Prevent" in the "comparisontable" "table"
+    And I should not see "Prohibit" in the "comparisontable" "table"
+    And I should see "Not set" in the "comparisontable" "table"
+
+  Scenario: visualize more than one role
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changecategory |
+      | Roles:      | Student, Studentdf           |
+    And I click on "Get the overview" "button"
+    Then I should see "Student" in the "comparisontable" "table"
+    And I should see "Studentdf" in the "comparisontable" "table"
+    And I should not see "Teacher" in the "comparisontable" "table"
+
+  Scenario: visualize all roles without selecting any role
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changecategory |
+    And I click on "Get the overview" "button"
+    Then I should see "Student" in the "comparisontable" "table"
+    And I should see "Studentdf" in the "comparisontable" "table"
+    And I should see "Teacher" in the "comparisontable" "table"
+
+  Scenario: visualize all roles by selecting All option
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changecategory |
+      | Roles:      | All                          |
+    And I click on "Get the overview" "button"
+    Then I should see "Student" in the "comparisontable" "table"
+    And I should see "Studentdf" in the "comparisontable" "table"
+    And I should see "Teacher" in the "comparisontable" "table"
+
+  @javascript
+  Scenario: filter capability list using javascript
+    Given I should see "moodle/site:config" in the "Capability" "field"
+    And I should see "moodle/course:change" in the "Capability" "field"
+    When I wait until the page is ready
+    And I set the field "capabilitysearch" to "moodle/course:change"
+    Then I should see "moodle/course:change" in the "Capability" "field"
+    And I should not see "moodle/site:config" in the "Capability" "field"
+
+  @javascript
+  Scenario: selecting capabilities using filters
+    Given I should see "moodle/course:change" in the "Capability" "field"
+    When I wait until the page is ready
+    And I set the field "capabilitysearch" to "moodle/course:change"
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changecategory |
+      | Roles:      | Student                      |
+    And I set the field "capabilitysearch" to ""
+    And I click on "Get the overview" "button"
+    Then I should see "moodle/course:changecategory" in the "comparisontable" "table"
diff --git a/admin/tool/capability/tests/behat/show_differences.feature b/admin/tool/capability/tests/behat/show_differences.feature
new file mode 100644 (file)
index 0000000..1c907a4
--- /dev/null
@@ -0,0 +1,67 @@
+@tool @tool_capability
+Feature: show only differences between roles for selected capabilities
+  In order to check roles capabilities
+  As an admin
+  I need to be able to filter capabilities report viewing only role differences
+
+  Background:
+    Given the following "roles" exist:
+      | shortname     | name      | archetype |
+      | studenteq     | Studenteq | student   |
+      | studentdf     | Studentdf | student   |
+    And the following "permission overrides" exist:
+      | capability                    | permission | role        | contextlevel | reference |
+      | moodle/course:changefullname  | Allow      | studentdf   | System       |           |
+      | moodle/course:changeshortname | Prohibit   | studentdf   | System       |           |
+    And I log in as "admin"
+    And I navigate to "Users > Permissions > Capability overview" in site administration
+
+  Scenario: Compare identical roles
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changefullname, moodle/course:changeshortname, moodle/course:changeidnumber, moodle/course:changesummary |
+      | Roles:      | Student, Studenteq                                                                                                     |
+    And I set the field "Show differences only" to "1"
+    And I click on "Get the overview" "button"
+    Then I should see "There are no differences to show between selected roles in this context"
+
+  Scenario: Compare different roles
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changefullname, moodle/course:changeshortname, moodle/course:changeidnumber, moodle/course:changesummary |
+      | Roles:      | Student, Studentdf                                                                                                     |
+    And I set the field "Show differences only" to "1"
+    And I click on "Get the overview" "button"
+    Then I should not see "There are no differences to show between selected roles in this context"
+    And I should see "moodle/course:changefullname" in the "comparisontable" "table"
+    And I should see "moodle/course:changeshortname" in the "comparisontable" "table"
+    And I should not see "moodle/course:changesummary" in the "comparisontable" "table"
+
+  Scenario: Compare different roles but comparing capabilities that are equals on both
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changeidnumber, moodle/course:changesummary |
+      | Roles:      | Student, Studentdf                                        |
+    And I set the field "Show differences only" to "1"
+    And I click on "Get the overview" "button"
+    Then I should see "There are no differences to show between selected roles in this context"
+
+  Scenario: Compare all roles without selecting specific role
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changefullname, moodle/site:config |
+    And I set the field "Show differences only" to "1"
+    And I click on "Get the overview" "button"
+    Then I should not see "moodle/site:config" in the "comparisontable" "table"
+    And I should see "moodle/course:changefullname" in the "comparisontable" "table"
+
+  Scenario: Compare all roles without selecting specific role on not defined capability
+    When I set the following fields to these values:
+      | Capability: | moodle/site:config |
+    And I set the field "Show differences only" to "1"
+    And I click on "Get the overview" "button"
+    Then I should see "There are no differences to show between selected roles in this context"
+
+  Scenario: Comparing only one role
+    When I set the following fields to these values:
+      | Capability: | moodle/course:changefullname, moodle/course:changeshortname, moodle/course:changeidnumber, moodle/course:changesummary |
+      | Roles:      | Student                                                                                                                |
+    And I set the field "Show differences only" to "1"
+    And I click on "Get the overview" "button"
+    Then I should see "There are no differences to show between selected roles in this context"
index 6b8b368..1c05534 100644 (file)
Binary files a/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-debug.js and b/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-debug.js differ
index 7e1baef..697a214 100644 (file)
Binary files a/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js and b/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js differ
index 6b8b368..1c05534 100644 (file)
Binary files a/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search.js and b/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search.js differ
index b3b1527..1e4b96c 100644 (file)
@@ -74,7 +74,7 @@ SEARCH.prototype = {
         this.button = this.form.all('input[type=submit]');
         this.lastsearch = this.form.one('input[name=search]');
 
-        var div = Y.Node.create('<div id="capabilitysearchui"></div>'),
+        var div = Y.Node.create('<div id="capabilitysearchui" data-fieldtype="text"></div>'),
             label = Y.Node.create('<label for="capabilitysearch">' + this.get('strsearch') + '</label>');
         this.input = Y.Node.create('<input type="text" id="capabilitysearch" />');
 
index 561a56a..cf935d8 100644 (file)
@@ -29,12 +29,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_customlang_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 83e879a..2f02b45 100644 (file)
@@ -254,20 +254,19 @@ class tool_customlang_utils {
      *
      * @param string $component the name of the component
      * @param array $strings
+     * @return void
      */
     protected static function dump_strings($lang, $component, $strings) {
         global $CFG;
 
         if ($lang !== clean_param($lang, PARAM_LANG)) {
-            debugging('Unable to dump local strings for non-installed language pack .'.s($lang));
-            return false;
+            throw new moodle_exception('Unable to dump local strings for non-installed language pack .'.s($lang));
         }
         if ($component !== clean_param($component, PARAM_COMPONENT)) {
             throw new coding_exception('Incorrect component name');
         }
         if (!$filename = self::get_component_filename($component)) {
-            debugging('Unable to find the filename for the component '.s($component));
-            return false;
+            throw new moodle_exception('Unable to find the filename for the component '.s($component));
         }
         if ($filename !== clean_param($filename, PARAM_FILE)) {
             throw new coding_exception('Incorrect file name '.s($filename));
@@ -284,8 +283,7 @@ class tool_customlang_utils {
         }
 
         if (!$f = fopen($filepath, 'w')) {
-            debugging('Unable to write '.s($filepath));
-            return false;
+            throw new moodle_exception('Unable to write '.s($filepath));
         }
         fwrite($f, <<<EOF
 <?php
index 33da80e..9ae8647 100644 (file)
@@ -712,7 +712,7 @@ class api {
             'requestedby' => $requestedby->fullname,
             'requesttype' => $typetext,
             'requestdate' => userdate($requestdata->timecreated),
-            'requestorigin' => $SITE->fullname,
+            'requestorigin' => format_string($SITE->fullname, true, ['context' => context_system::instance()]),
             'requestoriginurl' => new moodle_url('/'),
             'requestcomments' => $requestdata->messagehtml,
             'datarequestsurl' => $datarequestsurl
index fe3652f..032014c 100644 (file)
@@ -26,6 +26,7 @@ namespace tool_dataprivacy\task;
 
 use action_link;
 use coding_exception;
+use context_system;
 use core\message\message;
 use core\task\adhoc_task;
 use core_user;
@@ -180,7 +181,8 @@ class process_data_request_task extends adhoc_task {
                 $message->contexturl = $datarequestsurl;
                 $message->contexturlname = get_string('datarequests', 'tool_dataprivacy');
                 // Message to the recipient.
-                $messagetextdata['message'] = get_string('resultdownloadready', 'tool_dataprivacy', $SITE->fullname);
+                $messagetextdata['message'] = get_string('resultdownloadready', 'tool_dataprivacy',
+                    format_string($SITE->fullname, true, ['context' => context_system::instance()]));
                 // Prepare download link.
                 $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $thing->get_itemid(),
                     $thing->get_filepath(), $thing->get_filename(), true);
@@ -192,7 +194,8 @@ class process_data_request_task extends adhoc_task {
                 // No point notifying a deleted user in Moodle.
                 $message->notification = 0;
                 // Message to the recipient.
-                $messagetextdata['message'] = get_string('resultdeleted', 'tool_dataprivacy', $SITE->fullname);
+                $messagetextdata['message'] = get_string('resultdeleted', 'tool_dataprivacy',
+                    format_string($SITE->fullname, true, ['context' => context_system::instance()]));
                 // Message will be sent to the deleted user via email only.
                 $emailonly = true;
                 break;
index 9a1c6ad..8d2bf1b 100644 (file)
@@ -75,7 +75,7 @@
                 <hr />
                 <div class="p-l-3">
                     <dl class="row">
-                        <dt class="col-xs-3">
+                        <dt class="col-3">
                             {{#link}}
                                 <a href="#{{name}}"><strong style="word-wrap:break-word">{{name}}</strong></a>
                             {{/link}}
                             {{/link}}
                             <div class="small text-muted" style="word-wrap:break-word">{{type}}</div>
                         </dt>
-                        <dd class="col-xs-9">{{summary}}</dd>
+                        <dd class="col-9">{{summary}}</dd>
                     </dl>
                     <dl>
                         {{#fields}}
                         <div class="row">
-                            <dt class="col-xs-3 font-weight-normal" style="word-wrap:break-word">{{field_name}}</dt>
-                            <dd class="col-xs-9">{{field_summary}}</dd>
+                            <dt class="col-3 font-weight-normal" style="word-wrap:break-word">{{field_name}}</dt>
+                            <dd class="col-9">{{field_summary}}</dd>
                         </div>
                         {{/fields}}
                     </dl>
                 <hr />
                 <div class="p-l-3">
                     <div class="row">
-                        <div class="col-xs-12">
+                        <div class="col-12">
                             {{nullprovider}}
                         </div>
                     </div>
index a913f3e..65894ec 100644 (file)
@@ -301,6 +301,11 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $datarequest = api::create_data_request($student->id, api::DATAREQUEST_TYPE_EXPORT);
 
         $requestid = $datarequest->get('id');
+
+        // Login as a user without DPO role.
+        $this->setUser($teacher);
+        $this->expectException(required_capability_exception::class);
+        api::approve_data_request($requestid);
     }
 
     /**
index 238f686..f283af1 100644 (file)
@@ -52,7 +52,7 @@ class tool_filetypes_renderer extends plugin_renderer_base {
         $out = $this->heading(get_string('pluginname', 'tool_filetypes'));
         if ($restricted) {
             $out .= html_writer::div(
-                    html_writer::div(get_string('configoverride', 'admin'), 'form-overridden'),
+                    html_writer::div(get_string('configoverride', 'admin'), 'alert alert-info'),
                     '', array('id' => 'adminsettings'));
         }
         if (count($combined) > 1) {
index ed2353a..c8375ee 100644 (file)
 .path-admin-tool-filetypes .generaltable .nonstandard {
     font-weight: bold;
 }
-
-/* Spacing around the 'Defined in config.php' stripe */
-.path-admin-tool-filetypes .form-overridden {
-    display: inline-block;
-    margin-bottom: 1em;
-    padding: 4px 6px;
-}
index c62e8de..3a8e797 100644 (file)
@@ -33,12 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_log_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index b68d31e..ec36ccf 100644 (file)
@@ -27,12 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_logstore_database_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 63a312f..5a9a7e0 100644 (file)
@@ -27,12 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_logstore_standard_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
diff --git a/admin/tool/lp/db/renamedclasses.php b/admin/tool/lp/db/renamedclasses.php
deleted file mode 100644 (file)
index 0530f09..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * This file contains renamed classes mappings.
- *
- * @package    tool_lp
- * @copyright  2016 Frédéric Massart - FMCorz.net
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-$renamedclasses = array(
-    'tool_lp\\external\\cohort_summary_exporter' => 'core_cohort\\external\\cohort_summary_exporter',
-    'tool_lp\\external\\course_module_summary_exporter' => 'core_course\\external\\course_module_summary_exporter',
-    'tool_lp\\external\\course_summary_exporter' => 'core_course\\external\\course_summary_exporter',
-    'tool_lp\\form\\persistent' => 'core\\form\\persistent',
-);
index 2e746d5..b8c93ac 100644 (file)
@@ -33,29 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_monitor_upgrade($oldversion) {
     global $CFG, $DB;
 
-    if ($oldversion < 2017021300) {
-
-        // Delete "orphaned" subscriptions.
-        $sql = "SELECT DISTINCT s.courseid
-                  FROM {tool_monitor_subscriptions} s
-       LEFT OUTER JOIN {course} c ON c.id = s.courseid
-                 WHERE s.courseid <> 0 and c.id IS NULL";
-        $deletedcourses = $DB->get_field_sql($sql);
-        if ($deletedcourses) {
-            list($sql, $params) = $DB->get_in_or_equal($deletedcourses);
-            $DB->execute("DELETE FROM {tool_monitor_subscriptions} WHERE courseid " . $sql, $params);
-        }
-
-        // Monitor savepoint reached.
-        upgrade_plugin_savepoint(true, 2017021300, 'tool', 'monitor');
-    }
-
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 1bae380..755026b 100644 (file)
@@ -35,10 +35,12 @@ list($options, $unrecognized) = cli_get_params(
         'keep-alive' => 0,
         'showsql' => false,
         'showdebugging' => false,
+        'ignorelimits' => false,
     ], [
         'h' => 'help',
         'e' => 'execute',
         'k' => 'keep-alive',
+        'i' => 'ignorelimits',
     ]
 );
 
@@ -57,6 +59,7 @@ Options:
      --showdebugging       Show developer level debugging information
  -e, --execute             Run all queued adhoc tasks
  -k, --keep-alive=N        Keep this script alive for N seconds and poll for new adhoc tasks
+ -i  --ignorelimits        Ignore task_adhoc_concurrency_limit and task_adhoc_max_runtime limits
 
 Example:
 \$sudo -u www-data /usr/bin/php admin/tool/task/cli/adhoc_task.php --execute
@@ -99,6 +102,8 @@ if (!empty($CFG->showcrondebugging)) {
     set_debugging(DEBUG_DEVELOPER, true);
 }
 
+$checklimits = empty($options['ignorelimits']);
+
 core_php_time_limit::raise();
 
 // Increase memory limit.
@@ -107,45 +112,8 @@ raise_memory_limit(MEMORY_EXTRA);
 // Emulate normal session - we use admin account by default.
 cron_setup_user();
 
-// Start output log.
-$timestart = time();
-$timenow = $timestart;
-$finishtime = $timenow + (int)$options['keep-alive'];
-$humantimenow = date('r', $timenow);
-mtrace("Server Time: {$humantimenow}\n");
-
-// Run all adhoc tasks.
-$taskcount = 0;
-$waiting = false;
-while (!\core\task\manager::static_caches_cleared_since($timestart)) {
-
-    $task = \core\task\manager::get_next_adhoc_task($timenow);
-
-    if ($task) {
-        if ($waiting) {
-            cli_writeln('');
-        }
-        $waiting = false;
-        cron_run_inner_adhoc_task($task);
-        $taskcount++;
-        unset($task);
-    } else {
-        if (time() > $finishtime) {
-            break;
-        }
-        if (!$waiting) {
-            cli_write('Waiting for more adhoc tasks to be queued ');
-        } else {
-            cli_write('.');
-        }
-        $waiting = true;
-        sleep(1);
-        $timenow = time();
-    }
-}
-if ($waiting) {
-    cli_writeln('');
-}
-
-mtrace("Ran {$taskcount} adhoc tasks found at {$humantimenow}");
+$humantimenow = date('r', time());
+$keepalive = (int)$options['keep-alive'];
 
+mtrace("Server Time: {$humantimenow}\n");
+cron_run_adhoc_tasks(time(), $keepalive, $checklimits);
index 1255622..76ab5da 100644 (file)
@@ -43,6 +43,7 @@ $string['pluginname'] = 'Scheduled task configuration';
 $string['resettasktodefaults'] = 'Reset task schedule to defaults';
 $string['resettasktodefaults_help'] = 'This will discard any local changes and revert the schedule for this task back to its original settings.';
 $string['runnow'] = 'Run now';
+$string['runagain'] = 'Run again';
 $string['runnow_confirm'] = 'Are you sure you want to run this task \'{$a}\' now? The task will run on the web server and may take some time to complete.';
 $string['runpattern'] = 'Run pattern';
 $string['scheduledtasks'] = 'Scheduled tasks';
index 939486d..fd7bc30 100644 (file)
@@ -92,6 +92,11 @@ $CFG->mtrace_wrapper = 'tool_task_mtrace_wrapper';
 echo html_writer::end_tag('pre');
 
 $output = $PAGE->get_renderer('tool_task');
+
+// Re-run the specified task (this will output an error if it doesn't exist).
+echo $OUTPUT->single_button(new moodle_url('/admin/tool/task/schedule_task.php',
+        array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
+        get_string('runagain', 'tool_task'));
 echo $output->link_back();
 
 echo $OUTPUT->footer();
index db95617..7f3e9d7 100644 (file)
@@ -90,7 +90,7 @@ $string['invalidcsvfile'] = 'Invalid input CSV file';
 $string['invalidencoding'] = 'Invalid encoding';
 $string['invalidmode'] = 'Invalid mode selected';
 $string['invalideupdatemode'] = 'Invalid update mode selected';
-$string['invalidvisibilitymode'] = 'Invalid visibility mode given';
+$string['invalidvisibilitymode'] = 'Invalid visible mode';
 $string['invalidroles'] = 'Invalid role names: {$a}';
 $string['invalidshortname'] = 'Invalid shortname';
 $string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}';
index dcf72ce..72237ea 100644 (file)
Binary files a/admin/tool/usertours/amd/build/tour.min.js and b/admin/tool/usertours/amd/build/tour.min.js differ
index 9ebad30..125bbab 100644 (file)
Binary files a/admin/tool/usertours/amd/build/tour.min.js.map and b/admin/tool/usertours/amd/build/tour.min.js.map differ
index 0f9ad94..4a2fd90 100644 (file)
@@ -646,14 +646,15 @@ export default class Tour {
 
         // Is this the first step?
         if (this.isFirstStep(stepConfig.stepNumber)) {
-            template.find('[data-role="previous"]').prop('disabled', true);
+            template.find('[data-role="previous"]').hide();
         } else {
             template.find('[data-role="previous"]').prop('disabled', false);
         }
 
         // Is this the final step?
         if (this.isLastStep(stepConfig.stepNumber)) {
-            template.find('[data-role="next"]').prop('disabled', true);
+            template.find('[data-role="next"]').hide();
+            template.find('[data-role="end"]').removeClass("btn-secondary").addClass("btn-primary");
         } else {
             template.find('[data-role="next"]').prop('disabled', false);
         }
index 7a60194..a6855ea 100644 (file)
@@ -35,12 +35,6 @@ use tool_usertours\manager;
 function xmldb_tool_usertours_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index feef78a..7a37f8e 100644 (file)
@@ -120,7 +120,8 @@ class main_view extends XMLDBAction {
         $result = $this->launch('get_db_directories');
         // Display list of DB directories if everything is ok
         if ($result && !empty($XMLDB->dbdirs)) {
-            $o .= '<table id="listdirectories" border="0" cellpadding="5" cellspacing="1" class="admintable generaltable">';
+            $o .= '<table id="listdirectories" border="0" cellpadding="5" cellspacing="1"' .
+                ' class="table-striped table-sm admintable generaltable">';
             $row = 0;
             foreach ($XMLDB->dbdirs as $key => $dbdir) {
                 // Detect if this is the lastused dir
index 68f18eb..d3adb18 100644 (file)
@@ -1,5 +1,14 @@
 This files describes API changes in /admin/*.
 
+=== 3.9 ===
+
+* The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info:
+    - upgrade_fix_block_instance_configuration()
+    - upgrade_theme_is_from_family()
+    - upgrade_find_theme_location()
+    - linkcoursesectionsupgradescriptwasrun setting
+    - upgrade_block_positions()
+
 === 3.8 ===
 
 * Admin setting "Open to Google" (opentogoogle) has been renamed to the more generic "Open to search engines" (opentowebcrawlers).
index 216906c..a06b2ed 100644 (file)
         $table->head = array ();
         $table->colclasses = array();
         $table->head[] = $fullnamedisplay;
-        $table->attributes['class'] = 'admintable generaltable';
+        $table->attributes['class'] = 'admintable generaltable table-sm';
         foreach ($extracolumns as $field) {
             $table->head[] = ${$field};
         }
index c3c413c..8b2d14b 100644 (file)
@@ -32,19 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_cas_upgrade($oldversion) {
     global $CFG;
 
-    if ($oldversion < 2017020700) {
-        // Convert info in config plugins from auth/cas to auth_cas.
-        upgrade_fix_config_auth_plugin_names('cas');
-        upgrade_fix_config_auth_plugin_defaults('cas');
-        upgrade_plugin_savepoint(true, 2017020700, 'auth', 'cas');
-    }
-
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 2a4625c..3a0691c 100644 (file)
@@ -32,19 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_db_upgrade($oldversion) {
     global $CFG, $DB;
 
-    if ($oldversion < 2017032800) {
-        // Convert info in config plugins from auth/db to auth_db
-        upgrade_fix_config_auth_plugin_names('db');
-        upgrade_fix_config_auth_plugin_defaults('db');
-        upgrade_plugin_savepoint(true, 2017032800, 'auth', 'db');
-    }
-
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index ac34c97..3d2b42d 100644 (file)
@@ -32,19 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_email_upgrade($oldversion) {
     global $CFG, $DB;
 
-    if ($oldversion < 2017020700) {
-        // Convert info in config plugins from auth/email to auth_email.
-        upgrade_fix_config_auth_plugin_names('email');
-        upgrade_fix_config_auth_plugin_defaults('email');
-        upgrade_plugin_savepoint(true, 2017020700, 'auth', 'email');
-    }
-
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index d99fdcc..abb56a9 100644 (file)
@@ -32,37 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_ldap_upgrade($oldversion) {
     global $CFG;
 
-    if ($oldversion < 2017020700) {
-        // Convert info in config plugins from auth/ldap to auth_ldap.
-        upgrade_fix_config_auth_plugin_names('ldap');
-        upgrade_fix_config_auth_plugin_defaults('ldap');
-        upgrade_plugin_savepoint(true, 2017020700, 'auth', 'ldap');
-    }
-
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    if ($oldversion < 2017080100) {
-        // The "auth_ldap/coursecreators" setting was replaced with "auth_ldap/coursecreatorcontext" (created
-        // dynamically from system-assignable roles) - so migrate any existing value to the first new slot.
-        if ($ldapcontext = get_config('auth_ldap', 'creators')) {
-            // Get info about the role that the old coursecreators setting would apply.
-            $creatorrole = get_archetype_roles('coursecreator');
-            $creatorrole = array_shift($creatorrole); // We can only use one, let's use the first.
-
-            // Create new setting.
-            set_config($creatorrole->shortname . 'context', $ldapcontext, 'auth_ldap');
-
-            // Delete old setting.
-            set_config('creators', null, 'auth_ldap');
-
-            upgrade_plugin_savepoint(true, 2017080100, 'auth', 'ldap');
-        }
-    }
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 8afe128..0795fc2 100644 (file)
@@ -32,19 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_manual_upgrade($oldversion) {
     global $CFG;
 
-    if ($oldversion < 2017020700) {
-        // Convert info in config plugins from auth/manual to auth_manual.
-        upgrade_fix_config_auth_plugin_names('manual');
-        upgrade_fix_config_auth_plugin_defaults('manual');
-        upgrade_plugin_savepoint(true, 2017020700, 'auth', 'manual');
-    }
-
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 905920d..fcb489d 100644 (file)
@@ -32,19 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_mnet_upgrade($oldversion) {
     global $CFG;
 
-    if ($oldversion < 2017020700) {
-        // Convert info in config plugins from auth/mnet to auth_mnet.
-        upgrade_fix_config_auth_plugin_names('mnet');
-        upgrade_fix_config_auth_plugin_defaults('mnet');
-        upgrade_plugin_savepoint(true, 2017020700, 'auth', 'mnet');
-    }
-
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 7619cf6..ebf92f3 100644 (file)
@@ -32,19 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_none_upgrade($oldversion) {
     global $CFG, $DB;
 
-    if ($oldversion < 2017020700) {
-        // Convert info in config plugins from auth/none to auth_none.
-        upgrade_fix_config_auth_plugin_names('none');
-        upgrade_fix_config_auth_plugin_defaults('none');
-        upgrade_plugin_savepoint(true, 2017020700, 'auth', 'none');
-    }
-
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 6dac6f2..8e8fc1a 100644 (file)
@@ -35,12 +35,6 @@ function xmldb_auth_oauth2_upgrade($oldversion) {
 
     $dbman = $DB->get_manager();
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index c57f98d..54d240e 100644 (file)
@@ -32,19 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_shibboleth_upgrade($oldversion) {
     global $CFG, $DB;
 
-    if ($oldversion < 2017020700) {
-        // Convert info in config plugins from auth/shibboleth to auth_shibboleth.
-        upgrade_fix_config_auth_plugin_names('shibboleth');
-        upgrade_fix_config_auth_plugin_defaults('shibboleth');
-        upgrade_plugin_savepoint(true, 2017020700, 'auth', 'shibboleth');
-    }
-
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 9171659..79a3b80 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in /auth/* - plugins,
 information provided here is intended especially for developers.
 
+=== 3.9 ===
+
+* The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info:
+    - upgrade_fix_config_auth_plugin_names()
+    - upgrade_fix_config_auth_plugin_defaults()
+
 === 3.7 ===
 
 * get_password_change_info() method is added to the base class and returns an array containing the subject and body of the message
index cdbd8a7..dfe68c6 100644 (file)
 module.exports = ({template, types}) => {
     const fs = require('fs');
     const path = require('path');
-    const glob = require('glob');
     const cwd = process.cwd();
-
-    // Static variable to hold the modules.
-    let moodleSubsystems = null;
-    let moodlePlugins = null;
-
-    /**
-     * Parse Moodle's JSON files containing the lists of components.
-     *
-     * The values are stored in the static variables because we
-     * only need to load them once per transpiling run.
-     */
-    function loadMoodleModules() {
-        moodleSubsystems = {'lib': 'core'};
-        moodlePlugins = {};
-        let components = fs.readFileSync('lib/components.json');
-        components = JSON.parse(components);
-
-        for (const [component, path] of Object.entries(components.subsystems)) {
-            if (path) {
-                // Prefix "core_" to the front of the subsystems.
-                moodleSubsystems[path] = `core_${component}`;
-            }
-        }
-
-        for (const [component, path] of Object.entries(components.plugintypes)) {
-            if (path) {
-                moodlePlugins[path] = component;
-            }
-        }
-
-        for (const file of glob.sync('**/db/subplugins.json')) {
-            var rawContents = fs.readFileSync(file);
-            var subplugins = JSON.parse(rawContents);
-
-            for (const [component, path] of Object.entries(subplugins.plugintypes)) {
-                if (path) {
-                    moodlePlugins[path] = component;
-                }
-            }
-        }
-    }
+    const ComponentList = require(path.resolve('GruntfileComponents.js'));
 
     /**
      * Search the list of components that match the given file name
@@ -99,26 +58,14 @@ module.exports = ({template, types}) => {
         const fileName = file.replace('.js', '');
 
         // Check subsystems first which require an exact match.
-        if (moodleSubsystems.hasOwnProperty(componentPath)) {
-            return `${moodleSubsystems[componentPath]}/${fileName}`;
-        }
-
-        // It's not a subsystem so it must be a plugin. Moodle defines root folders
-        // where plugins can be installed so our path with be <plugin_root>/<plugin_name>.
-        // Let's separate the two.
-        let pathParts = componentPath.split('/');
-        const pluginName = pathParts.pop();
-        const pluginPath = pathParts.join('/');
-
-        // The plugin path mutch match exactly because some plugins are subplugins of
-        // other plugins which means their paths would partially match.
-        if (moodlePlugins.hasOwnProperty(pluginPath)) {
-            return `${moodlePlugins[pluginPath]}_${pluginName}/${fileName}`;
+        const componentName = ComponentList.getComponentFromPath(componentPath);
+        if (componentName) {
+            return `${componentName}/${fileName}`;
         }
 
         // This matches the previous PHP behaviour that would throw an exception
         // if it couldn't parse an AMD file.
-        throw new Error('Unable to find module name for ' + searchFileName);
+        throw new Error(`Unable to find module name for ${searchFileName} (${componentPath}::${file}}`);
     }
 
     /**
@@ -149,10 +96,6 @@ module.exports = ({template, types}) => {
         pre() {
             this.seenDefine = false;
             this.addedReturnForDefaultExport = false;
-
-            if (moodleSubsystems === null) {
-                loadMoodleModules();
-            }
         },
         visitor: {
             // Plugin ordering is only respected if we visit the "Program" node.
index 8caf014..c531260 100644 (file)
@@ -717,7 +717,7 @@ class core_backup_renderer extends plugin_renderer_base {
         $url = $component->get_url();
 
         $output = html_writer::start_tag('div', array('class' => 'restore-course-search form-inline mb-1'));
-        $output .= html_writer::start_tag('div', array('class' => 'rcs-results w-75'));
+        $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
 
         $table = new html_table();
         $table->head = array('', get_string('shortnamecourse'), get_string('fullnamecourse'));
@@ -877,7 +877,7 @@ class core_backup_renderer extends plugin_renderer_base {
         $url = $component->get_url();
 
         $output = html_writer::start_tag('div', array('class' => 'restore-course-search form-inline mb-1'));
-        $output .= html_writer::start_tag('div', array('class' => 'rcs-results w-75'));
+        $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
 
         $table = new html_table();
         $table->head = array('', get_string('name'), get_string('description'));
index 341ef6d..a95f9cc 100644 (file)
@@ -685,7 +685,7 @@ class core_badges_renderer extends plugin_renderer_base {
         $paging = new paging_bar($badges->totalcount, $badges->page, $badges->perpage, $this->page->url, 'page');
         $htmlpagingbar = $this->render($paging);
         $table = new html_table();
-        $table->attributes['class'] = 'collection';
+        $table->attributes['class'] = 'table table-bordered table-striped';
 
         $sortbyname = $this->helper_sortable_heading(get_string('name'),
                 'name', $badges->sort, $badges->dir);
@@ -743,7 +743,7 @@ class core_badges_renderer extends plugin_renderer_base {
 
         $htmlpagingbar = $this->render($paging);
         $table = new html_table();
-        $table->attributes['class'] = 'collection';
+        $table->attributes['class'] = 'table table-bordered table-striped';
 
         $sortbyname = $this->helper_sortable_heading(get_string('name'),
                 'name', $badges->sort, $badges->dir);
index afaa541..c7ae451 100644 (file)
 function xmldb_block_badges_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index fee24ff..b97b4ff 100644 (file)
 function xmldb_block_calendar_month_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 4134778..f99b9ea 100644 (file)
 function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 3535e7e..b30d8f2 100644 (file)
@@ -48,12 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_completionstatus_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 6f3406e..18a78cf 100644 (file)
@@ -48,12 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_course_summary_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 56f328f..52176df 100644 (file)
@@ -33,12 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_html_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index ecf4b3d..c8ca634 100644 (file)
@@ -55,12 +55,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_navigation_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index a110739..7b7aab0 100644 (file)
 function xmldb_block_quiz_results_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index d4c5f04..84dfeab 100644 (file)
@@ -47,12 +47,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_recent_activity_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 3379813..844c8c6 100644 (file)
@@ -33,12 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_rss_client_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 558a8cc..d14edc2 100644 (file)
@@ -49,12 +49,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_section_links_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 2e776a6..ad3b539 100644 (file)
@@ -48,12 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_selfcompletion_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 0eb69d5..afe0e14 100644 (file)
@@ -55,12 +55,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_settings_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.3.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Automatically generated Moodle v3.4.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
index 22c0b2e..3b3f2b2 100644 (file)
Binary files a/blocks/timeline/amd/build/event_list.min.js and b/blocks/timeline/amd/build/event_list.min.js differ
index 52c0abd..4d8f7bd 100644 (file)
Binary files a/blocks/timeline/amd/build/event_list.min.js.map and b/blocks/timeline/amd/build/event_list.min.js.map differ
index 3545cbf..c7c504e 100644 (file)
@@ -266,8 +266,11 @@ function(
             }
 
             var calendarEvents = result.events.filter(function(event) {
-                // Do not include events that does not have a due date.
-                return event.eventtype != "open" && event.eventtype != "opensubmission";
+                if (event.eventtype == "open" || event.eventtype == "opensubmission") {
+                    var dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);
+                    return dayTimestamp > midnight;
+                }
+                return true;
             });
             // We expect to receive limit + 1 events back from the server.
             // Any less means there are no more events to load.
index cdf6654..68af2cd 100644 (file)
@@ -25,13 +25,16 @@ use MongoDB\Exception\BadMethodCallException;
  */
 class BulkWriteResult
 {
+    /** @var WriteResult */
     private $writeResult;
+
+    /** @var mixed[] */
     private $insertedIds;
+
+    /** @var boolean */
     private $isAcknowledged;
 
     /**
-     * Constructor.
-     *
      * @param WriteResult $writeResult
      * @param mixed[]     $insertedIds
      */
index 98a703f..3c360fa 100644 (file)
 
 namespace MongoDB;
 
-use MongoDB\BSON\Serializable;
-use MongoDB\Driver\Cursor;
+use Iterator;
+use MongoDB\Driver\CursorId;
 use MongoDB\Driver\Exception\ConnectionException;
 use MongoDB\Driver\Exception\RuntimeException;
 use MongoDB\Driver\Exception\ServerException;
-use MongoDB\Exception\InvalidArgumentException;
 use MongoDB\Exception\ResumeTokenException;
-use IteratorIterator;
-use Iterator;
+use MongoDB\Model\ChangeStreamIterator;
+use function call_user_func;
+use function in_array;
 
 /**
  * Iterator for a change stream.
@@ -42,27 +42,39 @@ class ChangeStream implements Iterator
      */
     const CURSOR_NOT_FOUND = 43;
 
-    private static $errorCodeCappedPositionLost = 136;
-    private static $errorCodeInterrupted = 11601;
-    private static $errorCodeCursorKilled = 237;
+    /** @var array */
+    private static $nonResumableErrorCodes = [
+        136, // CappedPositionLost
+        237, // CursorKilled
+        11601, // Interrupted
+    ];
 
-    private $resumeToken;
+    /** @var callable */
     private $resumeCallable;
-    private $csIt;
+
+    /** @var ChangeStreamIterator */
+    private $iterator;
+
+    /** @var integer */
     private $key = 0;
-    private $hasAdvanced = false;
 
     /**
-     * Constructor.
+     * Whether the change stream has advanced to its first result. This is used
+     * to determine whether $key should be incremented after an iteration event.
      *
+     * @var boolean
+     */
+    private $hasAdvanced = false;
+
+    /**
      * @internal
-     * @param Cursor $cursor
-     * @param callable $resumeCallable
+     * @param ChangeStreamIterator $iterator
+     * @param callable             $resumeCallable
      */
-    public function __construct(Cursor $cursor, callable $resumeCallable)
+    public function __construct(ChangeStreamIterator $iterator, callable $resumeCallable)
     {
+        $this->iterator = $iterator;
         $this->resumeCallable = $resumeCallable;
-        $this->csIt = new IteratorIterator($cursor);
     }
 
     /**
@@ -71,15 +83,29 @@ class ChangeStream implements Iterator
      */
     public function current()
     {
-        return $this->csIt->current();
+        return $this->iterator->current();
     }
 
     /**
-     * @return \MongoDB\Driver\CursorId
+     * @return CursorId
      */
     public function getCursorId()
     {
-        return $this->csIt->getInnerIterator()->getId();
+        return $this->iterator->getInnerIterator()->getId();
+    }
+
+    /**
+     * Returns the resume token for the iterator's current position.
+     *
+     * Null may be returned if no change documents have been iterated and the
+     * server did not include a postBatchResumeToken in its aggregate or getMore
+     * command response.
+     *
+     * @return array|object|null
+     */
+    public function getResumeToken()
+    {
+        return $this->iterator->getResumeToken();
     }
 
     /**
@@ -91,60 +117,40 @@ class ChangeStream implements Iterator
         if ($this->valid()) {
             return $this->key;
         }
+
         return null;
     }
 
     /**
      * @see http://php.net/iterator.next
      * @return void
+     * @throws ResumeTokenException
      */
     public function next()
     {
         try {
-            $this->csIt->next();
-            if ($this->valid()) {
-                if ($this->hasAdvanced) {
-                    $this->key++;
-                }
-                $this->hasAdvanced = true;
-                $this->resumeToken = $this->extractResumeToken($this->csIt->current());
-            }
-            /* If the cursorId is 0, the server has invalidated the cursor so we
-             * will never perform another getMore. This means that we cannot
-             * resume and we can therefore unset the resumeCallable, which will
-             * free any reference to Watch. This will also free the only
-             * reference to an implicit session, since any such reference
-             * belongs to Watch. */
-            if ((string) $this->getCursorId() === '0') {
-                $this->resumeCallable = null;
-            }
+            $this->iterator->next();
+            $this->onIteration($this->hasAdvanced);
         } catch (RuntimeException $e) {
-            if ($this->isResumableError($e)) {
-                $this->resume();
-            }
+            $this->resumeOrThrow($e);
         }
     }
 
     /**
      * @see http://php.net/iterator.rewind
      * @return void
+     * @throws ResumeTokenException
      */
     public function rewind()
     {
         try {
-            $this->csIt->rewind();
-            if ($this->valid()) {
-                $this->hasAdvanced = true;
-                $this->resumeToken = $this->extractResumeToken($this->csIt->current());
-            }
-            // As with next(), free the callable once we know it will never be used.
-            if ((string) $this->getCursorId() === '0') {
-                $this->resumeCallable = null;
-            }
+            $this->iterator->rewind();
+            /* Unlike next() and resume(), the decision to increment the key
+             * does not depend on whether the change stream has advanced. This
+             * ensures that multiple calls to rewind() do not alter state. */
+            $this->onIteration(false);
         } catch (RuntimeException $e) {
-            if ($this->isResumableError($e)) {
-                $this->resume();
-            }
+            $this->resumeOrThrow($e);
         }
     }
 
@@ -154,75 +160,95 @@ class ChangeStream implements Iterator
      */
     public function valid()
     {
-        return $this->csIt->valid();
+        return $this->iterator->valid();
     }
 
     /**
-     * Extracts the resume token (i.e. "_id" field) from the change document.
+     * Determines if an exception is a resumable error.
      *
-     * @param array|document $document Change document
-     * @return mixed
-     * @throws InvalidArgumentException
-     * @throws ResumeTokenException if the resume token is not found or invalid
+     * @see https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.rst#resumable-error
+     * @param RuntimeException $exception
+     * @return boolean
      */
-    private function extractResumeToken($document)
+    private function isResumableError(RuntimeException $exception)
     {
-        if ( ! is_array($document) && ! is_object($document)) {
-            throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
+        if ($exception instanceof ConnectionException) {
+            return true;
         }
 
-        if ($document instanceof Serializable) {
-            return $this->extractResumeToken($document->bsonSerialize());
+        if (! $exception instanceof ServerException) {
+            return false;
         }
 
-        $resumeToken = is_array($document)
-            ? (isset($document['_id']) ? $document['_id'] : null)
-            : (isset($document->_id) ? $document->_id : null);
-
-        if ( ! isset($resumeToken)) {
-            throw ResumeTokenException::notFound();
+        if ($exception->hasErrorLabel('NonResumableChangeStreamError')) {
+            return false;
         }
 
-        if ( ! is_array($resumeToken) && ! is_object($resumeToken)) {
-            throw ResumeTokenException::invalidType($resumeToken);
+        if (in_array($exception->getCode(), self::$nonResumableErrorCodes)) {
+            return false;
         }
 
-        return $resumeToken;
+        return true;
     }
 
     /**
-     * Determines if an exception is a resumable error.
+     * Perform housekeeping after an iteration event.
      *
-     * @see https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.rst#resumable-error
-     * @param RuntimeException $exception
-     * @return boolean
+     * @param boolean $incrementKey Increment $key if there is a current result
+     * @throws ResumeTokenException
      */
-    private function isResumableError(RuntimeException $exception)
+    private function onIteration($incrementKey)
     {
-        if ($exception instanceof ConnectionException) {
-            return true;
+        /* If the cursorId is 0, the server has invalidated the cursor and we
+         * will never perform another getMore nor need to resume since any
+         * remaining results (up to and including the invalidate event) will
+         * have been received in the last response. Therefore, we can unset the
+         * resumeCallable. This will free any reference to Watch as well as the
+         * only reference to any implicit session created therein. */
+        if ((string) $this->getCursorId() === '0') {
+            $this->resumeCallable = null;
         }
 
-        if ( ! $exception instanceof ServerException) {
-            return false;
+        /* Return early if there is not a current result. Avoid any attempt to
+         * increment the iterator's key. */
+        if (! $this->valid()) {
+            return;
         }
 
-        if (in_array($exception->getCode(), [self::$errorCodeCappedPositionLost, self::$errorCodeCursorKilled, self::$errorCodeInterrupted])) {
-            return false;
+        if ($incrementKey) {
+            $this->key++;
         }
 
-        return true;
+        $this->hasAdvanced = true;
     }
 
     /**
-     * Creates a new changeStream after a resumable server error.
+     * Recreates the ChangeStreamIterator after a resumable server error.
      *
      * @return void
      */
     private function resume()
     {
-        $newChangeStream = call_user_func($this->resumeCallable, $this->resumeToken);
-        $this->csIt = $newChangeStream->csIt;
-        $this->csIt->rewind();
+        $this->iterator = call_user_func($this->resumeCallable, $this->getResumeToken(), $this->hasAdvanced);
+        $this->iterator->rewind();
+
+        $this->onIteration($this->hasAdvanced);
+    }
+
+    /**
+     * Either resumes after a resumable error or re-throws the exception.
+     *
+     * @param RuntimeException $exception
+     * @throws RuntimeException
+     */
+    private function resumeOrThrow(RuntimeException $exception)
+    {
+        if ($this->isResumableError($exception)) {
+            $this->resume();
+
+            return;
+        }
+
+        throw $exception;
     }
 }
index be26ec5..2031080 100644 (file)
 
 namespace MongoDB;
 
+use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
 use MongoDB\Driver\Manager;
 use MongoDB\Driver\ReadConcern;
 use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\Session;
 use MongoDB\Driver\WriteConcern;
-use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
-use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException;
 use MongoDB\Exception\InvalidArgumentException;
 use MongoDB\Exception\UnexpectedValueException;
 use MongoDB\Exception\UnsupportedException;
+use MongoDB\Model\BSONArray;
+use MongoDB\Model\BSONDocument;
 use MongoDB\Model\DatabaseInfoIterator;
 use MongoDB\Operation\DropDatabase;
 use MongoDB\Operation\ListDatabases;
 use MongoDB\Operation\Watch;
+use function is_array;
 
 class Client
 {
+    /** @var array */
     private static $defaultTypeMap = [
-        'array' => 'MongoDB\Model\BSONArray',
-        'document' => 'MongoDB\Model\BSONDocument',
-        'root' => 'MongoDB\Model\BSONDocument',
+        'array' => BSONArray::class,
+        'document' => BSONDocument::class,
+        'root' => BSONDocument::class,
     ];
+
+    /** @var integer */
     private static $wireVersionForReadConcern = 4;
+
+    /** @var integer */
     private static $wireVersionForWritableCommandWriteConcern = 5;
 
+    /** @var Manager */
     private $manager;
+
+    /** @var ReadConcern */
     private $readConcern;
+
+    /** @var ReadPreference */
     private $readPreference;
+
+    /** @var string */
     private $uri;
+
+    /** @var array */
     private $typeMap;
+
+    /** @var WriteConcern */
     private $writeConcern;
 
     /**
@@ -146,13 +166,13 @@ class Client
      */
     public function dropDatabase($databaseName, array $options = [])
     {
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
@@ -217,6 +237,7 @@ class Client
      * List databases.
      *
      * @see ListDatabases::__construct() for supported options
+     * @param array $options
      * @return DatabaseInfoIterator
      * @throws UnexpectedValueException if the command response was malformed
      * @throws InvalidArgumentException for parameter/option parsing errors
@@ -225,7 +246,7 @@ class Client
     public function listDatabases(array $options = [])
     {
         $operation = new ListDatabases($options);
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
         return $operation->execute($server);
     }
@@ -267,8 +288,8 @@ class Client
      * Start a new client session.
      *
      * @see http://php.net/manual/en/mongodb-driver-manager.startsession.php
-     * @param array  $options      Session options
-     * @return MongoDB\Driver\Session
+     * @param array $options Session options
+     * @return Session
      */
     public function startSession(array $options = [])
     {
@@ -286,17 +307,17 @@ class Client
      */
     public function watch(array $pipeline = [], array $options = [])
     {
-        if ( ! isset($options['readPreference'])) {
+        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
             $options['readPreference'] = $this->readPreference;
         }
 
-        $server = $this->manager->selectServer($options['readPreference']);
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
             $options['readConcern'] = $this->readConcern;
         }
 
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
index 769e835..8238644 100644 (file)
 namespace MongoDB;
 
 use MongoDB\BSON\JavascriptInterface;
-use MongoDB\BSON\Serializable;
-use MongoDB\ChangeStream;
 use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
 use MongoDB\Driver\Manager;
 use MongoDB\Driver\ReadConcern;
 use MongoDB\Driver\ReadPreference;
 use MongoDB\Driver\WriteConcern;
-use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
 use MongoDB\Exception\InvalidArgumentException;
 use MongoDB\Exception\UnexpectedValueException;
 use MongoDB\Exception\UnsupportedException;
+use MongoDB\Model\BSONArray;
+use MongoDB\Model\BSONDocument;
 use MongoDB\Model\IndexInfo;
 use MongoDB\Model\IndexInfoIterator;
 use MongoDB\Operation\Aggregate;
 use MongoDB\Operation\BulkWrite;
-use MongoDB\Operation\CreateIndexes;
 use MongoDB\Operation\Count;
 use MongoDB\Operation\CountDocuments;
+use MongoDB\Operation\CreateIndexes;
 use MongoDB\Operation\DeleteMany;
 use MongoDB\Operation\DeleteOne;
 use MongoDB\Operation\Distinct;
@@ -58,24 +58,52 @@ use MongoDB\Operation\UpdateMany;
 use MongoDB\Operation\UpdateOne;
 use MongoDB\Operation\Watch;
 use Traversable;
+use function array_diff_key;
+use function array_intersect_key;
+use function current;
+use function is_array;
+use function strlen;
 
 class Collection
 {
+    /** @var array */
     private static $defaultTypeMap = [
-        'array' => 'MongoDB\Model\BSONArray',
-        'document' => 'MongoDB\Model\BSONDocument',
-        'root' => 'MongoDB\Model\BSONDocument',
+        'array' => BSONArray::class,
+        'document' => BSONDocument::class,
+        'root' => BSONDocument::class,
     ];
+
+    /** @var integer */
     private static $wireVersionForFindAndModifyWriteConcern = 4;
+
+    /** @var integer */
     private static $wireVersionForReadConcern = 4;
+
+    /** @var integer */
     private static $wireVersionForWritableCommandWriteConcern = 5;
 
+    /** @var integer */
+    private static $wireVersionForReadConcernWithWriteStage = 8;
+
+    /** @var string */
     private $collectionName;
+
+    /** @var string */
     private $databaseName;
+
+    /** @var Manager */
     private $manager;
+
+    /** @var ReadConcern */
     private $readConcern;
+
+    /** @var ReadPreference */
     private $readPreference;
+
+    /** @var array */
     private $typeMap;
+
+    /** @var WriteConcern */
     private $writeConcern;
 
     /**
@@ -116,11 +144,11 @@ class Collection
         }
 
         if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
-            throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+            throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], ReadConcern::class);
         }
 
         if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
-            throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+            throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], ReadPreference::class);
         }
 
         if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
@@ -128,7 +156,7 @@ class Collection
         }
 
         if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
-            throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+            throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class);
         }
 
         $this->manager = $manager;
@@ -189,32 +217,39 @@ class Collection
      */
     public function aggregate(array $pipeline, array $options = [])
     {
-        $hasOutStage = \MongoDB\is_last_pipeline_operator_out($pipeline);
+        $hasWriteStage = is_last_pipeline_operator_write($pipeline);
 
-        if ( ! isset($options['readPreference'])) {
+        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
             $options['readPreference'] = $this->readPreference;
         }
 
-        if ($hasOutStage) {
+        if ($hasWriteStage) {
             $options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
         }
 
-        $server = $this->manager->selectServer($options['readPreference']);
+        $server = select_server($this->manager, $options);
 
-        /* A "majority" read concern is not compatible with the $out stage, so
-         * avoid providing the Collection's read concern if it would conflict.
+        /* MongoDB 4.2 and later supports a read concern when an $out stage is
+         * being used, but earlier versions do not.
+         *
+         * A read concern is also not compatible with transactions.
          */
-        if ( ! isset($options['readConcern']) &&
-             ! ($hasOutStage && $this->readConcern->getLevel() === ReadConcern::MAJORITY) &&
-            \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+        if (! isset($options['readConcern']) &&
+            server_supports_feature($server, self::$wireVersionForReadConcern) &&
+            ! is_in_transaction($options) &&
+            ( ! $hasWriteStage || server_supports_feature($server, self::$wireVersionForReadConcernWithWriteStage))
+        ) {
             $options['readConcern'] = $this->readConcern;
         }
 
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
-        if ($hasOutStage && ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+        if ($hasWriteStage &&
+            ! isset($options['writeConcern']) &&
+            server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) &&
+            ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
@@ -236,12 +271,12 @@ class Collection
      */
     public function bulkWrite(array $operations, array $options = [])
     {
-        if ( ! isset($options['writeConcern'])) {
+        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
         $operation = new BulkWrite($this->databaseName, $this->collectionName, $operations, $options);
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
         return $operation->execute($server);
     }
@@ -262,13 +297,13 @@ class Collection
      */
     public function count($filter = [], array $options = [])
     {
-        if ( ! isset($options['readPreference'])) {
+        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
             $options['readPreference'] = $this->readPreference;
         }
 
-        $server = $this->manager->selectServer($options['readPreference']);
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
             $options['readConcern'] = $this->readConcern;
         }
 
@@ -291,13 +326,13 @@ class Collection
      */
     public function countDocuments($filter = [], array $options = [])
     {
-        if ( ! isset($options['readPreference'])) {
+        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
             $options['readPreference'] = $this->readPreference;
         }
 
-        $server = $this->manager->selectServer($options['readPreference']);
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
             $options['readConcern'] = $this->readConcern;
         }
 
@@ -357,9 +392,9 @@ class Collection
      */
     public function createIndexes(array $indexes, array $options = [])
     {
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
@@ -382,12 +417,12 @@ class Collection
      */
     public function deleteMany($filter, array $options = [])
     {
-        if ( ! isset($options['writeConcern'])) {
+        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
         $operation = new DeleteMany($this->databaseName, $this->collectionName, $filter, $options);
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
         return $operation->execute($server);
     }
@@ -406,12 +441,12 @@ class Collection
      */
     public function deleteOne($filter, array $options = [])
     {
-        if ( ! isset($options['writeConcern'])) {
+        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
         $operation = new DeleteOne($this->databaseName, $this->collectionName, $filter, $options);
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
         return $operation->execute($server);
     }
@@ -420,9 +455,9 @@ class Collection
      * Finds the distinct values for a specified field across the collection.
      *
      * @see Distinct::__construct() for supported options
-     * @param string $fieldName Field for which to return distinct values
-     * @param array|object $filter  Query by which to filter documents
-     * @param array        $options Command options
+     * @param string       $fieldName Field for which to return distinct values
+     * @param array|object $filter    Query by which to filter documents
+     * @param array        $options   Command options
      * @return mixed[]
      * @throws UnexpectedValueException if the command response was malformed
      * @throws UnsupportedException if options are not supported by the selected server
@@ -431,13 +466,17 @@ class Collection
      */
     public function distinct($fieldName, $filter = [], array $options = [])
     {
-        if ( ! isset($options['readPreference'])) {
+        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
             $options['readPreference'] = $this->readPreference;
         }
 
-        $server = $this->manager->selectServer($options['readPreference']);
+        if (! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
             $options['readConcern'] = $this->readConcern;
         }
 
@@ -458,13 +497,13 @@ class Collection
      */
     public function drop(array $options = [])
     {
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
@@ -478,7 +517,7 @@ class Collection
      *
      * @see DropIndexes::__construct() for supported options
      * @param string|IndexInfo $indexName Index name or model object
-     * @param array  $options   Additional options
+     * @param array            $options   Additional options
      * @return array|object Command result document
      * @throws UnsupportedException if options are not supported by the selected server
      * @throws InvalidArgumentException for parameter/option parsing errors
@@ -492,13 +531,13 @@ class Collection
             throw new InvalidArgumentException('dropIndexes() must be used to drop multiple indexes');
         }
 
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
@@ -519,13 +558,13 @@ class Collection
      */
     public function dropIndexes(array $options = [])
     {
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
@@ -545,15 +584,15 @@ class Collection
      * @throws InvalidArgumentException for parameter/option parsing errors
      * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
      */
-    public function EstimatedDocumentCount(array $options = [])
+    public function estimatedDocumentCount(array $options = [])
     {
-        if ( ! isset($options['readPreference'])) {
+        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
             $options['readPreference'] = $this->readPreference;
         }
 
-        $server = $this->manager->selectServer($options['readPreference']);
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
             $options['readConcern'] = $this->readConcern;
         }
 
@@ -567,8 +606,8 @@ class Collection
      *
      * @see Explain::__construct() for supported options
      * @see http://docs.mongodb.org/manual/reference/command/explain/
-     * @param Explainable $explainable  Command on which to run explain
-     * @param array       $options      Additional options
+     * @param Explainable $explainable Command on which to run explain
+     * @param array       $options     Additional options
      * @return array|object
      * @throws UnsupportedException if explainable or options are not supported by the selected server
      * @throws InvalidArgumentException for parameter/option parsing errors
@@ -576,15 +615,15 @@ class Collection
      */
     public function explain(Explainable $explainable, array $options = [])
     {
-        if ( ! isset($options['readPreference'])) {
+        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
             $options['readPreference'] = $this->readPreference;
         }
 
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
-        $server = $this->manager->selectServer($options['readPreference']);
+        $server = select_server($this->manager, $options);
 
         $operation = new Explain($this->databaseName, $explainable, $options);
 
@@ -605,17 +644,17 @@ class Collection
      */
     public function find($filter = [], array $options = [])
     {
-        if ( ! isset($options['readPreference'])) {
+        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
             $options['readPreference'] = $this->readPreference;
         }
 
-        $server = $this->manager->selectServer($options['readPreference']);
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
             $options['readConcern'] = $this->readConcern;
         }
 
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
@@ -638,17 +677,17 @@ class Collection
      */
     public function findOne($filter = [], array $options = [])
     {
-        if ( ! isset($options['readPreference'])) {
+        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
             $options['readPreference'] = $this->readPreference;
         }
 
-        $server = $this->manager->selectServer($options['readPreference']);
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
             $options['readConcern'] = $this->readConcern;
         }
 
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
@@ -674,13 +713,13 @@ class Collection
      */
     public function findOneAndDelete($filter, array $options = [])
     {
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern)) {
+        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern) && ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
@@ -711,13 +750,13 @@ class Collection
      */
     public function findOneAndReplace($filter, $replacement, array $options = [])
     {
-        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+        $server = select_server($this->manager, $options);
 
-        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern)) {
+        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern) && ! is_in_transaction($options)) {
             $options['writeConcern'] = $this->writeConcern;
         }
 
-        if ( ! isset($options['typeMap'])) {
+        if (! isset($options['typeMap'])) {
             $options['typeMap'] = $this->typeMap;
         }
 
@@ -748,13 +787,13 @@ class Collection
<