Merge branch 'MDL-66883-master' of git://github.com/bmbrands/moodle
authorJun Pataleta <jun@moodle.com>
Mon, 20 Jan 2020 08:36:40 +0000 (16:36 +0800)
committerJun Pataleta <jun@moodle.com>
Mon, 20 Jan 2020 08:36:40 +0000 (16:36 +0800)
657 files changed:
.travis.yml
Gruntfile.js
GruntfileComponents.js [new file with mode: 0644]
admin/category.php
admin/cli/cron.php
admin/cli/upgrade.php
admin/environment.xml
admin/index.php
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/settings/server.php
admin/templates/setting.mustache
admin/templates/setting_configpasswordunmask.mustache
admin/templates/settings_search_results.mustache
admin/tests/behat/filter_users.feature
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/behat/tests/behat/get_and_set_fields.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/data_request.php
admin/tool/dataprivacy/classes/task/process_data_request_task.php
admin/tool/dataprivacy/createdatarequest.php
admin/tool/dataprivacy/db/install.xml
admin/tool/dataprivacy/db/upgrade.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/settings.php
admin/tool/dataprivacy/templates/component_status.mustache
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/datadelete.feature
admin/tool/dataprivacy/tests/behat/dataexport.feature
admin/tool/dataprivacy/version.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/policy/amd/build/jquery-eu-cookie-law-popup.min.js
admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js.map
admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
admin/tool/policy/readme_moodle.txt
admin/tool/policy/thirdpartylibs.xml
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/uploaduser/tests/behat/upload_users.feature
admin/tool/usertours/amd/build/tour.min.js
admin/tool/usertours/amd/build/tour.min.js.map
admin/tool/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/CAS/CAS.php
auth/cas/CAS/CAS/Client.php
auth/cas/CAS/README.md
auth/cas/CAS/moodle_readme.txt
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/backup.class.php
backup/backup.php
backup/controller/backup_controller.class.php
backup/controller/base_controller.class.php
backup/controller/restore_controller.class.php
backup/externallib.php
backup/restore.php
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/classes/local/event/strategies/raw_event_retrieval_strategy.php
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/classes/category.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/lib.php
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/singleview/templates/bulk_insert.mustache
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/autogroup.php
group/classes/output/user_groups_editable.php
group/externallib.php
group/index.php
group/lib.php
group/overview.php
group/templates/group_details.mustache
group/tests/behat/auto_creation.feature
group/tests/behat/create_groups.feature
install/lang/ar_wp/langconfig.php [moved from mod/lti/service/toolproxy/db/renamedclasses.php with 61% similarity]
install/lang/el/error.php
install/lang/fr_incl/langconfig.php [moved from mod/lti/service/memberships/db/renamedclasses.php with 59% similarity]
install/lang/km/admin.php
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/plugin.php
lang/en/role.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/mustache.min.js
lib/amd/build/mustache.min.js.map
lib/amd/build/notification.min.js
lib/amd/build/notification.min.js.map
lib/amd/build/tag.min.js
lib/amd/build/tag.min.js.map
lib/amd/src/adapter.js
lib/amd/src/loglevel.js
lib/amd/src/mustache.js
lib/amd/src/notification.js
lib/amd/src/tag.js
lib/antivirus/clamav/adminlib.php
lib/antivirus/clamav/classes/scanner.php
lib/antivirus/clamav/db/upgrade.php
lib/antivirus/clamav/lang/en/antivirus_clamav.php
lib/antivirus/clamav/settings.php
lib/antivirus/clamav/tests/scanner_test.php
lib/antivirus/clamav/version.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/local/cli/shutdown.php [new file with mode: 0644]
lib/classes/lock/postgres_lock_factory.php
lib/classes/plugin_manager.php
lib/classes/plugininfo/base.php
lib/classes/shutdown_manager.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/datalib.php
lib/db/access.php
lib/db/install.php
lib/db/install.xml
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/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js
lib/editor/atto/plugins/accessibilitychecker/yui/src/button/js/button.js
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/form/templatable_form_element.php
lib/htmlpurifier/HTMLPurifier.php
lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Bool.php
lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php
lib/htmlpurifier/HTMLPurifier/CSSDefinition.php
lib/htmlpurifier/HTMLPurifier/Config.php
lib/htmlpurifier/HTMLPurifier/ConfigSchema.php
lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema.ser
lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt
lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt [new file with mode: 0644]
lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt
lib/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer/README [changed mode: 0644->0755]
lib/htmlpurifier/HTMLPurifier/EntityParser.php
lib/htmlpurifier/HTMLPurifier/HTMLModule.php
lib/htmlpurifier/HTMLPurifier/HTMLModule/SafeScripting.php
lib/htmlpurifier/HTMLPurifier/Language/messages/en-x-test.php
lib/htmlpurifier/HTMLPurifier/Language/messages/en-x-testmini.php
lib/htmlpurifier/HTMLPurifier/Lexer/DOMLex.php
lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php
lib/htmlpurifier/HTMLPurifier/VarParser.php
lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php
lib/htmlpurifier/readme_moodle.txt
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/mlbackend/python/classes/processor.php
lib/moodlelib.php
lib/mustache/readme_moodle.txt
lib/mustache/src/Mustache/Engine.php
lib/mustache/src/Mustache/Parser.php
lib/mustache/src/Mustache/Tokenizer.php
lib/plagiarismlib.php
lib/questionlib.php
lib/requirejs.php
lib/scssphp/Cache.php
lib/scssphp/Colors.php
lib/scssphp/Compiler.php
lib/scssphp/Formatter.php
lib/scssphp/Formatter/Expanded.php
lib/scssphp/Formatter/Nested.php
lib/scssphp/Node/Number.php
lib/scssphp/Parser.php
lib/scssphp/SourceMap/Base64VLQ.php
lib/scssphp/SourceMap/Base64VLQEncoder.php [deleted file]
lib/scssphp/Version.php
lib/scssphp/moodle_readme.txt
lib/setup.php
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/app_behat_runtime.js
lib/tests/behat/behat_permissions.php
lib/tests/fixtures/repeated_events.ics [new file with mode: 0644]
lib/tests/fixtures/testable_plugin_manager.php
lib/tests/fixtures/testable_plugininfo_base.php
lib/tests/moodlelib_test.php
lib/tests/plugin_manager_test.php
lib/tests/plugininfo/base_test.php [new file with mode: 0644]
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
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/chooserdialogue/js/chooserdialogue.js
lib/yui/src/notification/js/dialogue.js
message/amd/build/message_drawer_view_overview_section.min.js
message/amd/build/message_drawer_view_overview_section.min.js.map
message/amd/src/message_drawer_view_overview_section.js
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/locallib.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/amd/build/local/layout/fullscreen.min.js
mod/forum/amd/build/local/layout/fullscreen.min.js.map
mod/forum/amd/src/local/layout/fullscreen.js
mod/forum/classes/local/exporters/author.php
mod/forum/classes/local/exporters/discussion_summary.php
mod/forum/classes/local/factories/url.php
mod/forum/classes/local/renderers/discussion.php
mod/forum/classes/post_form.php
mod/forum/db/upgrade.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/mod_form.php
mod/forum/post.php
mod/forum/report/summary/lang/en/forumreport_summary.php
mod/forum/report/summary/tests/behat/summary_filter_groups.feature [new file with mode: 0644]
mod/forum/report/summary/tests/behat/summary_filter_no_groups.feature [new file with mode: 0644]
mod/forum/templates/discussion_list.mustache
mod/forum/templates/forum_action_menu.mustache
mod/forum/templates/forum_discussion_favourite_toggle.mustache
mod/forum/templates/forum_discussion_nested_v2_first_post.mustache
mod/forum/templates/forum_discussion_post.mustache
mod/forum/templates/inpage_reply.mustache
mod/forum/templates/inpage_reply_v2.mustache
mod/forum/templates/local/grades/local/grader/grading.mustache
mod/forum/templates/local/grades/local/grader/navigation.mustache
mod/forum/templates/local/grades/view_grade.mustache
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/tests/exporters_author_test.php
mod/forum/tests/externallib_test.php
mod/forum/tests/mail_test.php
mod/forum/version.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/access.php
mod/lti/db/upgrade.php
mod/lti/lang/en/lti.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/mod_form.js
mod/lti/mod_form.php
mod/lti/service/toolsettings/db/renamedclasses.php [deleted file]
mod/lti/version.php
mod/page/db/upgrade.php
mod/quiz/addrandomform.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/quiz/styles.css
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/renderable.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/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/src/loader.js
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/boost/templates/navbar-secure.mustache
theme/boost/templates/navbar.mustache
theme/classic/style/moodle.css
theme/classic/templates/navbar-secure.mustache
theme/classic/templates/navbar.mustache
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 d7d1646..fe72683 100644 (file)
@@ -74,4 +74,6 @@ if ($options['stop']) {
     die;
 }
 
