Merge branch 'MDL-68541-39' of https://github.com/PoetOS/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 5 May 2020 23:04:14 +0000 (01:04 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 5 May 2020 23:04:14 +0000 (01:04 +0200)
373 files changed:
.eslintignore
.stylelintignore
.travis.yml
admin/settings/development.php
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/task/lang/en/tool_task.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/classes/helper.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/uploadcourse/index.php
admin/tool/uploadcourse/lang/en/tool_uploadcourse.php
admin/tool/uploadcourse/tests/behat/create.feature
admin/tool/uploadcourse/tests/behat/update.feature
admin/tool/uploadcourse/tests/course_test.php
admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv [new file with mode: 0644]
admin/tool/uploadcourse/tests/helper_test.php
admin/tool/xmldb/lang/en/tool_xmldb.php
admin/user/user_bulk_download.php
blocks/site_main_menu/tests/behat/add_url.feature
cache/classes/helper.php
cache/classes/loaders.php
cache/classes/store.php
cache/tests/cache_test.php
cache/upgrade.txt
calendar/amd/build/crud.min.js
calendar/amd/build/crud.min.js.map
calendar/amd/build/modal_delete.min.js
calendar/amd/build/modal_delete.min.js.map
calendar/amd/src/crud.js
calendar/amd/src/modal_delete.js
composer.json
composer.lock
config-dist.php
contentbank/amd/build/actions.min.js
contentbank/amd/build/actions.min.js.map
contentbank/amd/src/actions.js
contentbank/classes/content.php
contentbank/classes/contenttype.php
contentbank/classes/external/rename_content.php [new file with mode: 0644]
contentbank/classes/privacy/provider.php
contentbank/contenttype/h5p/lang/en/contenttype_h5p.php
contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature
contentbank/contenttype/h5p/tests/behat/manage_content.feature [new file with mode: 0644]
contentbank/contenttype/h5p/tests/content_h5p_test.php
contentbank/templates/renamecontent.mustache [new file with mode: 0644]
contentbank/tests/behat/delete_content.feature
contentbank/tests/content_test.php
contentbank/tests/contenttype_test.php
contentbank/tests/external/rename_content_test.php [new file with mode: 0644]
contentbank/tests/privacy_test.php [new file with mode: 0644]
contentbank/view.php
course/classes/management_renderer.php
course/renderer.php
course/tests/behat/activity_chooser.feature
customfield/classes/data_controller.php
customfield/classes/field_controller.php
customfield/classes/handler.php
customfield/field/date/classes/field_controller.php
customfield/field/date/tests/plugin_test.php
customfield/field/select/classes/field_controller.php
customfield/field/select/tests/plugin_test.php
customfield/field/textarea/classes/data_controller.php
dataformat/pdf/classes/writer.php
dataformat/upgrade.txt
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/styles.css
h5p/h5plib/v124/lang/en/h5plib_v124.php
h5p/tests/editor_test.php
install/lang/fr/error.php
install/lang/scn/langconfig.php [new file with mode: 0644]
lang/en/admin.php
lang/en/cache.php
lang/en/contentbank.php
lang/en/course.php
lang/en/enrol.php
lang/en/error.php
lang/en/h5p.php
lang/en/moodle.php
lang/en/repository.php
lang/en/role.php
lib/amd/build/custom_interaction_events.min.js
lib/amd/build/custom_interaction_events.min.js.map
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-autocomplete.min.js.map
lib/amd/build/local/modal/alert.min.js [new file with mode: 0644]
lib/amd/build/local/modal/alert.min.js.map [new file with mode: 0644]
lib/amd/build/modal.min.js
lib/amd/build/modal.min.js.map
lib/amd/build/modal_cancel.min.js
lib/amd/build/modal_cancel.min.js.map
lib/amd/build/modal_factory.min.js
lib/amd/build/modal_factory.min.js.map
lib/amd/build/modal_registry.min.js
lib/amd/build/modal_registry.min.js.map
lib/amd/build/modal_save_cancel.min.js
lib/amd/build/modal_save_cancel.min.js.map
lib/amd/build/notification.min.js
lib/amd/build/notification.min.js.map
lib/amd/build/prefetch.min.js [new file with mode: 0644]
lib/amd/build/prefetch.min.js.map [new file with mode: 0644]
lib/amd/build/templates.min.js
lib/amd/build/templates.min.js.map
lib/amd/src/custom_interaction_events.js
lib/amd/src/form-autocomplete.js
lib/amd/src/local/modal/alert.js [new file with mode: 0644]
lib/amd/src/modal.js
lib/amd/src/modal_cancel.js
lib/amd/src/modal_factory.js
lib/amd/src/modal_registry.js
lib/amd/src/modal_save_cancel.js
lib/amd/src/notification.js
lib/amd/src/prefetch.js [new file with mode: 0644]
lib/amd/src/templates.js
lib/antivirus/clamav/lang/en/antivirus_clamav.php
lib/behat/classes/behat_core_generator.php
lib/behat/lib.php
lib/classes/component.php
lib/classes/dataformat.php [new file with mode: 0644]
lib/classes/dataformat/base.php
lib/classes/dataformat/spout_base.php
lib/classes/ip_utils.php
lib/classes/plugin_manager.php
lib/classes/session/redis.php
lib/classes/task/completion_daily_task.php
lib/classes/task/manager.php
lib/classes/task/messaging_cleanup_task.php
lib/dataformatlib.php
lib/db/access.php
lib/db/services.php
lib/db/upgrade.php
lib/editor/atto/plugins/bold/tests/behat/bold.feature
lib/editor/atto/plugins/bold/yui/build/moodle-atto_bold-button/moodle-atto_bold-button-debug.js
lib/editor/atto/plugins/bold/yui/build/moodle-atto_bold-button/moodle-atto_bold-button-min.js
lib/editor/atto/plugins/bold/yui/build/moodle-atto_bold-button/moodle-atto_bold-button.js
lib/editor/atto/plugins/bold/yui/src/button/js/button.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
lib/editor/atto/plugins/image/yui/src/button/js/button.js
lib/editor/atto/plugins/strike/tests/behat/strike.feature
lib/editor/atto/plugins/strike/yui/build/moodle-atto_strike-button/moodle-atto_strike-button-debug.js
lib/editor/atto/plugins/strike/yui/build/moodle-atto_strike-button/moodle-atto_strike-button-min.js
lib/editor/atto/plugins/strike/yui/build/moodle-atto_strike-button/moodle-atto_strike-button.js
lib/editor/atto/plugins/strike/yui/src/button/js/button.js
lib/editor/atto/tests/behat/customtoolbar.feature
lib/editor/atto/tests/behat/disablecontrol.feature
lib/form/checkbox.php
lib/form/course.php
lib/form/dateselector.php
lib/form/datetimeselector.php
lib/form/duration.php
lib/form/tests/behat/autocomplete.feature
lib/form/tests/behat/behat_core_form.php [new file with mode: 0644]
lib/form/tests/behat/fixtures/repeat_defaults_form.php [new file with mode: 0644]
lib/form/tests/behat/repeat_defaults.feature [new file with mode: 0644]
lib/form/tests/course_test.php [new file with mode: 0644]
lib/form/tests/fixtures/autocomplete-disabledif.php [new file with mode: 0644]
lib/formslib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrequirementslib.php
lib/plist/LICENSE [new file with mode: 0644]
lib/plist/README.md [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFBinaryPropertyList.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFPropertyList.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFType.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFTypeDetector.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/IOException.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/PListException.php [new file with mode: 0644]
lib/plist/readme_moodle.txt [new file with mode: 0644]
lib/questionlib.php
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/events.min.js [new file with mode: 0644]
lib/table/amd/build/local/dynamic/events.min.js.map [new file with mode: 0644]
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/build/local/dynamic/selectors.min.js
lib/table/amd/build/local/dynamic/selectors.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/events.js [moved from mod/quiz/accessrule/safebrowser/version.php with 70% similarity]
lib/table/amd/src/local/dynamic/repository.js
lib/table/amd/src/local/dynamic/selectors.js
lib/table/classes/dynamic.php
lib/table/classes/external/dynamic/fetch.php
lib/table/tests/external/dynamic/fetch_test.php
lib/tablelib.php
lib/templates/drawer.mustache
lib/templates/local/modal/alert.mustache [new file with mode: 0644]
lib/templates/single_select.mustache
lib/templates/url_select.mustache
lib/tests/behat/action_modal.feature
lib/tests/behat/alpha_chooser.feature
lib/tests/behat/behat_navigation.php
lib/tests/behat/largeforms.feature
lib/tests/behat/locking.feature
lib/tests/behat/permissionmanager.feature
lib/tests/behat/readonlyform.feature
lib/tests/behat/securelayout.feature
lib/tests/completion_daily_task_test.php [new file with mode: 0644]
lib/tests/dataformat_test.php [new file with mode: 0644]
lib/tests/questionlib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/xapi/classes/local/statement.php
lib/xapi/classes/local/statement/item_attachment.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_context.php [moved from mod/quiz/accessrule/safebrowser/classes/privacy/provider.php with 56% similarity]
lib/xapi/classes/local/statement/item_result.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_score.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_attachment_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_context_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_result_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_score_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement_test.php
media/player/videojs/classes/plugin.php
message/amd/build/message_drawer.min.js
message/amd/build/message_drawer.min.js.map
message/amd/build/message_drawer_helper.min.js
message/amd/build/message_drawer_helper.min.js.map
message/amd/build/message_popover.min.js
message/amd/build/message_popover.min.js.map
message/amd/build/message_preferences.min.js [new file with mode: 0644]
message/amd/build/message_preferences.min.js.map [new file with mode: 0644]
message/amd/build/message_user_button.min.js
message/amd/build/message_user_button.min.js.map
message/amd/src/message_drawer.js
message/amd/src/message_drawer_helper.js
message/amd/src/message_popover.js
message/amd/src/message_preferences.js [new file with mode: 0644]
message/amd/src/message_user_button.js
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/email/classes/task/send_email_task.php
message/output/email/lang/en/deprecated.txt [new file with mode: 0644]
message/output/email/lang/en/message_email.php
message/output/email/tests/send_email_task_test.php
message/output/lib.php
message/output/popup/db/upgrade.php
message/output/popup/message_output_popup.php
message/output/popup/tests/messaging_cleanup_test.php [new file with mode: 0644]
message/output/popup/version.php
message/templates/message_jumpto.mustache [new file with mode: 0644]
message/templates/message_popover.mustache
message/templates/message_preferences.mustache [new file with mode: 0644]
message/templates/message_preferences_component.mustache [new file with mode: 0644]
message/templates/message_preferences_notification_processor.mustache [new file with mode: 0644]
message/tests/behat/message_preferences.feature [new file with mode: 0644]
message/upgrade.txt
mod/assign/feedback/editpdf/tests/behat/view_previous_annotations.feature
mod/assign/lang/en/assign.php
mod/assign/tests/behat/edit_previous_feedback.feature
mod/feedback/classes/responses_table.php
mod/forum/export.php
mod/forum/report/summary/templates/bulk_action_menu.mustache
mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php
mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php
mod/h5pactivity/lang/en/h5pactivity.php
mod/h5pactivity/lib.php
mod/h5pactivity/tests/behat/add_h5pactivity.feature
mod/h5pactivity/tests/behat/sending_attempt.feature
mod/h5pactivity/tests/local/attempt_test.php
mod/h5pactivity/tests/privacy_test.php
mod/h5pactivity/tests/restore_test.php [new file with mode: 0644]
mod/h5pactivity/tests/xapi/handler_test.php
mod/lesson/lang/en/lesson.php
mod/lti/lang/en/lti.php
mod/lti/mod_form.php
mod/quiz/accessrule/safebrowser/rule.php [deleted file]
mod/quiz/accessrule/safebrowser/tests/rule_test.php [deleted file]
mod/quiz/accessrule/seb/amd/build/managetemplates.min.js [new file with mode: 0644]
mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map [new file with mode: 0644]
mod/quiz/accessrule/seb/amd/src/managetemplates.js [new file with mode: 0644]
mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php [new file with mode: 0644]
mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/access_manager.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/config_key.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/access_prevented.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_created.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_deleted.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_disabled.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_enabled.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_updated.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/helper.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/hideif_rule.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/link_generator.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/local/form/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/local/table/template_list.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/privacy/provider.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/property_list.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/quiz_settings.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/settings_provider.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/template_controller.php [new file with mode: 0644]
mod/quiz/accessrule/seb/config.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/access.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/caches.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/install.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/install.xml [new file with mode: 0644]
mod/quiz/accessrule/seb/db/upgrade.php [moved from mod/quiz/accessrule/safebrowser/lang/en/quizaccess_safebrowser.php with 53% similarity]
mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php [new file with mode: 0644]
mod/quiz/accessrule/seb/lib.php [new file with mode: 0644]
mod/quiz/accessrule/seb/rule.php [new file with mode: 0644]
mod/quiz/accessrule/seb/settings.php [new file with mode: 0644]
mod/quiz/accessrule/seb/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/behat/edit_form.feature [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/generator/behat_quizaccess_seb_generator.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/generator/lib.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/access_manager_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/backup_restore_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/base.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/config_key_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/event_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/helper_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/hideif_rule_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/link_generator_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/privacy_provider_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/property_list_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/quiz_settings_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/rule_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/JSON_unencrypted_mac_001.txt [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/encrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencryptedwithoutoriginator.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted_mac_001.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted_win_223.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/settings_provider_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/template_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/version.php [new file with mode: 0644]
mod/quiz/accessrule/timelimit/lang/en/quizaccess_timelimit.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/lang/en/quiz.php
mod/quiz/locallib.php
mod/quiz/tests/behat/attempt_begin.feature
mod/workshop/tests/behat/file_type_restriction.feature
privacy/classes/tests/request/content_writer.php
question/format/gift/format.php
question/format/gift/tests/giftformat_test.php
question/type/ddimageortext/lang/en/qtype_ddimageortext.php
question/type/ddmarker/amd/build/form.min.js
question/type/ddmarker/amd/build/form.min.js.map
question/type/ddmarker/amd/build/question.min.js
question/type/ddmarker/amd/build/question.min.js.map
question/type/ddmarker/amd/build/shapes.min.js
question/type/ddmarker/amd/build/shapes.min.js.map
question/type/ddmarker/amd/src/form.js
question/type/ddmarker/amd/src/question.js
question/type/ddmarker/amd/src/shapes.js
question/type/ddmarker/lang/en/qtype_ddmarker.php
question/type/ddmarker/renderer.php
question/type/ddmarker/styles.css
question/type/ddmarker/tests/behat/behat_qtype_ddmarker.php
question/type/ddmarker/tests/behat/preview.feature
question/type/ddmarker/tests/walkthrough_test.php
tag/tests/behat/delete_tag.feature
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/grade.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/action_redir.php
user/amd/build/participants.min.js
user/amd/build/participants.min.js.map
user/amd/build/repository.min.js [new file with mode: 0644]
user/amd/build/repository.min.js.map [new file with mode: 0644]
user/amd/build/status_field.min.js
user/amd/build/status_field.min.js.map
user/amd/src/participants.js
user/amd/src/repository.js [new file with mode: 0644]
user/amd/src/status_field.js
user/classes/table/participants.php
user/index.php
user/tests/behat/course_preference.feature
version.php

index 2ef8a38..b9c0b6a 100644 (file)
@@ -65,6 +65,7 @@ lib/php-jwt/
 lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
+lib/plist/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index f00e696..5d9e5c1 100644 (file)
@@ -66,6 +66,7 @@ lib/php-jwt/
 lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
+lib/plist/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index 245d7fc..f60032e 100644 (file)
@@ -18,7 +18,7 @@ services:
 
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
-    - 7.3
+    - 7.4
     - 7.2
 
 addons:
@@ -48,11 +48,11 @@ jobs:
     fast_finish: true
 
     include:
-          # Run mysql only on 7.3 - it's just too slow
-        - php: 7.3
+          # Run mysql only on highest - it's just too slow
+        - php: 7.4
           env: DB=mysqli   TASK=PHPUNIT
-          # Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
-        - php: 7.2
+          # Run grunt/npm install on highest version too ('node' is an alias for the latest node.js version.)
+        - php: 7.4
           env: DB=none     TASK=GRUNT   NVM_VERSION='lts/carbon'
 
 cache:
index f8a5ceb..46206fc 100644 (file)
@@ -10,7 +10,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp = new admin_settingpage('experimentalsettings', new lang_string('experimentalsettings', 'admin'));
     //TODO: Re-enable cc-import once re-implemented in 2.0.x
     //$temp->add(new admin_setting_configcheckbox('enableimsccimport', new lang_string('enable_cc_import', 'imscc'), new lang_string('enable_cc_import_description', 'imscc'), 0));
-    $temp->add(new admin_setting_configcheckbox('enablesafebrowserintegration', new lang_string('enablesafebrowserintegration', 'admin'), new lang_string('configenablesafebrowserintegration', 'admin'), 0));
 
     $temp->add(new admin_setting_configcheckbox('dndallowtextandlinks', new lang_string('dndallowtextandlinks', 'admin'), new lang_string('configdndallowtextandlinks', 'admin'), 0));
 
index 8daab15..fd0b465 100644 (file)
@@ -24,7 +24,7 @@
 
 $string['classname'] = 'Class name';
 $string['component'] = 'Component';
-$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:587. If a port isn\'t specified, the default port for the type of mail server will be used.';
+$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:993. If a port isn\'t specified, the default port for the type of mail server will be used.';
 $string['defaultexpiration'] = 'Default address expiry period';
 $string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
 $string['description'] = 'Description';
index 9ccd292..299603f 100644 (file)
  */
 
 $string['asap'] = 'ASAP';
-$string['adhocempty'] = 'Adhoc task queue is empty';
-$string['adhocqueuesize'] = 'Adhoc task queue has {$a} tasks';
+$string['adhocempty'] = 'Ad hoc task queue is empty';
+$string['adhocqueuesize'] = 'Ad hoc task queue has {$a} tasks';
 $string['adhocqueueold'] = 'Oldest task is {$a->age} which is more than {$a->max}';
 $string['backtoscheduledtasks'] = 'Back to scheduled tasks';
 $string['blocking'] = 'Blocking';
 $string['cannotfindthepathtothecli'] = 'Cannot find the path to the PHP CLI executable so task execution aborted. Set the \'Path to PHP CLI\' setting in Site administration / Server / System paths.';
-$string['checkadhocqueue'] = 'Adhoc task queue';
+$string['checkadhocqueue'] = 'Ad hoc task queue';
 $string['checkcronrunning'] = 'Cron running';
 $string['checkmaxfaildelay'] = 'Tasks max fail delay';
 $string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail delay for task \'{$a}\'? After clearing the delay, the task will run according to its normal schedule.';
@@ -58,7 +58,7 @@ $string['runpattern'] = 'Run pattern';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
-$string['taskfailures'] = 'There are {$a} task(s) failing';
+$string['taskfailures'] = '{$a} task(s) failing';
 $string['tasklogs'] = 'Task logs';
 $string['tasknofailures'] = 'There are no tasks failing';
 $string['taskscheduleday'] = 'Day';
index 8ae7a8d..88cd888 100644 (file)
@@ -284,6 +284,15 @@ class tool_uploadcourse_course {
         return $this->errors;
     }
 
+    /**
+     * Return array of valid fields for default values
+     *
+     * @return array
+     */
+    protected function get_valid_fields() {
+        return array_merge(self::$validfields, \tool_uploadcourse_helper::get_custom_course_field_names());
+    }
+
     /**
      * Assemble the course data based on defaults.
      *
@@ -293,7 +302,7 @@ class tool_uploadcourse_course {
      * @return array
      */
     protected function get_final_create_data($data) {
-        foreach (self::$validfields as $field) {
+        foreach ($this->get_valid_fields() as $field) {
             if (!isset($data[$field]) && isset($this->defaults[$field])) {
                 $data[$field] = $this->defaults[$field];
             }
@@ -316,9 +325,9 @@ class tool_uploadcourse_course {
         global $DB;
         $newdata = array();
         $existingdata = $DB->get_record('course', array('shortname' => $this->shortname));
-        foreach (self::$validfields as $field) {
+        foreach ($this->get_valid_fields() as $field) {
             if ($missingonly) {
-                if (!is_null($existingdata->$field) and $existingdata->$field !== '') {
+                if (isset($existingdata->$field) and $existingdata->$field !== '') {
                     continue;
                 }
             }
@@ -699,6 +708,27 @@ class tool_uploadcourse_course {
             $coursedata[$rolekey] = $rolename;
         }
 
+        // Custom fields. If the course already exists and mode isn't set to force creation, we can use its context.
+        if ($exists && $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL) {
+            $context = context_course::instance($coursedata['id']);
+        } else {
+            // The category ID is taken from the defaults if it exists, otherwise from course data.
+            $context = context_coursecat::instance($this->defaults['category'] ?? $coursedata['category']);
+        }
+        $customfielddata = tool_uploadcourse_helper::get_custom_course_field_data($this->rawdata, $this->defaults, $context,
+            $errors);
+        if (!empty($errors)) {
+            foreach ($errors as $key => $message) {
+                $this->error($key, $message);
+            }
+
+            return false;
+        }
+
+        foreach ($customfielddata as $name => $value) {
+            $coursedata[$name] = $value;
+        }
+
         // Some validation.
         if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) {
             $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse'));
index 2325c9c..1011c37 100644 (file)
@@ -337,6 +337,103 @@ class tool_uploadcourse_helper {
         return $rolenames;
     }
 
+    /**
+     * Return array of all custom course fields indexed by their shortname
+     *
+     * @return \core_customfield\field_controller[]
+     */
+    public static function get_custom_course_fields(): array {
+        $result = [];
+
+        $fields = \core_course\customfield\course_handler::create()->get_fields();
+        foreach ($fields as $field) {
+            $result[$field->get('shortname')] = $field;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Return array of custom field element names
+     *
+     * @return string[]
+     */
+    public static function get_custom_course_field_names(): array {
+        $result = [];
+
+        $fields = self::get_custom_course_fields();
+        foreach ($fields as $field) {
+            $controller = \core_customfield\data_controller::create(0, null, $field);
+            $result[] = $controller->get_form_element_name();
+        }
+
+        return $result;
+    }
+
+    /**
+     * Return any elements from passed $data whose key matches one of the custom course fields defined for the site
+     *
+     * @param array $data
+     * @param array $defaults
+     * @param context $context
+     * @param array $errors Will be populated with any errors
+     * @return array
+     */
+    public static function get_custom_course_field_data(array $data, array $defaults, context $context,
+            array &$errors = []): array {
+
+        $fields = self::get_custom_course_fields();
+        $result = [];
+
+        $canchangelockedfields = guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context);
+
+        foreach ($data as $name => $originalvalue) {
+            if (preg_match('/^customfield_(?<name>.*)?$/', $name, $matches)
+                    && isset($fields[$matches['name']])) {
+
+                $fieldname = $matches['name'];
+                $field = $fields[$fieldname];
+
+                // Skip field if it's locked and user doesn't have capability to change locked fields.
+                if ($field->get_configdata_property('locked') && !$canchangelockedfields) {
+                    continue;
+                }
+
+                // Create field data controller.
+                $controller = \core_customfield\data_controller::create(0, null, $field);
+                $controller->set('id', 1);
+
+                $defaultvalue = $defaults["customfield_{$fieldname}"] ?? $controller->get_default_value();
+                $value = (empty($originalvalue) ? $defaultvalue : $field->parse_value($originalvalue));
+
+                // If we initially had a value, but now don't, then reset it to the default.
+                if (!empty($originalvalue) && empty($value)) {
+                    $value = $defaultvalue;
+                }
+
+                // Validate data with controller.
+                $fieldformdata = [$controller->get_form_element_name() => $value];
+                $validationerrors = $controller->instance_form_validation($fieldformdata, []);
+                if (count($validationerrors) > 0) {
+                    $errors['customfieldinvalid'] = new lang_string('customfieldinvalid', 'tool_uploadcourse',
+                        $field->get_formatted_name());
+
+                    continue;
+                }
+
+                $controller->set($controller->datafield(), $value);
+
+                // Pass an empty object to the data controller, which will transform it to a correct name/value pair.
+                $instance = new stdClass();
+                $controller->instance_form_before_set_data($instance);
+
+                $result = array_merge($result, (array) $instance);
+            }
+        }
+
+        return $result;
+    }
+
     /**
      * Helper to increment an ID number.
      *
@@ -493,5 +590,4 @@ class tool_uploadcourse_helper {
         }
         return $id;
     }
-
-}
+}
\ No newline at end of file
index 58c39d4..8854abb 100644 (file)
@@ -173,6 +173,10 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             $mform->addHelpButton('defaults[enablecompletion]', 'enablecompletion', 'completion');
         }
 
+        // Add custom fields to the form.
+        $handler = \core_course\customfield\course_handler::create();
+        $handler->instance_form_definition($mform, 0, 'defaultvaluescustomfieldcategory', 'tool_uploadcourse');
+
         // Hidden fields.
         $mform->addElement('hidden', 'importid');
         $mform->setType('importid', PARAM_INT);
@@ -182,6 +186,10 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
 
         $this->add_action_buttons(true, get_string('uploadcourses', 'tool_uploadcourse'));
 
+        // Prepare custom fields data.
+        $data = (object) $data;
+        $handler->instance_form_before_set_data($data);
+
         $this->set_data($data);
     }
 
@@ -219,6 +227,9 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             $enddate = $format->get_default_course_enddate($mform, array('startdate' => 'defaults[startdate]'));
             $mform->setDefault('defaults[enddate]', $enddate);
         }
+
+        // Tweak the form with values provided by custom fields in use.
+        \core_course\customfield\course_handler::create()->instance_form_definition_after_data($mform);
     }
 
     /**
@@ -237,6 +248,9 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             $errors['defaults[enddate]'] = get_string($errorcode, 'error');
         }
 
+        // Custom fields validation.
+        array_merge($errors, \core_course\customfield\course_handler::create()->instance_form_validation($data, $files));
+
         return $errors;
     }
 }
index 69f4410..8c2ccfd 100644 (file)
@@ -78,6 +78,12 @@ if ($form2data = $mform2->is_cancelled()) {
     $options = (array) $form2data->options;
     $defaults = (array) $form2data->defaults;
 
+    // Custom field defaults.
+    $customfields = tool_uploadcourse_helper::get_custom_course_field_names();
+    foreach ($customfields as $customfield) {
+        $defaults[$customfield] = $form2data->{$customfield};
+    }
+
     // Restorefile deserves its own logic because formslib does not really appreciate
     // when the name of a filepicker is an array...
     $options['restorefile'] = '';
index 9a0e2c6..b44b4f3 100644 (file)
@@ -75,6 +75,7 @@ $string['csvdelimiter_help'] = 'CSV delimiter of the CSV file.';
 $string['csvfileerror'] = 'There is something wrong with the format of the CSV file. Please check the number of headings and columns match, and that the delimiter and file encoding are correct: {$a}';
 $string['csvline'] = 'Line';
 $string['defaultvalues'] = 'Default course values';
+$string['defaultvaluescustomfieldcategory'] = 'Default values for \'{$a}\'';
 $string['encoding'] = 'Encoding';
 $string['encoding_help'] = 'Encoding of the CSV file.';
 $string['errorwhilerestoringcourse'] = 'Error while restoring the course';
@@ -102,6 +103,7 @@ $string['mode_help'] = 'This allows you to specify if courses can be created and
 $string['nochanges'] = 'No changes';
 $string['pluginname'] = 'Course upload';
 $string['preview'] = 'Preview';
+$string['customfieldinvalid'] = 'Custom field \'{$a}\' is empty or contains invalid data';
 $string['reset'] = 'Reset course after upload';
 $string['reset_help'] = 'Whether to reset the course after creating/updating it.';
 $string['result'] = 'Result';
index 9fbfa01..a17fc2f 100644 (file)
@@ -42,3 +42,66 @@ Feature: An admin can create courses using a CSV file
     And I should see "Course 1"
     And I should see "Course 2"
     And I should see "Course 3"
+
+  @javascript
+  Scenario: Creation of new courses with custom fields
+    Given the following "custom field categories" exist:
+      | name  | component   | area   | itemid |
+      | Other | core_course | course | 0      |
+    And the following "custom fields" exist:
+      | name    | category | type     | shortname | configdata            |
+      | Field 1 | Other    | checkbox | checkbox  |                       |
+      | Field 2 | Other    | date     | date      |                       |
+      | Field 3 | Other    | select   | select    | {"options":"a\nb\nc"} |
+      | Field 4 | Other    | text     | text      |                       |
+      | Field 5 | Other    | textarea | textarea  |                       |
+    When I upload "admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Create new courses only, skip existing ones"
+    And I click on "Preview" "button"
+    And I click on "Upload courses" "button"
+    Then I should see "Course created"
+    And I should see "Courses created: 1"
+    And I am on site homepage
+    And I should see "Course fields 1"
+    And I should see "Field 1: Yes"
+    And I should see "Field 2: Tuesday, 1 October 2019, 2:00"
+    And I should see "Field 3: b"
+    And I should see "Field 4: Hello"
+    And I should see "Field 5: Goodbye"
+
+  @javascript
+  Scenario: Creation of new courses with custom fields using defaults
+    Given the following "custom field categories" exist:
+      | name  | component   | area   | itemid |
+      | Other | core_course | course | 0      |
+    And the following "custom fields" exist:
+      | name    | category | type     | shortname | configdata                                          |
+      | Field 1 | Other    | checkbox | checkbox  | {"checkbydefault":1}                                |
+      | Field 2 | Other    | date     | date      | {"includetime":0}                                   |
+      | Field 3 | Other    | select   | select    | {"options":"a\nb\nc","defaultvalue":"b"}            |
+      | Field 4 | Other    | text     | text      | {"defaultvalue":"Hello"}                            |
+      | Field 5 | Other    | textarea | textarea  | {"defaultvalue":"Some text","defaultvalueformat":1} |
+    When I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Create all, increment shortname if needed"
+    And I click on "Preview" "button"
+    And I expand all fieldsets
+    And the field "Field 1" matches value "1"
+    And the field "Field 3" matches value "b"
+    And the field "Field 4" matches value "Hello"
+    And the field "Field 5" matches value "Some text"
+    # We have to enable the date field manually.
+    And I set the following fields to these values:
+      | customfield_date[enabled] | 1    |
+      | customfield_date[day]     | 1    |
+      | customfield_date[month]   | June |
+      | customfield_date[year]    | 2020 |
+    And I click on "Upload courses" "button"
+    Then I should see "Course created"
+    And I should see "Courses created: 3"
+    And I am on site homepage
+    And I should see "Course 1"
+    And I should see "Field 1: Yes"
+    And I should see "Field 2: 1 June 2020"
+    And I should see "Field 3: b"
+    And I should see "Field 4: Hello"
+    And I should see "Field 5: Some text"
\ No newline at end of file
index dbdbef5..dbca1f4 100644 (file)
@@ -7,7 +7,8 @@ Feature: An admin can update courses using a CSV file
   Background:
     Given the following "courses" exist:
       | fullname | shortname | category |
-      | Some random name | C1 | 0 |
+      | Some random name | C1  | 0 |
+      | Another course   | CF1 | 0 |
     And I log in as "admin"
     And I navigate to "Courses > Upload courses" in site administration
 
@@ -28,3 +29,31 @@ Feature: An admin can update courses using a CSV file
     And I should see "Course 1"
     And I should not see "Course 2"
     And I should not see "Course 3"
+
+  @javascript
+  Scenario: Updating a course with custom fields
+    Given the following "custom field categories" exist:
+      | name  | component   | area   | itemid |
+      | Other | core_course | course | 0      |
+    And the following "custom fields" exist:
+      | name    | category | type     | shortname | configdata            |
+      | Field 1 | Other    | checkbox | checkbox  |                       |
+      | Field 2 | Other    | date     | date      |                       |
+      | Field 3 | Other    | select   | select    | {"options":"a\nb\nc"} |
+      | Field 4 | Other    | text     | text      |                       |
+      | Field 5 | Other    | textarea | textarea  |                       |
+    When I upload "admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv" file to "File" filemanager
+    And I set the following fields to these values:
+      | Upload mode | Only update existing courses |
+      | Update mode | Update with CSV data only    |
+    And I click on "Preview" "button"
+    And I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I should see "Courses updated: 1"
+    And I am on site homepage
+    And I should see "Course fields 1"
+    And I should see "Field 1: Yes"
+    And I should see "Field 2: Tuesday, 1 October 2019, 2:00"
+    And I should see "Field 3: b"
+    And I should see "Field 4: Hello"
+    And I should see "Field 5: Goodbye"
\ No newline at end of file
index 0add18e..7d811cb 100644 (file)
@@ -1081,6 +1081,136 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertEquals(strtotime('12th July 2013'), $enroldata['manual']->enrolenddate);
     }
 
+    /**
+     * Test upload processing of course custom fields
+     */
+    public function test_custom_fields_data() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom fields.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'date', 'mydatefield');
+        $this->create_custom_field($category, 'text', 'mytextfield');
+        $this->create_custom_field($category, 'textarea', 'mytextareafield');
+
+        // Perform upload.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_mydatefield' => '2020-04-01 16:00',
+            'customfield_mytextfield' => 'Hello',
+            'customfield_mytextareafield' => 'Is it me you\'re looking for?',
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertTrue($uploader->prepare());
+        $uploader->proceed();
+
+        // Confirm presence of course custom fields.
+        $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+        $this->assertEquals('Wednesday, 1 April 2020, 4:00 PM', $data->mydatefield, '', 0.0, 10, false, true);
+        $this->assertEquals($dataupload['customfield_mytextfield'], $data->mytextfield);
+        $this->assertContains($dataupload['customfield_mytextareafield'], $data->mytextareafield);
+    }
+
+    /**
+     * Test upload processing of course custom field that is required but empty
+     */
+    public function test_custom_fields_data_required() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom field.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'select', 'myselect', ['required' => true, 'options' => "Cat\nDog"]);
+
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_myselect' => null,
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertFalse($uploader->prepare());
+        $this->assertArrayHasKey('customfieldinvalid', $uploader->get_errors());
+
+        // Try again with a default value.
+        $defaults = [
+            'customfield_myselect' => 2, // Our second option: Dog.
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload, $defaults);
+        $this->assertTrue($uploader->prepare());
+        $uploader->proceed();
+
+        // Confirm presence of course custom fields.
+        $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+        $this->assertEquals('Dog', $data->myselect);
+    }
+
+    /**
+     * Test upload processing of course custom field with an invalid select option
+     */
+    public function test_custom_fields_data_invalid_select_option() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom field.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'select', 'myselect',
+            ['required' => true, 'options' => "Cat\nDog", 'defaultvalue' => 'Cat']);
+
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_myselect' => 'Fish', // No, invalid.
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertTrue($uploader->prepare());
+        $uploader->proceed();
+
+        // Confirm presence of course custom fields.
+        $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+        $this->assertEquals('Cat', $data->myselect);
+    }
+
+    /**
+     * Test upload processing of course custom field with an out of range date
+     */
+    public function test_custom_fields_data_invalid_date() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+        // Create our custom field.
+        $category = $this->get_customfield_generator()->create_category();
+        $this->create_custom_field($category, 'date', 'mydate',
+            ['mindate' => strtotime('2020-04-01'), 'maxdate' => '2020-04-30']);
+
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $dataupload = [
+            'shortname' => $course->shortname,
+            'customfield_mydate' => '2020-05-06', // Out of range.
+        ];
+
+        $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+        $this->assertFalse($uploader->prepare());
+        $this->assertArrayHasKey('customfieldinvalid', $uploader->get_errors());
+    }
+
     public function test_idnumber_problems() {
         $this->resetAfterTest(true);
 
@@ -1224,7 +1354,34 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
         $this->assertFalse($co->prepare());
         $this->assertArrayHasKey('cannotrenameshortnamealreadyinuse', $co->get_errors());
+    }
 
+    /**
+     * Get custom field plugin generator
+     *
+     * @return core_customfield_generator
+     */
+    protected function get_customfield_generator() : core_customfield_generator {
+        return $this->getDataGenerator()->get_plugin_generator('core_customfield');
     }
 
-}
+    /**
+     * Helper method to create custom course field
+     *
+     * @param \core_customfield\category_controller $category
+     * @param string $type
+     * @param string $shortname
+     * @param array $configdata
+     * @return \core_customfield\field_controller
+     */
+    protected function create_custom_field(\core_customfield\category_controller $category, string $type, string $shortname,
+            array $configdata = []) : \core_customfield\field_controller {
+
+        return $this->get_customfield_generator()->create_field([
+            'categoryid' => $category->get('id'),
+            'type' => $type,
+            'shortname' => $shortname,
+            'configdata' => $configdata,
+        ]);
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv b/admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv
new file mode 100644 (file)
index 0000000..f949599
--- /dev/null
@@ -0,0 +1,2 @@
+shortname,fullname,summary,category,customfield_checkbox,customfield_date,customfield_select,customfield_text,customfield_textarea
+CF1,Course fields 1,Testing course fields,1,1,2019-10-01 14:00,b,Hello,Goodbye
\ No newline at end of file
index 6768507..feff463 100644 (file)
@@ -250,6 +250,81 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertArrayHasKey('invalidroles', $errors);
     }
 
+    /**
+     * Test custom field data processing
+     */
+    public function test_get_custom_course_field_data() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create all the fields!
+        $category = $this->get_customfield_generator()->create_category();
+
+        $checkboxfield = $this->create_custom_field($category, 'checkbox', 'mycheckbox');
+        $datefield = $this->create_custom_field($category, 'date', 'mydate');
+        $selectfield = $this->create_custom_field($category, 'select', 'myselect', ['options' => "Red\nGreen\nBlue"]);
+        $textfield = $this->create_custom_field($category, 'text', 'mytext', ['locked' => 1]);
+        $textareafield = $this->create_custom_field($category, 'textarea', 'mytextarea');
+
+        $fields = tool_uploadcourse_helper::get_custom_course_fields();
+        $this->assertCount(5, $fields);
+
+        $this->assertArrayHasKey($checkboxfield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_checkbox\field_controller::class, $fields[$checkboxfield->get('shortname')]);
+
+        $this->assertArrayHasKey($datefield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_date\field_controller::class, $fields[$datefield->get('shortname')]);
+
+        $this->assertArrayHasKey($selectfield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_select\field_controller::class, $fields[$selectfield->get('shortname')]);
+
+        $this->assertArrayHasKey($textfield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_text\field_controller::class, $fields[$textfield->get('shortname')]);
+
+        $this->assertArrayHasKey($textareafield->get('shortname'), $fields);
+        $this->assertInstanceOf(customfield_textarea\field_controller::class, $fields[$textareafield->get('shortname')]);
+
+        $data = [
+            'customfield_mycheckbox' => '1',
+            'customfield_mydate' => '2019-10-01',
+            'customfield_myselect' => 'Green',
+            'customfield_mytext' => 'Hello',
+            'customfield_myunknownfield' => 'Goodbye',
+        ];
+
+        $expected = [
+            'customfield_mycheckbox' => '1',
+            'customfield_mydate' => strtotime('2019-10-01'),
+            'customfield_myselect' => 2,
+            'customfield_mytext' => 'Hello',
+        ];
+
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_and_enrol($course, 'manager');
+        $this->setUser($user);
+
+        $context = context_course::instance($course->id);
+
+        $this->assertEquals($expected, tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context));
+
+        // Now add our custom textarea field (separately because the value of it's 'itemid' element is unknown).
+        $data['customfield_mytextarea'] = 'Something';
+        $fields = tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context);
+        $this->assertArrayHasKey('customfield_mytextarea_editor', $fields);
+        $this->assertArrayHasKey('text', $fields['customfield_mytextarea_editor']);
+        $this->assertEquals('Something', $fields['customfield_mytextarea_editor']['text']);
+
+        // Now prohibit the capability to change locked fields for the manager role.
+        $managerrole = $DB->get_record('role', ['shortname' => 'manager']);
+        role_change_permission($managerrole->id, $context, 'moodle/course:changelockedcustomfields', CAP_PROHIBIT);
+
+        // The locked 'mytext' custom field should not be returned.
+        $fields = tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context);
+        $this->assertCount(4, $fields);
+        $this->assertArrayNotHasKey('customfield_mytext', $fields);
+    }
+
     public function test_increment_idnumber() {
         $this->resetAfterTest(true);
 
@@ -394,4 +469,33 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path));
         $this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path));
     }