+\core\local\cli\shutdown::script_supports_graceful_exit();
+
 cron_run();
index d827e6d..12904e5 100644 (file)
@@ -137,7 +137,7 @@ if (!$envstatus) {
 
 // Test plugin dependencies.
 $failed = array();
-if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
     cli_problem(get_string('pluginscheckfailed', 'admin', array('pluginslist' => implode(', ', array_unique($failed)))));
     cli_error(get_string('pluginschecktodo', 'admin'));
 }
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 c34a437..c95c73c 100644 (file)
@@ -241,7 +241,7 @@ if (!core_tables_exist()) {
 
     // check plugin dependencies
     $failed = array();
-    if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+    if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
         $PAGE->navbar->add(get_string('pluginscheck', 'admin'));
         $PAGE->set_title($strinstallation);
         $PAGE->set_heading($strinstallation . ' - Moodle ' . $CFG->target_release);
@@ -508,7 +508,7 @@ if (!$cache and $version > $CFG->version) {  // upgrade
     } else {
         // Always verify plugin dependencies!
         $failed = array();
-        if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+        if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
             echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
             die();
         }
@@ -682,7 +682,7 @@ if (!$cache and moodle_needs_upgrading()) {
 
         // Make sure plugin dependencies are always checked.
         $failed = array();
-        if (!$pluginman->all_plugins_ok($version, $failed)) {
+        if (!$pluginman->all_plugins_ok($version, $failed, $CFG->branch)) {
             $output = $PAGE->get_renderer('core', 'admin');
             echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
             die();
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..4dbe8c0 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');
     }
 
     /**
@@ -983,7 +983,7 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string HTML code
      */
     public function plugins_check_table(core_plugin_manager $pluginman, $version, array $options = array()) {
-
+        global $CFG;
         $plugininfo = $pluginman->get_plugins();
 
         if (empty($plugininfo)) {
@@ -1069,8 +1069,10 @@ class core_admin_renderer extends plugin_renderer_base {
                 }
 
                 $coredependency = $plugin->is_core_dependency_satisfied($version);
+                $incompatibledependency = $plugin->is_core_compatible_satisfied($CFG->branch);
+
                 $otherpluginsdependencies = $pluginman->are_dependencies_satisfied($plugin->get_other_required_plugins());
-                $dependenciesok = $coredependency && $otherpluginsdependencies;
+                $dependenciesok = $coredependency && $otherpluginsdependencies && $incompatibledependency;
 
                 $statuscode = $plugin->get_status();
                 $row->attributes['class'] .= ' status-' . $statuscode;
@@ -1120,8 +1122,11 @@ class core_admin_renderer extends plugin_renderer_base {
                 }
 
                 $status = new html_table_cell($sourcelabel.' '.$status);
-
-                $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version));
+                if ($plugin->pluginsupported != null) {
+                    $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version, $CFG->branch));
+                } else {
+                    $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version));
+                }
 
                 $statusisboring = in_array($statuscode, array(
                         core_plugin_manager::PLUGIN_STATUS_NODB, core_plugin_manager::PLUGIN_STATUS_UPTODATE));
@@ -1452,15 +1457,17 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param \core\plugininfo\base $plugin the plugin we are rendering the row for.
      * @param core_plugin_manager $pluginman provides data on all the plugins.
      * @param string $version
+     * @param int $branch the current Moodle branch
      * @return string HTML code
      */
-    protected function required_column(\core\plugininfo\base $plugin, core_plugin_manager $pluginman, $version) {
+    protected function required_column(\core\plugininfo\base $plugin, core_plugin_manager $pluginman, $version, $branch = null) {
 
         $requires = array();
         $displayuploadlink = false;
         $displayupdateslink = false;
 
-        foreach ($pluginman->resolve_requirements($plugin, $version) as $reqname => $reqinfo) {
+        $requirements = $pluginman->resolve_requirements($plugin, $version, $branch);
+        foreach ($requirements as $reqname => $reqinfo) {
             if ($reqname === 'core') {
                 if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
                     $class = 'requires-ok';
@@ -1469,7 +1476,19 @@ class core_admin_renderer extends plugin_renderer_base {
                     $class = 'requires-failed';
                     $label = html_writer::span(get_string('dependencyfails', 'core_plugin'), 'badge badge-danger');
                 }
-                if ($reqinfo->reqver != ANY_VERSION) {
+
+                if ($branch != null && !$plugin->is_core_compatible_satisfied($branch)) {
+                    $requires[] = html_writer::tag('li',
+                    html_writer::span(get_string('incompatibleversion', 'core_plugin', $branch), 'dep dep-core').
+                    ' '.$label, array('class' => $class));
+
+                } else if ($branch != null && $plugin->pluginsupported != null) {
+                    $requires[] = html_writer::tag('li',
+                        html_writer::span(get_string('moodlebranch', 'core_plugin',
+                        array('min' => $plugin->pluginsupported[0], 'max' => $plugin->pluginsupported[1])), 'dep dep-core').
+                        ' '.$label, array('class' => $class));
+
+                } else if ($reqinfo->reqver != ANY_VERSION) {
                     $requires[] = html_writer::tag('li',
                         html_writer::span(get_string('moodleversion', 'core_plugin', $plugin->versionrequires), 'dep dep-core').
                         ' '.$label, array('class' => $class));
@@ -1556,6 +1575,13 @@ class core_admin_renderer extends plugin_renderer_base {
             );
         }
 
+        // Check if supports is present, and $branch is not in, only if $incompatible check was ok.
+        if ($plugin->pluginsupported != null && $class == 'requires-ok' && $branch != null) {
+            if ($pluginman->check_explicitly_supported($plugin, $branch) == $pluginman::VERSION_NOT_SUPPORTED) {
+                $out .= html_writer::div(get_string('notsupported', 'core_plugin', $branch));
+            }
+        }
+
         return $out;
 
     }
@@ -1902,7 +1928,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 +1941,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 4b0f427..0d55217 100644 (file)
@@ -44,15 +44,10 @@ $temp = new admin_settingpage('sessionhandling', new lang_string('sessionhandlin
 if (empty($CFG->session_handler_class) and $DB->session_lock_supported()) {
     $temp->add(new admin_setting_configcheckbox('dbsessions', new lang_string('dbsessions', 'admin'), new lang_string('configdbsessions', 'admin'), 0));
 }
-$temp->add(new admin_setting_configselect('sessiontimeout', new lang_string('sessiontimeout', 'admin'), new lang_string('configsessiontimeout', 'admin'), 7200, array(14400 => new lang_string('numhours', '', 4),
-                                                                                                                                                      10800 => new lang_string('numhours', '', 3),
-                                                                                                                                                      7200 => new lang_string('numhours', '', 2),
-                                                                                                                                                      5400 => new lang_string('numhours', '', '1.5'),
-                                                                                                                                                      3600 => new lang_string('numminutes', '', 60),
-                                                                                                                                                      2700 => new lang_string('numminutes', '', 45),
-                                                                                                                                                      1800 => new lang_string('numminutes', '', 30),
-                                                                                                                                                      900 => new lang_string('numminutes', '', 15),
-                                                                                                                                                      300 => new lang_string('numminutes', '', 5))));
+
+$temp->add(new admin_setting_configduration('sessiontimeout', new lang_string('sessiontimeout', 'admin'),
+    new lang_string('configsessiontimeout', 'admin'), 8 * 60 * 60));
+
 $temp->add(new admin_setting_configtext('sessioncookie', new lang_string('sessioncookie', 'admin'), new lang_string('configsessioncookie', 'admin'), '', PARAM_ALPHANUM));
 $temp->add(new admin_setting_configtext('sessioncookiepath', new lang_string('sessioncookiepath', 'admin'), new lang_string('configsessioncookiepath', 'admin'), '', PARAM_RAW));
 $temp->add(new admin_setting_configtext('sessioncookiedomain', new lang_string('sessioncookiedomain', 'admin'), new lang_string('configsessioncookiedomain', 'admin'), '', PARAM_RAW, 50));
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 f078fab..19644be 100644 (file)
@@ -259,9 +259,9 @@ Feature: Set up contextual data for tests
     Then the "groups" select box should contain "Group 1 (1)"
     And the "groups" select box should contain "Group 2 (1)"
     And I set the field "groups" to "Group 1 (1)"
-    And the "members" select box should contain "Student 1"
+    And the "members" select box should contain "Student 1 (student1@example.com)"
     And I set the field "groups" to "Group 2 (1)"
-    And the "members" select box should contain "Student 2"
+    And the "members" select box should contain "Student 2 (student2@example.com)"
 
   Scenario: Add cohorts and cohort members with data generator
     Given the following "categories" exist:
index 9ade5e0..3f1d88e 100644 (file)
@@ -147,13 +147,13 @@ Feature: Verify that all form fields values can be get and set
     And I navigate to "Users > Groups" in current page administration
     # Select (multi-select & AJAX) - Checking "I set the field" and "select box should contain".
     And I set the field "groups" to "Group 2"
-    And the "members" select box should contain "Student 2"
-    And the "members" select box should contain "Student 3"
-    And the "members" select box should not contain "Student 1"
+    And the "members" select box should contain "Student 2 (s2@example.com)"
+    And the "members" select box should contain "Student 3 (s3@example.com)"
+    And the "members" select box should not contain "Student 1 (s1@example.com)"
     And I set the field "groups" to "Group 1"
-    And the "members" select box should contain "Student 1"
-    And the "members" select box should contain "Student 2"
-    And the "members" select box should not contain "Student 3"
+    And the "members" select box should contain "Student 1 (s1@example.com)"
+    And the "members" select box should contain "Student 2 (s2@example.com)"
+    And the "members" select box should not contain "Student 3 (s3@example.com)"
     # Checkbox (AJAX) - Checking "I set the field" and "I set the following fields to these values".
     And I am on "Course 1" course homepage
     And I add a "Lesson" to section "1"
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..906dcb5 100644 (file)
@@ -258,7 +258,20 @@ class api {
         // The user making the request.
         $datarequest->set('requestedby', $requestinguser);
         // Set status.
-        $datarequest->set('status', self::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        $status = self::DATAREQUEST_STATUS_AWAITING_APPROVAL;
+        if (self::is_automatic_request_approval_on($type)) {
+            // Set status to approved if automatic data request approval is enabled.
+            $status = self::DATAREQUEST_STATUS_APPROVED;
+            // Set the privacy officer field if the one making the data request is a privacy officer.
+            if (self::is_site_dpo($requestinguser)) {
+                $datarequest->set('dpo', $requestinguser);
+            }
+            // Mark this request as system approved.
+            $datarequest->set('systemapproved', true);
+            // No need to notify privacy officer(s) about automatically approved data requests.
+            $notify = false;
+        }
+        $datarequest->set('status', $status);
         // Set request type.
         $datarequest->set('type', $type);
         // Set request comments.
@@ -269,13 +282,22 @@ class api {
         // Store subject access request.
         $datarequest->create();
 
+        // Queue the ad-hoc task for automatically approved data requests.
+        if ($status == self::DATAREQUEST_STATUS_APPROVED) {
+            $userid = null;
+            if ($type == self::DATAREQUEST_TYPE_EXPORT) {
+                $userid = $foruser;
+            }
+            self::queue_data_request_task($datarequest->get('id'), $userid);
+        }
+
         if ($notify) {
             // Get the list of the site Data Protection Officers.
-            $dpos = api::get_site_dpos();
+            $dpos = self::get_site_dpos();
 
             // Email the data request to the Data Protection Officer(s)/Admin(s).
             foreach ($dpos as $dpo) {
-                api::notify_dpo($dpo, $datarequest);
+                self::notify_dpo($dpo, $datarequest);
             }
         }
 
@@ -624,12 +646,11 @@ class api {
         $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
 
         // Fire an ad hoc task to initiate the data request process.
-        $task = new process_data_request_task();
-        $task->set_custom_data(['requestid' => $requestid]);
+        $userid = null;
         if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) {
-            $task->set_userid($request->get('userid'));
+            $userid = $request->get('userid');
         }
-        manager::queue_adhoc_task($task, true);
+        self::queue_data_request_task($requestid, $userid);
 
         return $result;
     }
@@ -712,7 +733,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
@@ -1277,4 +1298,35 @@ class api {
 
         return $formattedtime;
     }
+
+    /**
+     * Whether automatic data request approval is turned on or not for the given request type.
+     *
+     * @param int $type The request type.
+     * @return bool
+     */
+    public static function is_automatic_request_approval_on(int $type): bool {
+        switch ($type) {
+            case self::DATAREQUEST_TYPE_EXPORT:
+                return !empty(get_config('tool_dataprivacy', 'automaticdataexportapproval'));
+            case self::DATAREQUEST_TYPE_DELETE:
+                return !empty(get_config('tool_dataprivacy', 'automaticdatadeletionapproval'));
+        }
+        return false;
+    }
+
+    /**
+     * Creates an ad-hoc task for the data request.
+     *
+     * @param int $requestid The data request ID.
+     * @param int $userid Optional. The user ID to run the task as, if necessary.
+     */
+    public static function queue_data_request_task(int $requestid, int $userid = null): void {
+        $task = new process_data_request_task();
+        $task->set_custom_data(['requestid' => $requestid]);
+        if ($userid) {
+            $task->set_userid($userid);
+        }
+        manager::queue_adhoc_task($task, true);
+    }
 }
index 3901563..6a94fad 100644 (file)
@@ -118,6 +118,10 @@ class data_request extends persistent {
                 'type' => PARAM_INT,
                 'default' => FORMAT_PLAIN
             ],
+            'systemapproved' => [
+                'default' => false,
+                'type' => PARAM_BOOL,
+            ],
             'creationmethod' => [
                 'default' => self::DATAREQUEST_CREATION_MANUAL,
                 'choices' => [
index fe3652f..bb29c6c 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;
@@ -145,14 +146,20 @@ class process_data_request_task extends adhoc_task {
         mtrace('The processing of the user data request has been completed...');
 
         // Create message to notify the user regarding the processing results.
-        $dpo = core_user::get_user($request->dpo);
         $message = new message();
         $message->courseid = $SITE->id;
         $message->component = 'tool_dataprivacy';
         $message->name = 'datarequestprocessingresults';
-        $message->userfrom = $dpo;
-        $message->replyto = $dpo->email;
-        $message->replytoname = fullname($dpo->email);
+        if (empty($request->dpo)) {
+            // Use the no-reply user as the sender if the privacy officer is not set. This is the case for automatically
+            // approved requests.
+            $fromuser = core_user::get_noreply_user();
+        } else {
+            $fromuser = core_user::get_user($request->dpo);
+            $message->replyto = $fromuser->email;
+            $message->replytoname = fullname($fromuser);
+        }
+        $message->userfrom = $fromuser;
 
         $typetext = null;
         // Prepare the context data for the email message body.
@@ -180,7 +187,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 +200,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;
@@ -216,7 +225,7 @@ class process_data_request_task extends adhoc_task {
             if ($emailonly) {
                 // Do not sent an email if the user has been deleted. The user email has been previously deleted.
                 if (!$foruser->deleted) {
-                    $messagesent = email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
+                    $messagesent = email_to_user($foruser, $fromuser, $subject, $message->fullmessage, $messagehtml);
                 }
             } else {
                 $messagesent = message_send($message);
@@ -262,7 +271,7 @@ class process_data_request_task extends adhoc_task {
 
                 // Send message.
                 if ($emailonly) {
-                    email_to_user($requestedby, $dpo, $subject, $message->fullmessage, $messagehtml);
+                    email_to_user($requestedby, $fromuser, $subject, $message->fullmessage, $messagehtml);
                 } else {
                     message_send($message);
                 }
index b32dc15..2124400 100644 (file)
@@ -94,7 +94,11 @@ if ($data = $mform->get_data()) {
     if ($manage) {
         $foruser = core_user::get_user($data->userid);
         $redirectmessage = get_string('datarequestcreatedforuser', 'tool_dataprivacy', fullname($foruser));
+    } else if (\tool_dataprivacy\api::is_automatic_request_approval_on($data->type)) {
+        // Let the user know that the request has been submitted and will be processed soon.
+        $redirectmessage = get_string('approvedrequestsubmitted', 'tool_dataprivacy');
     } else {
+        // Let the user know that the request has been submitted to the privacy officer.
         $redirectmessage = get_string('requestsubmitted', 'tool_dataprivacy');
     }
     redirect($returnurl, $redirectmessage);
index fc5e96b..a7b564c 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20181107" COMMENT="XMLDB file for Moodle tool/dataprivacy"
+<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20191217" COMMENT="XMLDB file for Moodle tool/dataprivacy"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
 >
@@ -16,6 +16,7 @@
         <FIELD NAME="dpo" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the Data Protection Officer who is reviewing th request"/>
         <FIELD NAME="dpocomment" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="DPO's comments (e.g. reason for rejecting the request, etc.)"/>
         <FIELD NAME="dpocommentformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="systemapproved" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user who created/modified this request object"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time this data request was created"/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The last time this data request was updated"/>
index 7b2a9d8..e90e82a 100644 (file)
@@ -322,5 +322,20 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
     // Automatically generated Moodle v3.8.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2019121700) {
+
+        // Define field systemapproved to be added to tool_dataprivacy_request.
+        $table = new xmldb_table('tool_dataprivacy_request');
+        $field = new xmldb_field('systemapproved', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'dpocommentformat');
+
+        // Conditionally launch add field systemapproved.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Dataprivacy savepoint reached.
+        upgrade_plugin_savepoint(true, 2019121700, 'tool', 'dataprivacy');
+    }
+
     return true;
 }
index 5a0efd4..9a31758 100644 (file)
@@ -31,7 +31,12 @@ $string['addcategory'] = 'Add category';
 $string['addnewdefaults'] = 'Add a new module default';
 $string['addpurpose'] = 'Add purpose';
 $string['approve'] = 'Approve';
+$string['approvedrequestsubmitted'] = 'Your request has been submitted and will be processed soon';
 $string['approverequest'] = 'Approve request';
+$string['automaticdatadeletionapproval'] = 'Automatic data deletion request approval';
+$string['automaticdatadeletionapproval_desc'] = 'If enabled, data deletion requests are automatically approved.<br/>Note that the automatic approval will only apply to new data deletion requests with this setting enabled. Existing data deletion requests pending approval will still have to be manually approved by the privacy officer.';
+$string['automaticdataexportapproval'] = 'Automatic data export request approval';
+$string['automaticdataexportapproval_desc'] = 'If enabled, data export requests are automatically approved.<br/>Note that the automatic approval will only apply to new data export requests with this setting enabled. Existing data export requests pending approval will still have to be manually approved by the privacy officer.';
 $string['automaticdeletionrequests'] = 'Create automatic data deletion requests';
 $string['automaticdeletionrequests_desc'] = 'If enabled, a data deletion request will be created automatically for any user accounts deleted manually.';
 $string['bulkapproverequests'] = 'Approve requests';
index de3d715..0a335e3 100644 (file)
@@ -34,6 +34,16 @@ if ($hassiteconfig) {
                 new lang_string('contactdataprotectionofficer_desc', 'tool_dataprivacy'), 0)
         );
 
+        $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/automaticdataexportapproval',
+                new lang_string('automaticdataexportapproval', 'tool_dataprivacy'),
+                new lang_string('automaticdataexportapproval_desc', 'tool_dataprivacy'), 0)
+        );
+
+        $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/automaticdatadeletionapproval',
+                new lang_string('automaticdatadeletionapproval', 'tool_dataprivacy'),
+                new lang_string('automaticdatadeletionapproval_desc', 'tool_dataprivacy'), 0)
+        );
+
         // Automatically create delete data request for users upon user deletion.
         // Automatically create delete data request for pre-existing deleted users.
         // Enabled by default.
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..7bf94cc 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);
     }
 
     /**
@@ -550,32 +555,65 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
     }
 
     /**
-     * Test for api::create_data_request()
+     * Data provider for data request creation tests.
+     *
+     * @return array
      */
-    public function test_create_data_request() {
-        $this->resetAfterTest();
-
-        $generator = new testing_data_generator();
-        $user = $generator->create_user();
-        $comment = 'sample comment';
-
-        // Login as user.
-        $this->setUser($user->id);
-
-        // Test data request creation.
-        $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
-        $this->assertEquals($user->id, $datarequest->get('userid'));
-        $this->assertEquals($user->id, $datarequest->get('requestedby'));
-        $this->assertEquals(0, $datarequest->get('dpo'));
-        $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type'));
-        $this->assertEquals(api::DATAREQUEST_STATUS_AWAITING_APPROVAL, $datarequest->get('status'));
-        $this->assertEquals($comment, $datarequest->get('comments'));
+    public function data_request_creation_provider() {
+        return [
+            'Export request by user, automatic approval off' => [
+                false, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', false, 0,
+                api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0
+            ],
+            'Export request by user, automatic approval on' => [
+                false, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', true, 0,
+                api::DATAREQUEST_STATUS_APPROVED, 1
+            ],
+            'Export request by PO, automatic approval off' => [
+                true, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', false, 0,
+                api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0
+            ],
+            'Export request by PO, automatic approval on' => [
+                true, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', true, 'dpo',
+                api::DATAREQUEST_STATUS_APPROVED, 1
+            ],
+            'Delete request by user, automatic approval off' => [
+                false, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', false, 0,
+                api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0
+            ],
+            'Delete request by user, automatic approval on' => [
+                false, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', true, 0,
+                api::DATAREQUEST_STATUS_APPROVED, 1
+            ],
+            'Delete request by PO, automatic approval off' => [
+                true, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', false, 0,
+                api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0
+            ],
+            'Delete request by PO, automatic approval on' => [
+                true, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', true, 'dpo',
+                api::DATAREQUEST_STATUS_APPROVED, 1
+            ],
+        ];
     }
 
     /**
-     * Test for api::create_data_request() made by DPO.
+     * Test for api::create_data_request()
+     *
+     * @dataProvider data_request_creation_provider
+     * @param bool $asprivacyofficer Whether the request is made as the Privacy Officer or the user itself.
+     * @param string $type The data request type.
+     * @param string $setting The automatic approval setting.
+     * @param bool $automaticapproval Whether automatic data request approval is turned on or not.
+     * @param int|string $expecteddpoval The expected value for the 'dpo' field. 'dpo' means we'd the expected value would be the
+     *                                   user ID of the privacy officer which happens in the case where a PO requests on behalf of
+     *                                   someone else and automatic data request approval is turned on.
+     * @param int $expectedstatus The expected status of the data request.
+     * @param int $expectedtaskcount The number of expected queued data requests tasks.
+     * @throws coding_exception
+     * @throws invalid_persistent_exception
      */
-    public function test_create_data_request_by_dpo() {
+    public function test_create_data_request($asprivacyofficer, $type, $setting, $automaticapproval, $expecteddpoval,
+                                             $expectedstatus, $expectedtaskcount) {
         global $USER;
 
         $this->resetAfterTest();
@@ -584,16 +622,34 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $user = $generator->create_user();
         $comment = 'sample comment';
 
-        // Login as DPO (Admin is DPO by default).
-        $this->setAdminUser();
+        // Login.
+        if ($asprivacyofficer) {
+            $this->setAdminUser();
+        } else {
+            $this->setUser($user->id);
+        }
+
+        // Set the automatic data request approval setting value.
+        set_config($setting, $automaticapproval, 'tool_dataprivacy');
+
+        // If set to 'dpo' use the currently logged-in user's ID (which should be the admin user's ID).
+        if ($expecteddpoval === 'dpo') {
+            $expecteddpoval = $USER->id;
+        }
 
         // Test data request creation.
-        $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
+        $datarequest = api::create_data_request($user->id, $type, $comment);
         $this->assertEquals($user->id, $datarequest->get('userid'));
         $this->assertEquals($USER->id, $datarequest->get('requestedby'));
-        $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type'));
-        $this->assertEquals(api::DATAREQUEST_STATUS_AWAITING_APPROVAL, $datarequest->get('status'));
+        $this->assertEquals($expecteddpoval, $datarequest->get('dpo'));
+        $this->assertEquals($type, $datarequest->get('type'));
+        $this->assertEquals($expectedstatus, $datarequest->get('status'));
         $this->assertEquals($comment, $datarequest->get('comments'));
+        $this->assertEquals($automaticapproval, $datarequest->get('systemapproved'));
+
+        // Test number of queued data request tasks.
+        $datarequesttasks = manager::get_adhoc_tasks(process_data_request_task::class);
+        $this->assertCount($expectedtaskcount, $datarequesttasks);
     }
 
     /**
@@ -2250,4 +2306,88 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         set_config('siteadmins', $child->id);
         $this->assertFalse(api::can_create_data_deletion_request_for_children($child->id));
     }
+
+    /**
+     * Data provider function for testing \tool_dataprivacy\api::queue_data_request_task().
+     *
+     * @return array
+     */
+    public function queue_data_request_task_provider() {
+        return [
+            'With user ID provided' => [true],
+            'Without user ID provided' => [false],
+        ];
+    }
+
+    /**
+     * Test for \tool_dataprivacy\api::queue_data_request_task().
+     *
+     * @dataProvider queue_data_request_task_provider
+     * @param bool $withuserid
+     */
+    public function test_queue_data_request_task(bool $withuserid) {
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+
+        if ($withuserid) {
+            $user = $this->getDataGenerator()->create_user();
+            api::queue_data_request_task(1, $user->id);
+            $expecteduserid = $user->id;
+        } else {
+            api::queue_data_request_task(1);
+            $expecteduserid = null;
+        }
+
+        // Test number of queued data request tasks.
+        $datarequesttasks = manager::get_adhoc_tasks(process_data_request_task::class);
+        $this->assertCount(1, $datarequesttasks);
+        $requesttask = reset($datarequesttasks);
+        $this->assertEquals($expecteduserid, $requesttask->get_userid());
+    }
+
+    /**
+     * Data provider for test_is_automatic_request_approval_on().
+     */
+    public function automatic_request_approval_setting_provider() {
+        return [
+            'Data export, not set' => [
+                'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, null, false
+            ],
+            'Data export, turned on' => [
+                'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, true, true
+            ],
+            'Data export, turned off' => [
+                'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, false, false
+            ],
+            'Data deletion, not set' => [
+                'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, null, false
+            ],
+            'Data deletion, turned on' => [
+                'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, true, true
+            ],
+            'Data deletion, turned off' => [
+                'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, false, false
+            ],
+        ];
+    }
+
+    /**
+     * Test for \tool_dataprivacy\api::is_automatic_request_approval_on().
+     *
+     * @dataProvider automatic_request_approval_setting_provider
+     * @param string $setting The automatic approval setting.
+     * @param int $type The data request type.
+     * @param bool $value The setting's value.
+     * @param bool $expected The expected result.
+     */
+    public function test_is_automatic_request_approval_on($setting, $type, $value, $expected) {
+        $this->resetAfterTest();
+
+        if ($value !== null) {
+            set_config($setting, $value, 'tool_dataprivacy');
+        }
+
+        $this->assertEquals($expected, api::is_automatic_request_approval_on($type));
+    }
 }
index 0a4da1b..4add17f 100644 (file)
@@ -240,3 +240,13 @@ Feature: Data delete from the privacy API
     And I reload the page
     And I open the action menu in "region-main" "region"
     Then I should not see "Resubmit as new request"
+
+  Scenario: Request data deletion as student with automatic approval turned on
+    Given the following config values are set as admin:
+      | automaticdatadeletionapproval | 1  | tool_dataprivacy |
+    And I log in as "victim"
+    And I follow "Profile" in the user menu
+    And I follow "Delete my account"
+    When I press "Save changes"
+    Then I should see "Your request has been submitted and will be processed soon"
+    And I should see "Approved" in the "Delete all of my personal data" "table_row"
index b651201..65fec34 100644 (file)
@@ -151,3 +151,13 @@ Feature: Data export from the privacy API
     When I reload the page
     And I set the field "Search" to "University2"
     Then I should see "Victim User 2"
+
+  Scenario: Request data export as student with automatic approval turned on
+    Given the following config values are set as admin:
+      | automaticdataexportapproval | 1  | tool_dataprivacy |
+    And I log in as "victim"
+    And I follow "Profile" in the user menu
+    And I follow "Export all of my personal data"
+    When I press "Save changes"
+    Then I should see "Your request has been submitted and will be processed soon"
+    And I should see "Approved" in the "Export all of my personal data" "table_row"
index 636f744..5242cdf 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2019111800;
+$plugin->version   = 2019121700;
 $plugin->requires  = 2019111200;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
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 22045d5..3774465 100644 (file)
Binary files a/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js and b/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js differ
index c676cc1..0fac0c4 100644 (file)
Binary files a/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js.map and b/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js.map differ