-}
+
+    /**
+     * Get custom field plugin generator
+     *
+     * @return core_customfield_generator
+     */
+    protected function get_customfield_generator() : core_customfield_generator {
+        return $this->getDataGenerator()->get_plugin_generator('core_customfield');
+    }
+
+    /**
+     * Helper method to create custom course field
+     *
+     * @param \core_customfield\category_controller $category
+     * @param string $type
+     * @param string $shortname
+     * @param array $configdata
+     * @return \core_customfield\field_controller
+     */
+    protected function create_custom_field(\core_customfield\category_controller $category, string $type, string $shortname,
+            array $configdata = []) : \core_customfield\field_controller {
+
+        return $this->get_customfield_generator()->create_field([
+            'categoryid' => $category->get('id'),
+            'type' => $type,
+            'shortname' => $shortname,
+            'configdata' => $configdata,
+        ]);
+    }
+}
\ No newline at end of file
index 77ababc..e879835 100644 (file)
@@ -158,7 +158,7 @@ $string['newtablefrommysql'] = 'New table from MySQL';
 $string['new_table_from_mysql'] = 'New table from MySQL';
 $string['nofieldsspecified'] = 'No fields specified';
 $string['nomasterprimaryuniquefound'] = 'The column(s) that your foreign key references must be included in a primary or unique KEY in the referenced table. Note that the column being in a UNIQUE INDEX is not good enough.';
-$string['nomissingorextraindexesfound'] = 'No missing or extra indexes have been found, your DB doesn\'t need further actions.';
+$string['nomissingorextraindexesfound'] = 'No missing or extra indexes have been found, so no further action is required.';
 $string['noreffieldsspecified'] = 'No reference fields specified';
 $string['noreftablespecified'] = 'Specified reference table not found';
 $string['noviolatedforeignkeysfound'] = 'No violated foreign keys found';
index babede0..4e5e8d3 100644 (file)
@@ -25,7 +25,6 @@
 define('NO_OUTPUT_BUFFERING', true);
 require_once('../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/dataformatlib.php');
 require_once($CFG->dirroot.'/user/profile/lib.php');
 
 $dataformat = optional_param('dataformat', '', PARAM_ALPHA);
@@ -69,9 +68,9 @@ if ($dataformat) {
     $downloadusers = new ArrayObject($SESSION->bulk_users);
     $iterator = $downloadusers->getIterator();
 
-    download_as_dataformat($filename, $dataformat, $fields, $iterator, function($userid) use ($extrafields, $fields) {
+    \core\dataformat::download_data($filename, $dataformat, $fields, $iterator, function($userid) use ($extrafields, $fields) {
         global $DB;
-        $row = array();
+
         if (!$user = $DB->get_record('user', array('id' => $userid))) {
             return null;
         }
index 1b2a7df..ac42715 100644 (file)
@@ -16,4 +16,4 @@ Feature: Add URL to main menu block
       | External URL | http://www.google.com |
       | id_display | In pop-up |
     Then "google" "link" should exist in the "Main menu" "block"
-    And "Add an activity or resource" "button" should exist in the "Main menu" "block"
+    And "Add an activity" "button" should exist in the "Main menu" "block"
index 2776339..dc4821b 100644 (file)
@@ -361,20 +361,23 @@ class cache_helper {
     /**
      * Ensure that the stats array is ready to collect information for the given store and definition.
      * @param string $store
+     * @param string $storeclass
      * @param string $definition A string that identifies the definition.
      * @param int $mode One of cache_store::MODE_*. Since 2.9.
      */
-    protected static function ensure_ready_for_stats($store, $definition, $mode = cache_store::MODE_APPLICATION) {
+    protected static function ensure_ready_for_stats($store, $storeclass, $definition, $mode = cache_store::MODE_APPLICATION) {
         // This function is performance-sensitive, so exit as quickly as possible
         // if we do not need to do anything.
         if (isset(self::$stats[$definition]['stores'][$store])) {
             return;
         }
+
         if (!array_key_exists($definition, self::$stats)) {
             self::$stats[$definition] = array(
                 'mode' => $mode,
                 'stores' => array(
                     $store => array(
+                        'class' => $storeclass,
                         'hits' => 0,
                         'misses' => 0,
                         'sets' => 0,
@@ -383,6 +386,7 @@ class cache_helper {
             );
         } else if (!array_key_exists($store, self::$stats[$definition]['stores'])) {
             self::$stats[$definition]['stores'][$store] = array(
+                'class' => $storeclass,
                 'hits' => 0,
                 'misses' => 0,
                 'sets' => 0,
@@ -418,15 +422,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param cache_definition $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $hits The number of hits to record (by default 1)
      */
     public static function record_cache_hit($store, $definition, $hits = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['hits'] += $hits;
     }
 
@@ -436,15 +447,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param string $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $misses The number of misses to record (by default 1)
      */
     public static function record_cache_miss($store, $definition, $misses = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['misses'] += $misses;
     }
 
@@ -454,15 +472,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param string $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $sets The number of sets to record (by default 1)
      */
     public static function record_cache_set($store, $definition, $sets = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['sets'] += $sets;
     }
 
index 8cd7914..6236cb0 100644 (file)
@@ -414,7 +414,7 @@ class cache implements cache_loader {
         $setaftervalidation = false;
         if ($result === false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss($this->storetype, $this->definition);
+                cache_helper::record_cache_miss($this->store, $this->definition);
             }
             if ($this->loader !== false) {
                 // We must pass the original (unparsed) key to the next loader in the chain.
@@ -426,7 +426,7 @@ class cache implements cache_loader {
             }
             $setaftervalidation = ($result !== false);
         } else if ($this->perfdebug) {
-            cache_helper::record_cache_hit($this->storetype, $this->definition);
+            cache_helper::record_cache_hit($this->store, $this->definition);
         }
         // 5. Validate strictness.
         if ($strictness === MUST_EXIST && $result === false) {
@@ -580,8 +580,8 @@ class cache implements cache_loader {
                     $hits++;
                 }
             }
-            cache_helper::record_cache_hit($this->storetype, $this->definition, $hits);
-            cache_helper::record_cache_miss($this->storetype, $this->definition, $misses);
+            cache_helper::record_cache_hit($this->store, $this->definition, $hits);
+            cache_helper::record_cache_miss($this->store, $this->definition, $misses);
         }
 
         // Return the result. Phew!
@@ -607,7 +607,7 @@ class cache implements cache_loader {
      */
     public function set($key, $data) {
         if ($this->perfdebug) {
-            cache_helper::record_cache_set($this->storetype, $this->definition);
+            cache_helper::record_cache_set($this->store, $this->definition);
         }
         if ($this->loader !== false) {
             // We have a loader available set it there as well.
@@ -762,7 +762,7 @@ class cache implements cache_loader {
         }
         $successfullyset = $this->store->set_many($data);
         if ($this->perfdebug && $successfullyset) {
-            cache_helper::record_cache_set($this->storetype, $this->definition, $successfullyset);
+            cache_helper::record_cache_set($this->store, $this->definition, $successfullyset);
         }
         return $successfullyset;
     }
@@ -1112,7 +1112,7 @@ class cache implements cache_loader {
         }
         if ($result !== false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_hit('** static acceleration **', $this->definition);
+                cache_helper::record_cache_hit(cache_store::STATIC_ACCEL, $this->definition);
             }
             if ($this->staticaccelerationsize > 1 && $this->staticaccelerationcount > 1) {
                 // Check to see if this is the last item on the static acceleration keys array.
@@ -1126,7 +1126,7 @@ class cache implements cache_loader {
             return $result;
         } else {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss('** static acceleration **', $this->definition);
+                cache_helper::record_cache_miss(cache_store::STATIC_ACCEL, $this->definition);
             }
             return false;
         }
@@ -1830,7 +1830,7 @@ class cache_session extends cache {
         // 4. Load if from the loader/datasource if we don't already have it.
         if ($result === false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss($this->storetype, $this->get_definition());
+                cache_helper::record_cache_miss($this->get_store(), $this->get_definition());
             }
             if ($this->get_loader() !== false) {
                 // We must pass the original (unparsed) key to the next loader in the chain.
@@ -1845,7 +1845,7 @@ class cache_session extends cache {
                 $this->set($key, $result);
             }
         } else if ($this->perfdebug) {
-            cache_helper::record_cache_hit($this->storetype, $this->get_definition());
+            cache_helper::record_cache_hit($this->get_store(), $this->get_definition());
         }
         // 5. Validate strictness.
         if ($strictness === MUST_EXIST && $result === false) {
@@ -1889,7 +1889,7 @@ class cache_session extends cache {
             $loader->set($key, $data);
         }
         if ($this->perfdebug) {
-            cache_helper::record_cache_set($this->storetype, $this->get_definition());
+            cache_helper::record_cache_set($this->get_store(), $this->get_definition());
         }
         if (is_object($data) && $data instanceof cacheable_object) {
             $data = new cache_cached_object($data);
@@ -2019,8 +2019,8 @@ class cache_session extends cache {
                     $hits++;
                 }
             }
-            cache_helper::record_cache_hit($this->storetype, $this->get_definition(), $hits);
-            cache_helper::record_cache_miss($this->storetype, $this->get_definition(), $misses);
+            cache_helper::record_cache_hit($this->get_store(), $this->get_definition(), $hits);
+            cache_helper::record_cache_miss($this->get_store(), $this->get_definition(), $misses);
         }
         return $return;
 
@@ -2097,7 +2097,7 @@ class cache_session extends cache {
         }
         $successfullyset = $this->get_store()->set_many($data);
         if ($this->perfdebug && $successfullyset) {
-            cache_helper::record_cache_set($this->storetype, $this->get_definition(), $successfullyset);
+            cache_helper::record_cache_set($this->store, $this->get_definition(), $successfullyset);
         }
         return $successfullyset;
     }
index 4fcb03f..a2cfe3e 100644 (file)
@@ -144,6 +144,10 @@ abstract class cache_store implements cache_store_interface {
      * Request caches. Static caches really.
      */
     const MODE_REQUEST = 4;
+    /**
+     * Static caches.
+     */
+    const STATIC_ACCEL = '** static accel. **';
 
     /**
      * Constructs an instance of the cache store.
index 9c0f1d9..0e6b203 100644 (file)
@@ -2092,15 +2092,15 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertFalse($request->get('missMe'));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(1, $endstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(1, $endstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2116,24 +2116,24 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertTrue($request->set('setMe4', 4));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2149,24 +2149,24 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertEquals($request->get('setMe4'), 4);
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2176,24 +2176,24 @@ class core_cache_testcase extends advanced_testcase {
         $request->get_many(array('setMe1', 'setMe2', 'setMe3', 'setMe4'));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
     }
 
     public function test_static_cache() {
@@ -2225,8 +2225,8 @@ class core_cache_testcase extends advanced_testcase {
 
         // Check that the static acceleration worked, even on empty arrays and the number 0.
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['** static acceleration **']['misses']);
-        $this->assertEquals(3, $endstats[$applicationid]['stores']['** static acceleration **']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['** static accel. **']['misses']);
+        $this->assertEquals(3, $endstats[$applicationid]['stores']['** static accel. **']['hits']);
     }
 
     public function test_performance_debug_off() {
index eaf6344..076bd53 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /cache/stores/* - cache store plugins.
 Information provided here is intended especially for developers.
 
+=== 3.9 ===
+* The record_cache_hit/miss/set methods now take a cache_store instead of a cache_definition object
+
 === 3.8 ===
 * The Redis cache store can now make use of the Zstandard compression algorithm (see MDL-66428).
 
index d44cd94..31d3420 100644 (file)
Binary files a/calendar/amd/build/crud.min.js and b/calendar/amd/build/crud.min.js differ
index c6e0e09..499e7b6 100644 (file)
Binary files a/calendar/amd/build/crud.min.js.map and b/calendar/amd/build/crud.min.js.map differ
index 184a283..77dbc02 100644 (file)
Binary files a/calendar/amd/build/modal_delete.min.js and b/calendar/amd/build/modal_delete.min.js differ
index 4a4c185..090c326 100644 (file)
Binary files a/calendar/amd/build/modal_delete.min.js.map and b/calendar/amd/build/modal_delete.min.js.map differ
index 7853a67..c18cb56 100644 (file)
@@ -63,6 +63,7 @@ function(
      * @return {Promise}
      */
     function confirmDeletion(eventId, eventTitle, eventCount) {
+        var pendingPromise = new Pending('core_calendar/crud:confirmDeletion');
         var deleteStrings = [
             {
                 key: 'deleteevent',
@@ -139,6 +140,11 @@ function(
 
             return deleteModal;
         })
+        .then(function(modal) {
+            pendingPromise.resolve();
+
+            return modal;
+        })
         .catch(Notification.exception);
 
         return finalPromise;
index 9541dec..e457761 100644 (file)
@@ -55,6 +55,8 @@ function(
      */
     var ModalDelete = function(root) {
         Modal.call(this, root);
+
+        this.setRemoveOnClose(true);
     };
 
     ModalDelete.TYPE = 'core_calendar-modal_delete';
index 9a3f1d0..3b08693 100644 (file)
@@ -13,7 +13,7 @@
     "require-dev": {
         "phpunit/phpunit": "7.5.*",
         "phpunit/dbunit": "4.0.*",
-        "moodlehq/behat-extension": "3.39.2",
+        "moodlehq/behat-extension": "3.39.3",
         "mikey179/vfsstream": "^1.6",
         "instaclick/php-webdriver": "dev-local as 1.x-dev"
     }
index 7fd4234..11dbff6 100644 (file)
@@ -4,42 +4,44 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "a2329ee2d14a351b74f99322f42722da",
+    "content-hash": "b1953ceec577434625a7aee12f650daa",
     "packages": [],
     "packages-dev": [
         {
             "name": "behat/behat",
-            "version": "v3.5.0",
+            "version": "v3.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Behat.git",
-                "reference": "e4bce688be0c2029dc1700e46058d86428c63cab"
+                "reference": "9bfe195b4745c32e068af03fa4df9558b4916d30"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Behat/zipball/e4bce688be0c2029dc1700e46058d86428c63cab",
-                "reference": "e4bce688be0c2029dc1700e46058d86428c63cab",
+                "url": "https://api.github.com/repos/Behat/Behat/zipball/9bfe195b4745c32e068af03fa4df9558b4916d30",
+                "reference": "9bfe195b4745c32e068af03fa4df9558b4916d30",
                 "shasum": ""
             },
             "require": {
-                "behat/gherkin": "^4.5.1",
+                "behat/gherkin": "^4.6.0",
                 "behat/transliterator": "^1.2",
                 "container-interop/container-interop": "^1.2",
                 "ext-mbstring": "*",
                 "php": ">=5.3.3",
                 "psr/container": "^1.0",
-                "symfony/class-loader": "~2.1||~3.0",
-                "symfony/config": "~2.3||~3.0||~4.0",
-                "symfony/console": "~2.7.40||^2.8.33||~3.3.15||^3.4.3||^4.0.3",
-                "symfony/dependency-injection": "~2.1||~3.0||~4.0",
-                "symfony/event-dispatcher": "~2.1||~3.0||~4.0",
-                "symfony/translation": "~2.3||~3.0||~4.0",
-                "symfony/yaml": "~2.1||~3.0||~4.0"
+                "symfony/config": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/console": "^2.7.51 || ^2.8.33 || ^3.3.15 || ^3.4.3 || ^4.0.3 || ^5.0",
+                "symfony/dependency-injection": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/event-dispatcher": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/translation": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/yaml": "^2.7.51 || ^3.0 || ^4.0 || ^5.0"
             },
             "require-dev": {
                 "herrera-io/box": "~1.6.1",
-                "phpunit/phpunit": "^4.8.36|^6.3",
-                "symfony/process": "~2.5|~3.0|~4.0"
+                "phpunit/phpunit": "^4.8.36 || ^6.3",
+                "symfony/process": "~2.5 || ^3.0 || ^4.0 || ^5.0"
+            },
+            "suggest": {
+                "ext-dom": "Needed to output test results in JUnit format."
             },
             "bin": [
                 "bin/behat"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.5.x-dev"
+                    "dev-master": "3.6.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Behat\\Behat": "src/",
-                    "Behat\\Testwork": "src/"
+                "psr-4": {
+                    "Behat\\Behat\\": "src/Behat/Behat/",
+                    "Behat\\Testwork\\": "src/Behat/Testwork/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -83,7 +85,7 @@
                 "symfony",
                 "testing"
             ],
-            "time": "2018-08-10T18:56:51+00:00"
+            "time": "2020-02-06T09:54:48+00:00"
         },
         {
             "name": "behat/gherkin",
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.5.2",
+            "version": "6.5.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82"
+                "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82",
-                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aab4ebd862aa7d04f01a4b51849d657db56d882e",
+                "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "guzzlehttp/promises": "^1.0",
                 "guzzlehttp/psr7": "^1.6.1",
-                "php": ">=5.5"
+                "php": ">=5.5",
+                "symfony/polyfill-intl-idn": "^1.11"
             },
             "require-dev": {
                 "ext-curl": "*",
                 "psr/log": "^1.1"
             },
             "suggest": {
-                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
                 "psr/log": "Required for using the Log middleware"
             },
             "type": "library",
                 "rest",
                 "web service"
             ],
-            "time": "2019-12-23T11:57:10+00:00"
+            "time": "2020-04-18T10:38:46+00:00"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.39.2",
+            "version": "v3.39.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "7a2df2124ba8a85ccf21e517d18c78f932bdbbce"
+                "reference": "d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/7a2df2124ba8a85ccf21e517d18c78f932bdbbce",
-                "reference": "7a2df2124ba8a85ccf21e517d18c78f932bdbbce",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b",
+                "reference": "d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b",
                 "shasum": ""
             },
             "require": {
-                "behat/behat": "3.5.*",
+                "behat/behat": "3.6.*",
                 "behat/mink": "~1.8",
-                "behat/mink-extension": "~2.2",
+                "behat/mink-extension": "~2.3",
                 "behat/mink-goutte-driver": "~1.2",
-                "behat/mink-selenium2-driver": "~1.3",
+                "behat/mink-selenium2-driver": "~1.4",
                 "php": ">=7.2.0",
-                "symfony/process": "2.8.*"
+                "symfony/process": "^4.0 || ^5.0"
             },
             "type": "library",
             "autoload": {
                 "Behat",
                 "moodle"
             ],
-            "time": "2020-04-09T16:06:14+00:00"
+            "time": "2020-04-20T09:32:44+00:00"
         },
         {
             "name": "myclabs/deep-copy",
             "time": "2017-02-14T16:28:37+00:00"
         },
         {
-            "name": "psr/http-message",
-            "version": "1.0.1",
+            "name": "psr/event-dispatcher",
+            "version": "1.0.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/php-fig/http-message.git",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+                "url": "https://github.com/php-fig/event-dispatcher.git",
+                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.0"
+                "php": ">=7.2.0"
             },
             "type": "library",
             "extra": {
             },
             "autoload": {
                 "psr-4": {
-                    "Psr\\Http\\Message\\": "src/"
+                    "Psr\\EventDispatcher\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "homepage": "http://www.php-fig.org/"
                 }
             ],
-            "description": "Common interface for HTTP messages",
-            "homepage": "https://github.com/php-fig/http-message",
+            "description": "Standard interfaces for event handling.",
             "keywords": [
-                "http",
-                "http-message",
+                "events",
                 "psr",
-                "psr-7",
-                "request",
-                "response"
+                "psr-14"
             ],
-            "time": "2016-08-06T14:39:51+00:00"
+            "time": "2019-01-08T18:20:26+00:00"
         },
         {
-            "name": "psr/log",
-            "version": "1.1.3",
+            "name": "psr/http-message",
+            "version": "1.0.1",
             "source": {
                 "type": "git",
-                "url": "https://github.com/php-fig/log.git",
-                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
-                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1.x-dev"
+                    "dev-master": "1.0.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Psr\\Log\\": "Psr/Log/"
+                    "Psr\\Http\\Message\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "homepage": "http://www.php-fig.org/"
                 }
             ],
-            "description": "Common interface for logging libraries",
-            "homepage": "https://github.com/php-fig/log",
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
             "keywords": [
-                "log",
+                "http",
+                "http-message",
                 "psr",
-                "psr-3"
+                "psr-7",
+                "request",
+                "response"
             ],
-            "time": "2020-03-23T09:12:05+00:00"
+            "time": "2016-08-06T14:39:51+00:00"
         },
         {
             "name": "ralouphie/getallheaders",
             ],
             "time": "2020-03-28T10:15:50+00:00"
         },
-        {
-            "name": "symfony/class-loader",
-            "version": "v3.4.39",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/class-loader.git",
-                "reference": "e4636a4f23f157278a19e5db160c63de0da297d8"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/e4636a4f23f157278a19e5db160c63de0da297d8",
-                "reference": "e4636a4f23f157278a19e5db160c63de0da297d8",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.5.9|>=7.0.8"
-            },
-            "require-dev": {
-                "symfony/finder": "~2.8|~3.0|~4.0",
-                "symfony/polyfill-apcu": "~1.1"
-            },
-            "suggest": {
-                "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.4-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\ClassLoader\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony ClassLoader Component",
-            "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-03-15T09:38:08+00:00"
-        },
         {
             "name": "symfony/config",
             "version": "v4.4.7",
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
             "time": "2020-03-27T16:54:36+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.18",
+            "version": "v5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "af7ec995de93671c03cc1b4e3176c8588bc79dcc"
+                "reference": "5fa1caadc8cdaa17bcfb25219f3b53fe294a9935"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/af7ec995de93671c03cc1b4e3176c8588bc79dcc",
-                "reference": "af7ec995de93671c03cc1b4e3176c8588bc79dcc",
+                "url": "https://api.github.com/repos/symfony/console/zipball/5fa1caadc8cdaa17bcfb25219f3b53fe294a9935",
+                "reference": "5fa1caadc8cdaa17bcfb25219f3b53fe294a9935",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "symfony/debug": "~2.8|~3.0",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.2.5",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php73": "^1.8",
+                "symfony/service-contracts": "^1.1|^2"
             },
             "conflict": {
-                "symfony/dependency-injection": "<3.3"
+                "symfony/dependency-injection": "<4.4",
+                "symfony/event-dispatcher": "<4.4",
+                "symfony/lock": "<4.4",
+                "symfony/process": "<4.4"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~3.3",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/event-dispatcher": "~2.8|~3.0",
-                "symfony/filesystem": "~2.8|~3.0",
-                "symfony/process": "~2.8|~3.0"
+                "symfony/config": "^4.4|^5.0",
+                "symfony/dependency-injection": "^4.4|^5.0",
+                "symfony/event-dispatcher": "^4.4|^5.0",
+                "symfony/lock": "^4.4|^5.0",
+                "symfony/process": "^4.4|^5.0",
+                "symfony/var-dumper": "^4.4|^5.0"
             },
             "suggest": {
                 "psr/log": "For using the console logger",
                 "symfony/event-dispatcher": "",
-                "symfony/filesystem": "",
+                "symfony/lock": "",
                 "symfony/process": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-29T09:02:23+00:00"
+            "time": "2020-03-30T11:42:42+00:00"
         },
         {
             "name": "symfony/css-selector",
             ],
             "time": "2020-03-27T16:56:45+00:00"
         },
-        {
-            "name": "symfony/debug",
-            "version": "v3.4.39",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/debug.git",
-                "reference": "ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29",
-                "reference": "ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "psr/log": "~1.0"
-            },
-            "conflict": {
-                "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
-            },
-            "require-dev": {
-                "symfony/http-kernel": "~2.8|~3.0|~4.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.4-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Debug\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony Debug Component",
-            "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-03-23T10:22:40+00:00"
-        },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.18",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "54243abc4e1a1a15e274e391bd6f7090b44711f1"
+                "reference": "755b18859be26b90f4bf63753432d3387458bf31"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/54243abc4e1a1a15e274e391bd6f7090b44711f1",
-                "reference": "54243abc4e1a1a15e274e391bd6f7090b44711f1",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/755b18859be26b90f4bf63753432d3387458bf31",
+                "reference": "755b18859be26b90f4bf63753432d3387458bf31",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "psr/container": "^1.0"
+                "php": "^7.1.3",
+                "psr/container": "^1.0",
+                "symfony/service-contracts": "^1.1.6|^2"
             },
             "conflict": {
-                "symfony/config": "<3.3.7",
-                "symfony/finder": "<3.3",
-                "symfony/yaml": "<3.3"
+                "symfony/config": "<4.3|>=5.0",
+                "symfony/finder": "<3.4",
+                "symfony/proxy-manager-bridge": "<3.4",
+                "symfony/yaml": "<3.4"
             },
             "provide": {
-                "psr/container-implementation": "1.0"
+                "psr/container-implementation": "1.0",
+                "symfony/service-implementation": "1.0"
             },
             "require-dev": {
-                "symfony/config": "~3.3",
-                "symfony/expression-language": "~2.8|~3.0",
-                "symfony/yaml": "~3.3"
+                "symfony/config": "^4.3",
+                "symfony/expression-language": "^3.4|^4.0|^5.0",
+                "symfony/yaml": "^3.4|^4.0|^5.0"
             },
             "suggest": {
                 "symfony/config": "",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.4-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-29T09:02:23+00:00"
+            "time": "2020-03-30T10:09:30+00:00"
         },
         {
             "name": "symfony/dom-crawler",
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.39",
+            "version": "v5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "9d4e22943b73acc1ba50595b7de1a01fe9dbad48"
+                "reference": "24f40d95385774ed5c71dbf014edd047e2f2f3dc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9d4e22943b73acc1ba50595b7de1a01fe9dbad48",
-                "reference": "9d4e22943b73acc1ba50595b7de1a01fe9dbad48",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/24f40d95385774ed5c71dbf014edd047e2f2f3dc",
+                "reference": "24f40d95385774ed5c71dbf014edd047e2f2f3dc",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^7.2.5",
+                "symfony/event-dispatcher-contracts": "^2"
             },
             "conflict": {
-                "symfony/dependency-injection": "<3.3"
+                "symfony/dependency-injection": "<4.4"
+            },
+            "provide": {
+                "psr/event-dispatcher-implementation": "1.0",
+                "symfony/event-dispatcher-implementation": "2.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0|~4.0",
-                "symfony/dependency-injection": "~3.3|~4.0",
-                "symfony/expression-language": "~2.8|~3.0|~4.0",
-                "symfony/stopwatch": "~2.8|~3.0|~4.0"
+                "symfony/config": "^4.4|^5.0",
+                "symfony/dependency-injection": "^4.4|^5.0",
+                "symfony/expression-language": "^4.4|^5.0",
+                "symfony/http-foundation": "^4.4|^5.0",
+                "symfony/service-contracts": "^1.1|^2",
+                "symfony/stopwatch": "^4.4|^5.0"
             },
             "suggest": {
                 "symfony/dependency-injection": "",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.4-dev"
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-03-15T09:38:08+00:00"
+            "time": "2020-03-27T16:56:45+00:00"
+        },
+        {
+            "name": "symfony/event-dispatcher-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+                "reference": "af23c2584d4577d54661c434446fb8fbed6025dd"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/af23c2584d4577d54661c434446fb8fbed6025dd",
+                "reference": "af23c2584d4577d54661c434446fb8fbed6025dd",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5",
+                "psr/event-dispatcher": "^1"
+            },
+            "suggest": {
+                "symfony/event-dispatcher-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\EventDispatcher\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to dispatching event",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
         },
         {
             "name": "symfony/filesystem",
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
             "time": "2020-03-27T16:56:45+00:00"
         },
         {
             "time": "2020-02-27T09:26:54+00:00"
         },
         {
-            "name": "symfony/polyfill-mbstring",
+            "name": "symfony/polyfill-intl-idn",
             "version": "v1.15.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
+                "url": "https://github.com/symfony/polyfill-intl-idn.git",
+                "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
-                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
+                "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": ">=5.3.3",
+                "symfony/polyfill-mbstring": "^1.3",
+                "symfony/polyfill-php72": "^1.10"
             },
             "suggest": {
-                "ext-mbstring": "For best performance"
+                "ext-intl": "For best performance"
             },
             "type": "library",
             "extra": {
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Polyfill\\Mbstring\\": ""
+                    "Symfony\\Polyfill\\Intl\\Idn\\": ""
                 },
                 "files": [
                     "bootstrap.php"
             ],
             "authors": [
                 {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
+                    "name": "Laurent Bassin",
+                    "email": "laurent@bassin.info"
                 },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony polyfill for the Mbstring extension",
+            "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
             "homepage": "https://symfony.com",
             "keywords": [
                 "compatibility",
-                "mbstring",
+                "idn",
+                "intl",
                 "polyfill",
                 "portable",
                 "shim"
             "time": "2020-03-09T19:04:49+00:00"
         },
         {
-            "name": "symfony/process",
-            "version": "v2.8.52",
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.15.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/process.git",
-                "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8"
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.15-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-09T19:04:49+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php72",
+            "version": "v1.15.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php72.git",
+                "reference": "37b0976c78b94856543260ce09b460a7bc852747"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747",
+                "reference": "37b0976c78b94856543260ce09b460a7bc852747",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.15-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php72\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-02-27T09:26:54+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php73",
+            "version": "v1.15.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php73.git",
+                "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8",
-                "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8",
+                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
+                "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.9"
+                "php": ">=5.3.3"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.8-dev"
+                    "dev-master": "1.15-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php73\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-02-27T09:26:54+00:00"
+        },
+        {
+            "name": "symfony/process",
+            "version": "v5.0.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/process.git",
+                "reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/process/zipball/c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e",
+                "reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-11T11:18:13+00:00"
+            "time": "2020-03-27T16:56:45+00:00"
+        },
+        {
+            "name": "symfony/service-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/service-contracts.git",
+                "reference": "144c5e51266b281231e947b51223ba14acf1a749"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749",
+                "reference": "144c5e51266b281231e947b51223ba14acf1a749",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5",
+                "psr/container": "^1.0"
+            },
+            "suggest": {
+                "symfony/service-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Service\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to writing services",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.18",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "90cb5ca3eb84b3053fef876e11e405fd819487fc"
+                "reference": "4e54d336f2eca5facad449d0b0118bb449375b76"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/90cb5ca3eb84b3053fef876e11e405fd819487fc",
-                "reference": "90cb5ca3eb84b3053fef876e11e405fd819487fc",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/4e54d336f2eca5facad449d0b0118bb449375b76",
+                "reference": "4e54d336f2eca5facad449d0b0118bb449375b76",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/translation-contracts": "^1.1.6|^2"
             },
             "conflict": {
-                "symfony/config": "<2.8",
-                "symfony/yaml": "<3.3"
+                "symfony/config": "<3.4",
+                "symfony/dependency-injection": "<3.4",
+                "symfony/http-kernel": "<4.4",
+                "symfony/yaml": "<3.4"
+            },
+            "provide": {
+                "symfony/translation-implementation": "1.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/intl": "^2.8.18|^3.2.5",
-                "symfony/yaml": "~3.3"
+                "symfony/config": "^3.4|^4.0|^5.0",
+                "symfony/console": "^3.4|^4.0|^5.0",
+                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+                "symfony/finder": "~2.8|~3.0|~4.0|^5.0",
+                "symfony/http-kernel": "^4.4",
+                "symfony/intl": "^3.4|^4.0|^5.0",
+                "symfony/service-contracts": "^1.1.2|^2",
+                "symfony/yaml": "^3.4|^4.0|^5.0"
             },
             "suggest": {
-                "psr/log": "To use logging capability in translator",
+                "psr/log-implementation": "To use logging capability in translator",
                 "symfony/config": "",
                 "symfony/yaml": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.4-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-18T14:19:00+00:00"
+            "time": "2020-03-27T16:54:36+00:00"
+        },
+        {
+            "name": "symfony/translation-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/translation-contracts.git",
+                "reference": "8cc682ac458d75557203b2f2f14b0b92e1c744ed"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/8cc682ac458d75557203b2f2f14b0b92e1c744ed",
+                "reference": "8cc682ac458d75557203b2f2f14b0b92e1c744ed",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5"
+            },
+            "suggest": {
+                "symfony/translation-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Translation\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to translation",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.18",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "af615970e265543a26ee712c958404eb9b7ac93d"
+                "reference": "ef166890d821518106da3560086bfcbeb4fadfec"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/af615970e265543a26ee712c958404eb9b7ac93d",
-                "reference": "af615970e265543a26ee712c958404eb9b7ac93d",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/ef166890d821518106da3560086bfcbeb4fadfec",
+                "reference": "ef166890d821518106da3560086bfcbeb4fadfec",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^7.1.3",
+                "symfony/polyfill-ctype": "~1.8"
+            },
+            "conflict": {
+                "symfony/console": "<3.4"
             },
             "require-dev": {
-                "symfony/console": "~2.8|~3.0"
+                "symfony/console": "^3.4|^4.0|^5.0"
             },
             "suggest": {
                 "symfony/console": "For validating YAML files using the lint command"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.4-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-20T15:04:53+00:00"
+            "time": "2020-03-30T11:41:10+00:00"
         },
         {
             "name": "theseer/tokenizer",
         },
         {
             "name": "webmozart/assert",
-            "version": "1.7.0",
+            "version": "1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "aed98a490f9a8f78468232db345ab9cf606cf598"
+                "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598",
-                "reference": "aed98a490f9a8f78468232db345ab9cf606cf598",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
+                "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
                 "shasum": ""
             },
             "require": {
                 "symfony/polyfill-ctype": "^1.8"
             },
             "conflict": {
-                "vimeo/psalm": "<3.6.0"
+                "vimeo/psalm": "<3.9.1"
             },
             "require-dev": {
                 "phpunit/phpunit": "^4.8.36 || ^7.5.13"
                 "check",
                 "validate"
             ],
-            "time": "2020-02-14T12:15:55+00:00"
+            "time": "2020-04-18T12:12:48+00:00"
         }
     ],
     "aliases": [
index fb45533..90989bb 100644 (file)
@@ -742,6 +742,9 @@ $CFG->admin = 'admin';
 // Force developer level debug and add debug info to the output of cron
 // $CFG->showcrondebugging = true;
 //
+// Force result of checks used to determine whether a site is considered "public" or not (such as for site registration).
+// $CFG->site_is_public = false;
+//
 //=========================================================================
 // 8. FORCED SETTINGS
 //=========================================================================
index 2e95d9a..4d78f53 100644 (file)
Binary files a/contentbank/amd/build/actions.min.js and b/contentbank/amd/build/actions.min.js differ
index 05f46fb..6e5344e 100644 (file)
Binary files a/contentbank/amd/build/actions.min.js.map and b/contentbank/amd/build/actions.min.js.map differ
index caa25c5..561cb99 100644 (file)
@@ -39,6 +39,7 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents)
      */
     var ACTIONS = {
         DELETE_CONTENT: '[data-action="deletecontent"]',
+        RENAME_CONTENT: '[data-action="renamecontent"]',
     };
 
     /**
@@ -108,6 +109,54 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents)
                 return;
             }).catch(Notification.exception);
         });
+
+        $(ACTIONS.RENAME_CONTENT).click(function(e) {
+            e.preventDefault();
+
+            var contentname = $(this).data('contentname');
+            var contentid = $(this).data('contentid');
+
+            var strings = [
+                {
+                    key: 'renamecontent',
+                    component: 'core_contentbank'
+                },
+                {
+                    key: 'rename',
+                    component: 'core_contentbank'
+                },
+            ];
+
+            var saveButtonText = '';
+            Str.get_strings(strings).then(function(langStrings) {
+                var modalTitle = langStrings[0];
+                saveButtonText = langStrings[1];
+
+                return ModalFactory.create({
+                    title: modalTitle,
+                    body: Templates.render('core_contentbank/renamecontent', {'contentid': contentid, 'name': contentname}),
+                    type: ModalFactory.types.SAVE_CANCEL
+                });
+            }).then(function(modal) {
+                modal.setSaveButtonText(saveButtonText);
+                modal.getRoot().on(ModalEvents.save, function() {
+                    // The action is now confirmed, sending an action for it.
+                    var newname = $("#newname").val();
+                    return renameContent(contentid, newname);
+                });
+
+                // Handle hidden event.
+                modal.getRoot().on(ModalEvents.hidden, function() {
+                    // Destroy when hidden.
+                    modal.destroy();
+                });
+
+                // Show the modal.
+                modal.show();
+
+                return;
+            }).catch(Notification.exception);
+        });
     };
 
     /**
@@ -146,6 +195,49 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents)
         }).fail(Notification.exception);
     }
 
+    /**
+     * Rename content in the content bank.
+     *
+     * @param {int} contentid The content to rename.
+     * @param {string} name The new name for the content.
+     */
+    function renameContent(contentid, name) {
+        var request = {
+            methodname: 'core_contentbank_rename_content',
+            args: {
+                contentid: contentid,
+                name: name
+            }
+        };
+        var requestType = 'success';
+        Ajax.call([request])[0].then(function(data) {
+            if (data) {
+                return Str.get_string('contentrenamed', 'core_contentbank');
+            }
+            requestType = 'error';
+            return Str.get_string('contentnotrenamed', 'core_contentbank');
+
+        }).then(function(message) {
+            var params = null;
+            if (requestType == 'success') {
+                params = {
+                    id: contentid,
+                    statusmsg: message
+                };
+                // Redirect to the content view page and display the message as a notification.
+                window.location.href = Url.relativeUrl('contentbank/view.php', params, false);
+            } else {
+                // Fetch error notifications.
+                Notification.addNotification({
+                    message: message,
+                    type: 'error'
+                });
+                Notification.fetchNotifications();
+            }
+            return;
+        }).catch(Notification.exception);
+    }
+
     return /** @alias module:core_contentbank/actions */ {
         // Public variables and functions.
 
index a77b7a0..27975cb 100644 (file)
@@ -24,6 +24,7 @@
 
 namespace core_contentbank;
 
+use core_text;
 use stored_file;
 use stdClass;
 use coding_exception;
@@ -102,6 +103,33 @@ abstract class content {
         return $DB->update_record('contentbank_content', $this->content);
     }
 
+    /**
+     * Set a new name to the content.
+     *
+     * @param string $name  The name of the content.
+     * @return bool  True if the content has been succesfully updated. False otherwise.
+     * @throws \coding_exception if not loaded.
+     */
+    public function set_name(string $name): bool {
+        if (empty($name)) {
+            return false;
+        }
+
+        // Clean name.
+        $name = clean_param($name, PARAM_TEXT);
+        if (core_text::strlen($name) > 255) {
+            $name = core_text::substr($name, 0, 255);
+        }
+
+        $oldname = $this->content->name;
+        $this->content->name = $name;
+        $updated = $this->update_content();
+        if (!$updated) {
+            $this->content->name = $oldname;
+        }
+        return $updated;
+    }
+
     /**
      * Returns the name of the content.
      *
index 46e92a9..05922b7 100644 (file)
@@ -24,7 +24,6 @@
 
 namespace core_contentbank;
 
-use coding_exception;
 use moodle_url;
 
 /**
@@ -39,7 +38,7 @@ abstract class contenttype {
     /** Plugin implements uploading feature */
     const CAN_UPLOAD = 'upload';
 
-    /** @var context This content's context. **/
+    /** @var context This contenttype's context. **/
     protected $context = null;
 
     /**
@@ -99,6 +98,18 @@ abstract class contenttype {
         return $DB->delete_records('contentbank_content', ['id' => $content->get_id()]);
     }
 
+    /**
+     * Rename this content from the content_bank.
+     * This method can be overwritten by the plugins if they need to change some other specific information.
+     *
+     * @param  content $content The content to rename.
+     * @param string $name  The name of the content.
+     * @return boolean true if the content has been renamed; false otherwise.
+     */
+    public function rename_content(content $content, string $name): bool {
+        return $content->set_name($name);
+    }
+
     /**
      * Returns the contenttype name of this content.
      *
@@ -240,6 +251,41 @@ abstract class contenttype {
         return true;
     }
 
+    /**
+     * Check if the user can managed this content.
+     *
+     * @param  content $content The content to be managed.
+     * @return bool     True if content could be managed. False otherwise.
+     */
+    public final function can_manage(content $content): bool {
+        global $USER;
+
+        if ($this->context->id != $content->get_content()->contextid) {
+            // The content has to have exactly the same context as this contenttype.
+            return false;
+        }
+
+        // Check main contentbank management permission.
+        $hascapability = has_capability('moodle/contentbank:manageanycontent', $this->context);
+        if ($content->get_content()->usercreated == $USER->id) {
+            // This content has been created by the current user; check if they can manage their content.
+            $hascapability = $hascapability || has_capability('moodle/contentbank:manageowncontent', $this->context);
+        }
+
+        return $hascapability && $this->is_manage_allowed($content);
+    }
+
+    /**
+     * Returns if content allows managing.
+     *
+     * @param  content $content The content to be managed.
+     * @return bool True if content allows uploading. False otherwise.
+     */
+    protected function is_manage_allowed(content $content): bool {
+        // Plugins can overwrite this function to add any check they need.
+        return true;
+    }
+
     /**
      * Returns the plugin supports the feature.
      *
diff --git a/contentbank/classes/external/rename_content.php b/contentbank/classes/external/rename_content.php
new file mode 100644 (file)
index 0000000..18d36f3
--- /dev/null
@@ -0,0 +1,136 @@
+<?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/>.
+
+/**
+ * External API to rename content bank content.
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/externallib.php');
+
+use external_api;
+use external_function_parameters;
+use external_single_structure;
+use external_value;
+use external_warnings;
+
+/**
+ * This is the external method for renaming a content.
+ *
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class rename_content extends external_api {
+    /**
+     * rename_content parameters.
+     *
+     * @since  Moodle 3.9
+     * @return external_function_parameters
+     */
+    public static function execute_parameters(): external_function_parameters {
+        return new external_function_parameters(
+            [
+                'contentid' => new external_value(PARAM_INT, 'The content id to rename', VALUE_REQUIRED),
+                'name' => new external_value(PARAM_RAW, 'The new name for the content', VALUE_REQUIRED),
+            ]
+        );
+    }
+
+    /**
+     * Rename content from the contentbank.
+     *
+     * @since  Moodle 3.9
+     * @param  int $contentid The content id to rename.
+     * @param  string $name The new name.
+     * @return array True if the content has been renamed; false and the warning, otherwise.
+     */
+    public static function execute(int $contentid, string $name): array {
+        global $DB;
+
+        $result = false;
+        $warnings = [];
+
+        $params = self::validate_parameters(self::execute_parameters(), [
+            'contentid' => $contentid,
+            'name' => $name,
+        ]);
+        $params['name'] = clean_param($params['name'], PARAM_TEXT);
+        try {
+            $record = $DB->get_record('contentbank_content', ['id' => $contentid], '*', MUST_EXIST);
+            $contenttypeclass = "\\$record->contenttype\\contenttype";
+            if (class_exists($contenttypeclass)) {
+                $context = \context::instance_by_id($record->contextid, MUST_EXIST);
+                self::validate_context($context);
+                $contenttype = new $contenttypeclass($context);
+                $contentclass = "\\$record->contenttype\\content";
+                $content = new $contentclass($record);
+                // Check capability.
+                if ($contenttype->can_manage($content)) {
+                    // This content can be renamed.
+                    if ($contenttype->rename_content($content, $params['name'])) {
+                        $result = true;
+                    } else {
+                        $warnings[] = [
+                            'item' => $contentid,
+                            'warningcode' => 'contentnotrenamed',
+                            'message' => get_string('contentnotrenamed', 'core_contentbank')
+                        ];
+                    }
+                } else {
+                    // The user has no permission to manage this content.
+                    $warnings[] = [
+                        'item' => $contentid,
+                        'warningcode' => 'nopermissiontomanage',
+                        'message' => get_string('nopermissiontomanage', 'core_contentbank')
+                    ];
+                }
+            }
+        } catch (\moodle_exception $e) {
+            // The content or the context don't exist.
+            $warnings[] = [
+                'item' => $contentid,
+                'warningcode' => 'exception',
+                'message' => $e->getMessage()
+            ];
+        }
+
+        return [
+            'result' => $result,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * rename_content return.
+     *
+     * @since  Moodle 3.9
+     * @return external_single_structure
+     */
+    public static function execute_returns(): external_single_structure {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'The processing result'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+}
index 3869f3d..d654ab3 100644 (file)
@@ -26,9 +26,14 @@ namespace core_contentbank\privacy;
 
 use core_privacy\local\metadata\collection;
 use core_privacy\local\request\approved_contextlist;
-use core_privacy\local\request\approved_userlist;
 use core_privacy\local\request\contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
 use core_privacy\local\request\userlist;
+use core_privacy\local\request\approved_userlist;
+use context_system;
+use context_coursecat;
+use context_course;
 
 /**
  * Privacy provider implementation for core_contentbank.
@@ -42,88 +47,236 @@ class provider implements
     \core_privacy\local\request\plugin\provider {
 
     /**
-     * Returns metadata.
-     * TODO: MDL-67798.
+     * Returns meta data about this system.
      *
      * @param collection $collection The initialised collection to add items to.
      * @return collection A listing of user data stored through this system.
      */
-    public static function get_metadata(collection $collection) : collection {
-        // We are not implementing a proper privacy provider for now.
-        // A right privacy provider will be implemented in MDL-67798.
-
+    public static function get_metadata(collection $collection): collection {
         $collection->add_database_table('contentbank_content', [
+            'name' => 'privacy:metadata:content:name',
+            'contenttype' => 'privacy:metadata:content:contenttype',
             'usercreated' => 'privacy:metadata:content:usercreated',
             'usermodified' => 'privacy:metadata:content:usermodified',
-        ], 'privacy:metadata:userid');
+            'timecreated' => 'privacy:metadata:content:timecreated',
+            'timemodified' => 'privacy:metadata:content:timemodified',
+        ], 'privacy:metadata:contentbankcontent');
 
         return $collection;
     }
 
     /**
-     * TODO: MDL-67798.
+     * Get the list of contexts that contain user information for the specified user.
      *
-     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
+     * @param   int $userid The user to search.
+     * @return  contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
      */
-    public static function get_users_in_context(userlist $userlist) {
-        // We are not implementing a proper privacy provider for now.
-        // A right privacy provider will be implemented in MDL-67798.
-    }
+    public static function get_contexts_for_userid(int $userid): contextlist {
+        $sql = "SELECT DISTINCT ctx.id
+                  FROM {context} ctx
+                  JOIN {contentbank_content} cb
+                       ON cb.contextid = ctx.id
+                 WHERE cb.usercreated = :userid
+                       AND (ctx.contextlevel = :contextlevel1
+                           OR ctx.contextlevel = :contextlevel2
+                           OR ctx.contextlevel = :contextlevel3)";
 
-    /**
-     * TODO: MDL-67798.
-     *
-     * @param   approved_userlist       $userlist The approved context and user information to delete information for.
-     */
-    public static function delete_data_for_users(approved_userlist $userlist) {
-        // We are not implementing a proper privacy provider for now.
-        // A right privacy provider will be implemented in MDL-67798.
+        $params = [
+            'userid'        => $userid,
+            'contextlevel1' => CONTEXT_SYSTEM,
+            'contextlevel2' => CONTEXT_COURSECAT,
+            'contextlevel3' => CONTEXT_COURSE,
+        ];
+
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
     }
 
     /**
-     * TODO: MDL-67798.
-     * Get the list of contexts that contain user information for the specified user.
+     * Get the list of users within a specific context.
      *
-     * @param   int         $userid     The user to search.
-     * @return  contextlist   $contextlist  The contextlist containing the list of contexts used in this plugin.
+     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
      */
-    public static function get_contexts_for_userid(int $userid) : contextlist {
-        // We are not implementing a proper privacy provider for now.
-        // A right privacy provider will be implemented in MDL-67798.
+    public static function get_users_in_context(userlist $userlist) {
+        $context = $userlist->get_context();
+
+        $allowedcontextlevels = [
+            CONTEXT_SYSTEM,
+            CONTEXT_COURSECAT,
+            CONTEXT_COURSE,
+        ];
+
+        if (!in_array($context->contextlevel, $allowedcontextlevels)) {
+            return;
+        }
+
+        $sql = "SELECT cb.usercreated as userid
+                  FROM {contentbank_content} cb
+                 WHERE cb.contextid = :contextid";
 
-        return (new contextlist());
+        $params = [
+            'contextid' => $context->id
+        ];
+
+        $userlist->add_from_sql('userid', $sql, $params);
     }
 
     /**
-     * TODO: MDL-67798.
      * Export all user data for the specified user, in the specified contexts.
      *
-     * @param   approved_contextlist    $contextlist    The approved contexts to export information for.
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
      */
     public static function export_user_data(approved_contextlist $contextlist) {
-        // We are not implementing a proper privacy provider for now.
-        // A right privacy provider will be implemented in MDL-67798.
+        global $DB;
+
+        // Remove contexts different from SYSTEM, COURSECAT or COURSE.
+        $contextids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT
+                || $context->contextlevel == CONTEXT_COURSE) {
+                $carry[] = $context->id;
+            }
+            return $carry;
+        }, []);
+
+        if (empty($contextids)) {
+            return;
+        }
+
+        $userid = $contextlist->get_user()->id;
+
+        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
+        // Retrieve the contentbank_content records created for the user.
+        $sql = "SELECT cb.id,
+                       cb.name,
+                       cb.contenttype,
+                       cb.usercreated,
+                       cb.usermodified,
+                       cb.timecreated,
+                       cb.timemodified,
+                       cb.contextid
+                  FROM {contentbank_content} cb
+                 WHERE cb.usercreated = :userid
+                       AND cb.contextid {$contextsql}
+                 ORDER BY cb.contextid";
+
+        $params = ['userid' => $userid] + $contextparams;
+
+        $contents = $DB->get_recordset_sql($sql, $params);
+        $data = [];
+        $lastcontextid = null;
+        $subcontext = [
+            get_string('name', 'core_contentbank'),
+        ];
+        foreach ($contents as $content) {
+            // The core_contentbank data export is organised in:
+            // {Sytem|Course Category|Course Context Level}/Content/data.json.
+            if ($lastcontextid && $lastcontextid != $content->contextid) {
+                $context = \context::instance_by_id($lastcontextid);
+                writer::with_context($context)->export_data($subcontext, (object)$data);
+                $data = [];
+            }
+            $data[] = (object) [
+                'name' => $content->name,
+                'contenttype' => $content->contenttype,
+                'usercreated' => transform::user($content->usercreated),
+                'usermodified' => transform::user($content->usermodified),
+                'timecreated' => transform::datetime($content->timecreated),
+                'timemodified' => transform::datetime($content->timemodified)
+            ];
+            $lastcontextid = $content->contextid;
+
+            // The core_contentbank files export is organised in:
+            // {Sytem|Course Category|Course Context Level}/Content/_files/public/_itemid/filename.
+            $context = \context::instance_by_id($lastcontextid);
+            writer::with_context($context)->export_area_files($subcontext, 'contentbank', 'public', $content->id);
+        }
+        if (!empty($data)) {
+            $context = \context::instance_by_id($lastcontextid);
+            writer::with_context($context)->export_data($subcontext, (object)$data);
+        }
+        $contents->close();
     }
 
     /**
-     * TODO: MDL-67798.
      * Delete all data for all users in the specified context.
      *
-     * @param   context                 $context   The specific context to delete data for.
+     * @param   context $context The specific context to delete data for.
      */
     public static function delete_data_for_all_users_in_context(\context $context) {
-        // We are not implementing a proper privacy provider for now.
-        // A right privacy provider will be implemented in MDL-67798.
+        global $DB;
+
+        if (!$context instanceof context_system && !$context instanceof context_coursecat
+                && !$context instanceof context_course) {
+            return;
+        }
+
+        static::delete_data($context, []);
+    }
+
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param approved_userlist $userlist The approved context and user information to delete information for.
+     */
+    public static function delete_data_for_users(approved_userlist $userlist) {
+        $context = $userlist->get_context();
+
+        if (!$context instanceof context_system && !$context instanceof context_coursecat
+                && !$context instanceof context_course) {
+            return;
+        }
+
+        static::delete_data($context, $userlist->get_userids());
     }
 
     /**
-     * TODO: MDL-67798.
      * Delete all user data for the specified user, in the specified contexts.
      *
-     * @param   approved_contextlist    $contextlist    The approved contexts and user information to delete information for.
+     * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
      */
     public static function delete_data_for_user(approved_contextlist $contextlist) {
-        // We are not implementing a proper privacy provider for now.
-        // A right privacy provider will be implemented in MDL-67798.
+        if (empty($contextlist->count())) {
+            return;
+        }
+
+        $userid = $contextlist->get_user()->id;
+        foreach ($contextlist->get_contexts() as $context) {
+            if (!$context instanceof context_system && !$context instanceof context_coursecat
+            && !$context instanceof context_course) {
+                continue;
+            }
+            static::delete_data($context, [$userid]);
+        }
+    }
+
+    /**
+     * Delete data related to a context and users (if defined).
+     *
+     * @param context $context A context.
+     * @param array $userids The user IDs.
+     */
+    protected static function delete_data(\context $context, array $userids) {
+        global $DB;
+
+        $params = ['contextid' => $context->id];
+        $select = 'contextid = :contextid';
+
+        // Delete the Content Bank files.
+        if (!empty($userids)) {
+            list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
+            $params += $inparams;
+            $select .= ' AND usercreated '.$insql;
+        }
+        $fs = get_file_storage();
+        $contents = $DB->get_records_select('contentbank_content',
+            $select, $params);
+        foreach ($contents as $content) {
+            $fs->delete_area_files($content->contextid, 'contentbank', 'public', $content->id);
+        }
+
+        // Delete all the contents.
+        $DB->delete_records_select('contentbank_content', $select, $params);
     }
 }
index 10ea721..179e80f 100644 (file)
@@ -25,5 +25,5 @@
 $string['pluginname'] = 'H5P';
 $string['pluginname_help'] = 'Content bank to upload and share H5P content';
 $string['privacy:metadata'] = 'The H5P content bank plugin does not store any personal data.';
-$string['h5p:access'] = 'Access to H5P content in the content bank';
-$string['h5p:upload'] = 'Upload new H5P content';
+$string['h5p:access'] = 'Access H5P content in the content bank';
+$string['h5p:upload'] = 'Upload new H5P content';
index 8d03118..61f28ea 100644 (file)
@@ -48,7 +48,7 @@ Feature: H5P file upload to content bank for admins
     And I click on "Save changes" "button"
     And I wait until the page is ready
     And I should see "filltheblanks.h5p"
-    And I navigate to "Plugins > Content bank > Manage content bank content types" in site administration
+    And I navigate to "Plugins > Content bank > Manage content types" in site administration
     And I click on "Disable" "icon" in the "H5P" "table_row"
     And I wait until the page is ready
     When I click on "Content bank" "link"
diff --git a/contentbank/contenttype/h5p/tests/behat/manage_content.feature b/contentbank/contenttype/h5p/tests/behat/manage_content.feature
new file mode 100644 (file)
index 0000000..5705a9e
--- /dev/null
@@ -0,0 +1,47 @@
+@core @core_contentbank @contenttype_h5p @_file_upload @javascript
+Feature: Manage H5P content from the content bank
+  In order to manage H5P content in the content bank
+  As an admin
+  I need to be able to edit any H5P content in the content bank
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And the following "contentbank content" exist:
+      | course| contenttype     | user     | contentname       |
+      | C1    | contenttype_h5p | admin    | filltheblanks.h5p |
+      | C1    | contenttype_h5p | teacher1 | ipsums.h5p        |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Navigation" block if not present
+    And I log out
+
+  Scenario: Teachers can rename their own content in the content bank
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    And I follow "ipsums.h5p"
+    When I open the action menu in "region-main-settings-menu" "region"
+    And I should see "Rename"
+    And I choose "Rename" in the open action menu
+    And I set the field "Content name" to "New name"
+    And I click on "Rename" "button"
+    And I wait until the page is ready
+    Then I should not see "ipsums.h5p"
+    And I should see "New name"
+
+  Scenario: Teachers can't rename content created by other users in the content bank
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    When I follow "filltheblanks.h5p"
+    Then "region-main-settings-menu" "region" should not exist
index a4082f8..657a459 100644 (file)
@@ -51,14 +51,14 @@ class contenttype_h5p_content_plugin_testcase extends advanced_testcase {
 
         // Create a dummy file.
         $filename = 'content.h5p';
-        $dummy = array(
+        $dummy = [
             'contextid' => \context_system::instance()->id,
             'component' => 'contentbank',
             'filearea' => 'public',
             'itemid' => $content->get_id(),
             'filepath' => '/',
             'filename' => $filename
-        );
+        ];
         $fs = get_file_storage();
         $fs->create_file_from_string($dummy, 'dummy content');
 
diff --git a/contentbank/templates/renamecontent.mustache b/contentbank/templates/renamecontent.mustache
new file mode 100644 (file)
index 0000000..ea638d5
--- /dev/null
@@ -0,0 +1,30 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_contentbank/renamecontent
+
+    Example context (json):
+    {
+        "contentid": 17,
+        "name": "Accordion"
+    }
+
+}}
+    <div class="form-check w-100 justify-content-start">
+        <label for="newname">{{#str}}contentname, core_contentbank{{/str}}</label>
+        <input type="text" size="5" id="newname" name="newname"  value="{{{ name }}}" class="form-control text-ltr">
+    </div>
index 7280b1b..b49a79c 100644 (file)
@@ -31,7 +31,7 @@ Feature: Delete H5P file from the content bank
     When I open the action menu in "region-main-settings-menu" "region"
     Then I should see "Delete"
     And I choose "Delete" in the open action menu
-    And I should see "Are you sure you want to delete content 'filltheblanks.h5p'?"
+    And I should see "Are you sure you want to delete the content 'filltheblanks.h5p'"
     And I click on "Cancel" "button" in the "Delete content" "dialogue"
     And I should see "filltheblanks.h5p"
     And I open the action menu in "region-main-settings-menu" "region"
index 04b722f..2fcd66c 100644 (file)
@@ -64,6 +64,55 @@ class core_contenttype_content_testcase extends \advanced_testcase {
         $this->assertEquals($record->name, $content->get_name());
     }
 
+    /**
+     * Data provider for test_set_name.
+     *
+     * @return  array
+     */
+    public function set_name_provider() {
+        return [
+            'Standard name' => ['New name', 'New name'],
+            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'],
+            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
+            'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
+            'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
+            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+        ];
+    }
+
+    /**
+     * Tests for 'set_name' behaviour.
+     *
+     * @dataProvider    set_name_provider
+     * @param   string  $newname    The name to set
+     * @param   string   $expected   The name result
+     *
+     * @covers ::set_name
+     */
+    public function test_set_name(string $newname, string $expected) {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $oldname = "Old name";
+        $context = context_system::instance();
+
+        // Create content.
+        $record = new stdClass();
+        $record->name = $oldname;
+
+        $contenttype = new contenttype($context);
+        $content = $contenttype->create_content($record);
+        $this->assertEquals($oldname, $content->get_name());
+
+        $content->set_name($newname);
+        $this->assertEquals($expected, $content->get_name());
+
+        $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
+        $this->assertEquals($expected, $record->name);
+    }
+
     /**
      * Tests for behaviour of get_content_type().
      *
index 7175b63..e74bd1b 100644 (file)
@@ -176,7 +176,6 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
         $this->assertInstanceOf('\\contenttype_testable\\content', $content);
     }
 
-
     /**
      * Test the behaviour of can_delete().
      */
@@ -254,4 +253,105 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
 
         $this->contenttype = new \contenttype_testable\contenttype($systemcontext);
     }
+
+    /**
+     * Data provider for test_rename_content.
+     *
+     * @return  array
+     */
+    public function rename_content_provider() {
+        return [
+            'Standard name' => ['New name', 'New name'],
+            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'],
+            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
+            'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
+            'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
+            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+        ];
+    }
+
+    /**
+     * Test the behaviour of rename_content().
+     *
+     * @dataProvider    rename_content_provider
+     * @param   string  $newname    The name to set
+     * @param   string   $expected   The name result
+     *
+     * @covers ::rename_content
+     */
+    public function test_rename_content(string $newname, string $expected) {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create course and teacher user.
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $coursecontext = \context_course::instance($course->id);
+        $contenttype = new contenttype($coursecontext);
+
+        // Add some content to the content bank as teacher.
+        $this->setUser($teacher);
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+        $contents = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id);
+        $content = array_shift($contents);
+
+        $oldname = $content->get_name();
+
+        // Check the content is renamed as expected by a user with permission.
+        $renamed = $contenttype->rename_content($content, $newname);
+        $this->assertTrue($renamed);
+        $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
+        $this->assertNotEquals($oldname, $record->name);
+        $this->assertEquals($expected, $record->name);
+    }
+
+    /**
+     * Test the behaviour of can_manage().
+     *
+     * @covers ::can_manage
+     */
+    public function test_can_manage() {
+        global $DB, $USER;
+
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+
+        // Create course and teacher user.
+        $teacherroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']);
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $manager = $this->getDataGenerator()->create_and_enrol($course, 'manager');
+        $coursecontext = \context_course::instance($course->id);
+
+        $contenttype = new contenttype($coursecontext);
+
+        // Add some content to the content bank as admin.
+        $this->setAdminUser();
+        $contentsbyadmin = $generator->generate_contentbank_data('contenttype_testable', 1, $USER->id, $coursecontext);
+        $contentbyadmin = array_shift($contentsbyadmin);
+
+        // Add some content to the content bank as teacher.
+        $contentsbyteacher = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id, $coursecontext);
+        $contentbyteacher = array_shift($contentsbyteacher);
+
+        // Check the content has been created as expected.
+        $records = $DB->count_records('contentbank_content');
+        $this->assertEquals(2, $records);
+
+        // Check manager can manage by default all the contents created.
+        $this->setUser($manager);
+        $this->assertTrue($contenttype->can_manage($contentbyteacher));
+        $this->assertTrue($contenttype->can_manage($contentbyadmin));
+
+        // Check teacher can only edit their own content.
+        $this->setUser($teacher);
+        $this->assertTrue($contenttype->can_manage($contentbyteacher));
+        $this->assertFalse($contenttype->can_manage($contentbyadmin));
+
+        // Unassign capability to teacher role and check they not can not edit any content.
+        unassign_capability('moodle/contentbank:manageowncontent', $teacherroleid);
+        $this->assertFalse($contenttype->can_manage($contentbyteacher));
+        $this->assertFalse($contenttype->can_manage($contentbyadmin));
+    }
 }
diff --git a/contentbank/tests/external/rename_content_test.php b/contentbank/tests/external/rename_content_test.php
new file mode 100644 (file)
index 0000000..4b1bf6b
--- /dev/null
@@ -0,0 +1,140 @@
+<?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/>.
+
+/**
+ * Core content bank external functions tests.
+ *
+ * @package    core_contentbank
+ * @category   external
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.9
+ */
+
+namespace core_contentbank;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+use core_contentbank\external\delete_content;
+use core_contentbank\external\external;
+use core_contentbank\external\rename_content;
+use external_api;
+
+/**
+ * Core content bank external functions tests.
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core_contentbank\external
+ */
+class rename_content_testcase extends \externallib_advanced_testcase {
+
+    /**
+     * Data provider for test_rename_content.
+     *
+     * @return  array
+     */
+    public function rename_content_provider() {
+        return [
+            'Standard name' => ['New name', 'New name'],
+            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'],
+            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
+            'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
+            'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
+            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+        ];
+    }
+
+    /**
+     * Test the behaviour of rename_content() for users with permission.
+     *
+     * @dataProvider    rename_content_provider
+     * @param   string  $newname    The name to set
+     * @param   string   $expected   The name result
+     *
+     * @covers ::execute
+     */
+    public function test_rename_content_with_permission(string $newname, string $expected) {
+        global $DB;
+        $this->resetAfterTest();
+
+        // Create users.
+        $roleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']);
+        $teacher = $this->getDataGenerator()->create_user();
+
+        $this->getDataGenerator()->role_assign($roleid, $teacher->id);
+        $this->setUser($teacher);
+
+        // Add some content to the content bank as teacher.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+        $contents = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id);
+        $content = array_shift($contents);
+
+        $oldname = $content->get_name();
+
+        // Call the WS and check the content is renamed as expected.
+        $result = rename_content::execute($content->get_id(), $newname);
+        $result = external_api::clean_returnvalue(rename_content::execute_returns(), $result);
+        $this->assertTrue($result['result']);
+        $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
+        $this->assertNotEquals($oldname, $record->name);
+        $this->assertEquals($expected, $record->name);
+
+        // Call the WS using an unexisting contentid and check an error is thrown.
+        $this->expectException(\invalid_response_exception::class);
+        $result = rename_content::execute_returns($content->get_id() + 1, $oldname);
+        $result = external_api::clean_returnvalue(rename_content::execute_returns(), $result);
+        $this->assertFalse($result['result']);
+    }
+
+    /**
+     * Test the behaviour of rename_content() for users with permission.
+     *
+     * @covers ::execute
+     */
+    public function test_rename_content_without_permission() {
+        global $DB;
+        $this->resetAfterTest();
+
+        // Create users.
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Add some content to the content bank as teacher.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+        $contents = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id);
+        $content = array_shift($contents);
+
+        $oldname = $content->get_name();
+        $newname = 'New name';
+
+        // Call the WS and check the content has not been renamed by the student.
+        $this->setUser($student);
+        $result = rename_content::execute($content->get_id(), $newname);
+        $result = external_api::clean_returnvalue(rename_content::execute_returns(), $result);
+        $this->assertFalse($result['result']);
+        $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
+        $this->assertEquals($oldname, $record->name);
+        $this->assertNotEquals($newname, $record->name);
+    }
+}
diff --git a/contentbank/tests/privacy_test.php b/contentbank/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..09c3f0f
--- /dev/null
@@ -0,0 +1,366 @@
+<?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/>.
+
+/**
+ * Base class for unit tests for core_contentbank.
+ *
+ * @package    core_contentbank
+ * @category   test
+ * @copyright  2020 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank;
+
+defined('MOODLE_INTERNAL') || die();
+
+use stdClass;
+use context_system;
+use context_coursecat;
+use context_course;
+use core_contentbank\privacy\provider;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\writer;
+use core_privacy\tests\provider_testcase;
+use core_privacy\local\request\userlist;
+use core_privacy\local\request\approved_userlist;
+
+/**
+ * Unit tests for contentbank\classes\privacy\provider.php
+ *
+ * @copyright  2020 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_contentbank_privacy_testcase extends provider_testcase {
+
+    /**
+     * Setup to ensure that fixtures are loaded.
+     */
+    public static function setupBeforeClass(): void {
+        global $CFG;
+        require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+    }
+
+    /**
+     * Test for provider::get_contexts_for_userid().
+     */
+    public function test_get_contexts_for_userid() {
+
+        $this->resetAfterTest();
+        // Setup scenario.
+        $scenario = $this->setup_scenario();
+
+        // Testing againts Manager who has content in the three contexts.
+        $contextlist = provider::get_contexts_for_userid($scenario->manager->id);
+        // There are three contexts in the list.
+        $contextlistids = $contextlist->get_contextids();
+        $this->assertCount(3, $contextlistids);
+        // Check the list against the expected list of contexts.
+        $this->assertContains($scenario->systemcontext->id, $contextlistids);
+        $this->assertContains($scenario->coursecategorycontext->id,
+            $contextlistids);
+        $this->assertContains($scenario->coursecontext->id, $contextlistids);
+
+        // Testing againts Teacher who has content in the one context.
+        $contextlist = provider::get_contexts_for_userid($scenario->teacher->id);
+        // There are only one context in the list.
+        $contextlistids = $contextlist->get_contextids();
+        $this->assertCount(1, $contextlistids);
+        // Check the againts Course Context.
+        $this->assertContains($scenario->coursecontext->id, $contextlistids);
+        // And there is not a System and Course Category Context.
+        $this->assertNotContains($scenario->systemcontext->id, $contextlistids);
+        $this->assertNotContains($scenario->coursecategorycontext->id, $contextlistids);
+    }
+
+    /**
+     * Test for provider::get_users_in_context().
+     */
+    public function test_get_users_in_context() {
+
+        $this->resetAfterTest();
+        // Setup scenario.
+        $scenario = $this->setup_scenario();
+
+        // Get the userlist to Context System, only Manager will be there.
+        $userlist = new userlist($scenario->systemcontext, 'core_contentbank');
+        provider::get_users_in_context($userlist);
+        $this->assertEquals([$scenario->manager->id], $userlist->get_userids());
+        // Teacher will not be there.
+        $this->assertNotEquals([$scenario->teacher->id], $userlist->get_userids());
+
+        // Get the userlist to Context Course, Manager and Teacher will be there.
+        $userlist = new userlist($scenario->coursecontext, 'core_contentbank');
+        provider::get_users_in_context($userlist);
+
+        $expected = [$scenario->manager->id, $scenario->teacher->id];
+        sort($expected);
+        $actual = $userlist->get_userids();
+        sort($actual);
+        $this->assertEquals($expected, $actual);
+    }
+
+    /**
+     * Test for provider::test_export_user_data().
+     */
+    public function test_export_user_data() {
+
+        $this->resetAfterTest();
+        // Setup scenario.
+        $scenario = $this->setup_scenario();
+
+        $subcontexts = [
+            get_string('name', 'core_contentbank')
+        ];
+        // Get the data for the System Context.
+        $writer = writer::with_context($scenario->systemcontext);
+        $this->assertFalse($writer->has_any_data());
+        // Export data for Manager.
+        $this->export_context_data_for_user($scenario->manager->id,
+            $scenario->systemcontext, 'core_contentbank');
+        $data = $writer->get_data($subcontexts);
+        $this->assertCount(3, (array) $data);
+        $this->assertCount(3, $writer->get_files($subcontexts));
+
+        // Get the data for the Course Categoy Context.
+        $writer = writer::with_context($scenario->coursecategorycontext);
+        // Export data for Manager.
+        $this->export_context_data_for_user($scenario->manager->id,
+            $scenario->coursecategorycontext, 'core_contentbank');
+        $data = $writer->get_data($subcontexts);
+        $this->assertCount(2, (array) $data);
+        $this->assertCount(2, $writer->get_files($subcontexts));
+
+        // Get the data for the Course Context.
+        $writer = writer::with_context($scenario->coursecontext);
+        // Export data for Manager.
+        $this->export_context_data_for_user($scenario->manager->id,
+            $scenario->coursecontext, 'core_contentbank');
+        $data = $writer->get_data($subcontexts);
+        $this->assertCount(2, (array) $data);
+        $this->assertCount(2, $writer->get_files($subcontexts));
+
+        // Export data for Teacher.
+        $writer = writer::reset();
+        $writer = writer::with_context($scenario->coursecontext);
+        $this->export_context_data_for_user($scenario->teacher->id,
+            $scenario->coursecontext, 'core_contentbank');
+        $data = $writer->get_data($subcontexts);
+        $this->assertCount(3, (array) $data);
+        $this->assertCount(3, $writer->get_files($subcontexts));
+    }
+
+    /**
+     * Test for provider::delete_data_for_all_users_in_context().
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+
+        $this->resetAfterTest();
+        // Setup scenario.
+        $scenario = $this->setup_scenario();
+
+        // Before delete data, we have 4 contents.
+        // - 3 in a system context.
+        // - 2 in a course category context.
+        // - 5 in a course context (2 by manager and 3 by teacher).
+
+        // Delete data based on system context.
+        provider::delete_data_for_all_users_in_context($scenario->systemcontext);
+        $count = $DB->count_records('contentbank_content');
+        // 3 content should be deleted.
+        // 7 contents should be remain.
+        $this->assertEquals(7, $count);
+
+        // Delete data based on course category context.
+        provider::delete_data_for_all_users_in_context($scenario->coursecategorycontext);
+        $count = $DB->count_records('contentbank_content');
+        // 2 contents should be deleted.
+        // 5 content should be remain.
+        $this->assertEquals(5, $count);
+
+        // Delete data based on course context.
+        provider::delete_data_for_all_users_in_context($scenario->coursecontext);
+         $count = $DB->count_records('contentbank_content');
+        // 5 content should be deleted.
+        // 0 content should be remain.
+        $this->assertEquals(0, $count);
+    }
+
+    /**
+     * Test for provider::test_delete_data_for_users().
+     */
+    public function test_delete_data_for_users() {
+        global $DB;
+
+        $this->resetAfterTest();
+        // Setup scenario.
+        $scenario = $this->setup_scenario();
+
+        // Before delete data, we have 4 contents.
+        // - 3 in a system context.
+        // - 2 in a course category context.
+        // - 5 in a course context (2 by manager and 3 by teacher).
+
+        // A list of users who has created content in Course Category Context.
+        $userlist1 = new userlist($scenario->coursecategorycontext,
+            'core_contentbank');
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(1, $userlist1);
+        // Only Manager should be.
+        $this->assertEquals([$scenario->manager->id], $userlist1->get_userids());
+
+        // A list of users who has created content in Course Context.
+        $userlist2 = new userlist($scenario->coursecontext, 'core_contentbank');
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(2, $userlist2);
+
+        // Manager and Teacher should be.
+        $expected = [$scenario->manager->id, $scenario->teacher->id];
+        sort($expected);
+        $actual = $userlist2->get_userids();
+        sort($actual);
+        $this->assertEquals($expected, $actual);
+
+        // Convert $userlist1 into an approved_contextlist.
+        $approvedlist1 = new approved_userlist($scenario->coursecategorycontext, 'core_contentbank', $userlist1->get_userids());
+        // Delete data for users in course category context.
+        provider::delete_data_for_users($approvedlist1);
+
+        // Re-fetch users in course category context.
+        $userlist1 = new userlist($scenario->coursecategorycontext,
+            'core_contentbank');
+        provider::get_users_in_context($userlist1);
+        // The user data in course category context should be deleted.
+        $this->assertCount(0, $userlist1);
+        // Re-fetch users in course category context.
+        $userlist2 = new userlist($scenario->coursecontext, 'core_contentbank');
+        provider::get_users_in_context($userlist2);
+        // The user data in course context should be still present.
+        $this->assertCount(2, $userlist2);
+
+        // Convert $userlist2 into an approved_contextlist.
+        $approvedlist2 = new approved_userlist($scenario->coursecontext,
+            'core_contentbank', $userlist2->get_userids());
+        // Delete data for users in course context.
+        provider::delete_data_for_users($approvedlist2);
+        $userlist2 = new userlist($scenario->coursecontext, 'core_contentbank');
+        provider::get_users_in_context($userlist2);
+        // The user data in course context should be deleted.
+        $this->assertCount(0, $userlist2);
+    }
+
+    /**
+     * Test for provider::delete_data_for_user().
+     */
+    public function test_delete_data_for_user() {
+         global $DB;
+
+        $this->resetAfterTest();
+        // Setup scenario.
+        $scenario = $this->setup_scenario();
+
+        // Before delete data, we have 4 contents.
+        // - 3 in a system context.
+        // - 2 in a course category context.
+        // - 5 in a course context (2 by manager and 3 by teacher).
+
+        // Get all the context for Manager.
+        $contextlist = provider::get_contexts_for_userid($scenario->manager->id);
+        $approvedcontextlist = new approved_contextlist($scenario->manager,
+            'core_contentbank', $contextlist->get_contextids());
+        // Delete all the data created by the Manager in all the contexts.
+        provider::delete_data_for_user($approvedcontextlist);
+
+        // After deletion, only 3 content for teacher should be present.
+        $count = $DB->count_records('contentbank_content');
+        $this->assertEquals(3, $count);
+
+        // Confirm that the remaining content was created by the teacher.
+        $count = $DB->count_records('contentbank_content',
+            ['usercreated' => $scenario->teacher->id]);
+        $this->assertEquals(3, $count);
+
+        // Get all the context for Teacher.
+        $contextlist = provider::get_contexts_for_userid($scenario->teacher->id);
+        $approvedcontextlist = new approved_contextlist($scenario->teacher,
+            'core_contentbank', $contextlist->get_contextids());
+        // Delete all the data created by the Teacher in all the contexts.
+        provider::delete_data_for_user($approvedcontextlist);
+
+        // After deletion, no content should be present.
+        $count = $DB->count_records('contentbank_content');
+        $this->assertEquals(0, $count);
+    }
+
+    /**
+     * Create a complex scenario to use into the tests.
+     *
+     * @return stdClass $scenario
+     */
+    protected function setup_scenario() {
+        global $DB;
+
+        $systemcontext = context_system::instance();
+        $manager = $this->getDataGenerator()->create_user();
+        $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
+        $this->getDataGenerator()->role_assign($managerroleid, $manager->id);
+
+        $coursecategory = $this->getDataGenerator()->create_category();
+        $coursecategorycontext = context_coursecat::instance($coursecategory->id);
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+        $teacher = $this->getDataGenerator()->create_and_enrol($course,
+            'editingteacher');
+
+        // Add some content to the content bank.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+        // Add contents by Manager in Context System.
+        $records = $generator->generate_contentbank_data('contenttype_testable',
+            1, $manager->id, $systemcontext, false, 'systemtestfile1.h5p');
+        $records = $generator->generate_contentbank_data('contenttype_testable',
+            1, $manager->id, $systemcontext, false, 'systemtestfile2.h5p');
+        $records = $generator->generate_contentbank_data('contenttype_testable',
+            1, $manager->id, $systemcontext, false, 'systemtestfile3.h5p');
+        // Add contents by Manager in Context Course Category.
+        $records = $generator->generate_contentbank_data('contenttype_testable',
+            1, $manager->id, $coursecategorycontext, false, 'coursecattestfile1.h5p');
+        $records = $generator->generate_contentbank_data('contenttype_testable',
+            1, $manager->id, $coursecategorycontext, false, 'coursecattestfile2.h5p');
+        // Add contents by Manager in Context Course.
+        $records = $generator->generate_contentbank_data('contenttype_testable',
+            1, $manager->id, $coursecontext, false, 'coursetestfile1.h5p');
+        $records = $generator->generate_contentbank_data('contenttype_testable',
+            1, $manager->id, $coursecontext, false, 'coursetestfile2.h5p');
+        // Add contents by Teacher.
+        $records = $generator->generate_contentbank_data('contenttype_testable',
+            1, $teacher->id, $coursecontext, false, 'courseteacherfile1.h5p');
+        $records = $generator->generate_contentbank_data('contenttype_testable',
+            1, $teacher->id, $coursecontext, false, 'courseteacherfile2.h5p');
+        $records = $generator->generate_contentbank_data('contenttype_testable',
+            1, $teacher->id, $coursecontext, false, 'courseteacherfile3.h5p');
+
+        $scenario = new stdClass();
+        $scenario->systemcontext = $systemcontext;
+        $scenario->coursecategorycontext = $coursecategorycontext;
+        $scenario->coursecontext = $coursecontext;
+        $scenario->manager = $manager;
+        $scenario->teacher = $teacher;
+
+        return $scenario;
+    }
+}
index 4c4a4c0..d34205c 100644 (file)
@@ -35,7 +35,10 @@ $record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST)
 $context = context::instance_by_id($record->contextid, MUST_EXIST);
 require_capability('moodle/contentbank:access', $context);
 
-$returnurl = new \moodle_url('/contentbank/index.php');
+$statusmsg = optional_param('statusmsg', '', PARAM_RAW);
+$errormsg = optional_param('errormsg', '', PARAM_RAW);
+
+$returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]);
 $plugin = core_plugin_manager::instance()->get_plugin_info($record->contenttype);
 if (!$plugin || !$plugin->is_enabled()) {
     print_error('unsupported', 'core_contentbank', $returnurl);
@@ -46,7 +49,6 @@ $title = get_string('contentbank');
 if ($PAGE->course) {
     require_login($PAGE->course->id);
 }
-$returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]);
 
 $PAGE->set_url(new \moodle_url('/contentbank/view.php', ['id' => $id]));
 $PAGE->set_context($context);
@@ -57,13 +59,32 @@ $PAGE->set_title($title);
 $PAGE->set_pagetype('contenbank');
 
 $contenttypeclass = "\\$record->contenttype\\contenttype";
-$contenttype = new $contenttypeclass($context);
 $contentclass = "\\$record->contenttype\\content";
+if (!class_exists($contenttypeclass) || !class_exists($contentclass)) {
+    print_error('contenttypenotfound', 'error', $returnurl, $record->contenttype);
+}
+$contenttype = new $contenttypeclass($context);
 $content = new $contentclass($record);
+
+// Create the cog menu with all the secondary actions, such as delete, rename...
+$actionmenu = new action_menu();
+$actionmenu->set_alignment(action_menu::TR, action_menu::BR);
+if ($contenttype->can_manage($content)) {
+    // Add the rename content item to the menu.
+    $attributes = [
+        'data-action' => 'renamecontent',
+        'data-contentname' => $content->get_name(),
+        'data-contentid' => $content->get_id(),
+    ];
+    $actionmenu->add_secondary_action(new action_menu_link(
+        new moodle_url('#'),
+        new pix_icon('e/styleparagraph', get_string('rename')),
+        get_string('rename'),
+        false,
+        $attributes
+    ));
+}
 if ($contenttype->can_delete($content)) {
-    // Create the cog menu with all the secondary actions, such as delete, rename...
-    $actionmenu = new action_menu();
-    $actionmenu->set_alignment(action_menu::TR, action_menu::BR);
     // Add the delete content item to the menu.
     $attributes = [
                 'data-action' => 'deletecontent',
@@ -78,18 +99,24 @@ if ($contenttype->can_delete($content)) {
         false,
         $attributes
     ));
-
-    // Add the cog menu to the header.
-    $PAGE->add_header_action(html_writer::div(
-        $OUTPUT->render($actionmenu),
-        'd-print-none',
-        ['id' => 'region-main-settings-menu']
-    ));
 }
 
+// Add the cog menu to the header.
+$PAGE->add_header_action(html_writer::div(
+    $OUTPUT->render($actionmenu),
+    'd-print-none',
+    ['id' => 'region-main-settings-menu']
+));
+
 echo $OUTPUT->header();
 echo $OUTPUT->box_start('generalbox');
 
+// If needed, display notifications.
+if ($errormsg !== '') {
+    echo $OUTPUT->notification($errormsg);
+} else if ($statusmsg !== '') {
+    echo $OUTPUT->notification($statusmsg, 'notifysuccess');
+}
 if ($contenttype->can_access()) {
     echo $contenttype->get_view_content($record);
 }
index bece6a1..07394ca 100644 (file)
@@ -208,17 +208,18 @@ class core_course_management_renderer extends plugin_renderer_base {
         }
         $courseicon = $this->output->pix_icon('i/course', get_string('courses'));
         $bcatinput = array(
+                'id' => 'categorylistitem' . $category->id,
                 'type' => 'checkbox',
                 'name' => 'bcat[]',
                 'value' => $category->id,
-                'class' => 'bulk-action-checkbox',
-                'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+                'class' => 'bulk-action-checkbox custom-control-input',
                 'data-action' => 'select'
         );
 
+        $checkboxclass = '';
         if (!$category->can_resort_subcategories() && !$category->has_manage_capability()) {
             // Very very hardcoded here.
-            $bcatinput['style'] = 'visibility:hidden';
+            $checkboxclass = 'd-none';
         }
 
         $viewcaturl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
@@ -260,8 +261,14 @@ class core_course_management_renderer extends plugin_renderer_base {
 
         $html = html_writer::start_tag('li', $attributes);
         $html .= html_writer::start_div('clearfix');
-        $html .= html_writer::start_div('float-left ba-checkbox');
-        $html .= html_writer::empty_tag('input', $bcatinput).'&nbsp;';
+        $html .= html_writer::start_div('float-left ' . $checkboxclass);
+        $html .= html_writer::start_div('custom-control custom-checkbox mr-1 ');
+        $html .= html_writer::empty_tag('input', $bcatinput);
+        $html .= html_writer::tag('label', '', array(
+            'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+            'class' => 'custom-control-label',
+            'for' => 'categorylistitem' . $category->id));
+        $html .= html_writer::end_div();
         $html .= html_writer::end_div();
         $html .= $icon;
         if ($hasactions) {
@@ -275,7 +282,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         $html .= html_writer::link($viewcaturl, $text, $textattributes);
         $html .= html_writer::start_div('float-right d-flex');
         if ($category->idnumber) {
-            $html .= html_writer::tag('span', s($category->idnumber), array('class' => 'dimmed idnumber'));
+            $html .= html_writer::tag('span', s($category->idnumber), array('class' => 'text-muted idnumber'));
         }
         if ($hasactions) {
             $html .= $this->category_listitem_actions($category, $actions);
@@ -285,7 +292,7 @@ class core_course_management_renderer extends plugin_renderer_base {
                 html_writer::span($category->get_courses_count()) .
                 html_writer::span(get_string('courses'), 'accesshide', array('id' => $countid)) .
                 $courseicon,
-                'course-count dimmed',
+                'course-count text-muted',
                 array('aria-labelledby' => $countid)
         );
         $html .= html_writer::end_div();
@@ -572,7 +579,7 @@ class core_course_management_renderer extends plugin_renderer_base {
                 $a->total = $totalcourses;
                 $str = get_string('showingxofycourses', 'moodle', $a);
             }
-            $html .= html_writer::div($str, 'listing-pagination-totals dimmed');
+            $html .= html_writer::div($str, 'listing-pagination-totals text-muted');
         }
 
         if ($viewmode !== 'default') {
@@ -607,16 +614,18 @@ class core_course_management_renderer extends plugin_renderer_base {
         );
 
         $bulkcourseinput = array(
+                'id' => 'courselistitem' . $course->id,
                 'type' => 'checkbox',
                 'name' => 'bc[]',
                 'value' => $course->id,
-                'class' => 'bulk-action-checkbox',
-                'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+                'class' => 'bulk-action-checkbox custom-control-input',
                 'data-action' => 'select'
         );
+
+        $checkboxclass = '';
         if (!$category->has_manage_capability()) {
             // Very very hardcoded here.
-            $bulkcourseinput['style'] = 'visibility:hidden';
+            $checkboxclass = 'd-none';
         }
 
         $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
@@ -629,13 +638,19 @@ class core_course_management_renderer extends plugin_renderer_base {
             $html .= html_writer::div($this->output->pix_icon('i/move_2d', get_string('dndcourse')), 'float-left drag-handle');
         }
 
-        $html .= html_writer::start_div('ba-checkbox float-left');
-        $html .= html_writer::empty_tag('input', $bulkcourseinput).'&nbsp;';
+        $html .= html_writer::start_div('float-left ' . $checkboxclass);
+        $html .= html_writer::start_div('custom-control custom-checkbox mr-1 ');
+        $html .= html_writer::empty_tag('input', $bulkcourseinput);
+        $html .= html_writer::tag('label', '', array(
+            'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+            'class' => 'custom-control-label',
+            'for' => 'courselistitem' . $course->id));
+        $html .= html_writer::end_div();
         $html .= html_writer::end_div();
         $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
         $html .= html_writer::start_div('float-right');
         if ($course->idnumber) {
-            $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'dimmed idnumber'));
+            $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'text-muted idnumber'));
         }
         $html .= $this->course_listitem_actions($category, $course);
         $html .= html_writer::end_div();
@@ -1121,7 +1136,7 @@ class core_course_management_renderer extends plugin_renderer_base {
                 $a->total = $totalcourses;
                 $str = get_string('showingxofycourses', 'moodle', $a);
             }
-            $html .= html_writer::div($str, 'listing-pagination-totals dimmed');
+            $html .= html_writer::div($str, 'listing-pagination-totals text-muted');
         }
 
         if ($totalcourses < $perpage) {
@@ -1184,10 +1199,10 @@ class core_course_management_renderer extends plugin_renderer_base {
         if (core_course_category::get($course->category)->can_move_courses_out_of()) {
             $bulkcourseinput = array(
                     'type' => 'checkbox',
+                    'id' => 'coursesearchlistitem' . $course->id,
                     'name' => 'bc[]',
                     'value' => $course->id,
-                    'class' => 'bulk-action-checkbox',
-                    'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+                    'class' => 'bulk-action-checkbox custom-control-input',
                     'data-action' => 'select'
             );
         }
@@ -1198,14 +1213,20 @@ class core_course_management_renderer extends plugin_renderer_base {
         $html .= html_writer::start_div('clearfix');
         $html .= html_writer::start_div('float-left');
         if ($bulkcourseinput) {
-            $html .= html_writer::empty_tag('input', $bulkcourseinput).'&nbsp;';
+            $html .= html_writer::start_div('custom-control custom-checkbox mr-1');
+            $html .= html_writer::empty_tag('input', $bulkcourseinput);
+            $html .= html_writer::tag('label', '', array(
+                'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+                'class' => 'custom-control-label',
+                'for' => 'coursesearchlistitem' . $course->id));
+            $html .= html_writer::end_div();
         }
         $html .= html_writer::end_div();
         $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
-        $html .= html_writer::tag('span', $categoryname, array('class' => 'float-left categoryname'));
+        $html .= html_writer::tag('span', $categoryname, array('class' => 'float-left ml-3 text-muted'));
         $html .= html_writer::start_div('float-right');
         $html .= $this->search_listitem_actions($course);
-        $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'dimmed idnumber'));
+        $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'text-muted idnumber'));
         $html .= html_writer::end_div();
         $html .= html_writer::end_div();
         $html .= html_writer::end_tag('li');
index 784d6ee..6f1a956 100644 (file)
@@ -903,7 +903,7 @@ class core_course_renderer extends plugin_renderer_base {
             $output .= course_get_cm_move($mod, $sectionreturn);
         }
 
-        $output .= html_writer::start_tag('div', array('class' => 'mod-indent-outer'));
+        $output .= html_writer::start_tag('div', array('class' => 'mod-indent-outer w-100'));
 
         // This div is used to indent the content.
         $output .= html_writer::div('', $indentclasses);
index 14f20e1..98ef06a 100644 (file)
@@ -18,13 +18,13 @@ Feature: Display and choose from the available activities in course
     And I am on "Course" course homepage with editing mode on
 
   Scenario: The available activities are displayed to the teacher in the activity chooser
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
-    Then I should see "Add an activity or resource" in the ".modal-title" "css_element"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
+    Then I should see "Add an activity" in the ".modal-title" "css_element"
     And I should see "Assignment" in the ".modal-body" "css_element"
 
   Scenario: The teacher can choose to add an activity from the activity items in the activity chooser
-    Given I click on "Add an activity or resource" "button" in the "Topic 3" "section"
-    When I click on "Add a new Assignment" "link" in the "Add an activity or resource" "dialogue"
+    Given I click on "Add an activity" "button" in the "Topic 3" "section"
+    When I click on "Add a new Assignment" "link" in the "Add an activity" "dialogue"
     Then I should see "Adding a new Assignment"
     And I set the following fields to these values:
       | Assignment name | Test Assignment Topic 3 |
@@ -32,19 +32,19 @@ Feature: Display and choose from the available activities in course
     Then I should see "Test Assignment Topic 3" in the "Topic 3" "section"
 
   Scenario: The teacher can choose to add an activity from the activity summary in the activity chooser
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
-    When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
+    When I click on "Information about the Assignment activity" "button" in the "Add an activity" "dialogue"
     When I click on "Add a new Assignment" "link" in the "help" "core_course > Activity chooser screen"
     Then I should see "Adding a new Assignment"
 
   Scenario: Show summary
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
-    When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
+    When I click on "Information about the Assignment activity" "button" in the "Add an activity" "dialogue"
     Then I should see "Assignment" in the "help" "core_course > Activity chooser screen"
     And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback."
 
   Scenario: Hide summary
-    Given I click on "Add an activity or resource" "button" in the &quo