Merge branch 'MDL-68800-lti-gbs-patch-fixes' of https://github.com/cengage/moodle
authorJake Dallimore <jake@moodle.com>
Thu, 4 Jun 2020 06:55:58 +0000 (14:55 +0800)
committerJake Dallimore <jake@moodle.com>
Thu, 4 Jun 2020 06:55:58 +0000 (14:55 +0800)
614 files changed:
.eslintignore
.stylelintignore
admin/index.php
admin/renderer.php
admin/settings/license.php [new file with mode: 0644]
admin/settings/plugins.php
admin/settings/top.php
admin/settings/userfeedback.php [new file with mode: 0644]
admin/templates/setting.mustache
admin/tool/behat/lang/en/tool_behat.php
admin/tool/dataprivacy/tests/behat/manage_categories.feature
admin/tool/dataprivacy/tests/behat/manage_purposes.feature
admin/tool/licensemanager/amd/build/delete_license.min.js [new file with mode: 0644]
admin/tool/licensemanager/amd/build/delete_license.min.js.map [new file with mode: 0644]
admin/tool/licensemanager/amd/src/delete_license.js [new file with mode: 0644]
admin/tool/licensemanager/classes/form/edit_license.php [new file with mode: 0644]
admin/tool/licensemanager/classes/helper.php [new file with mode: 0644]
admin/tool/licensemanager/classes/manager.php [new file with mode: 0644]
admin/tool/licensemanager/classes/output/renderer.php [new file with mode: 0644]
admin/tool/licensemanager/classes/output/table.php [new file with mode: 0644]
admin/tool/licensemanager/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/licensemanager/index.php [moved from admin/licenses.php with 55% similarity]
admin/tool/licensemanager/lang/en/tool_licensemanager.php [new file with mode: 0644]
admin/tool/licensemanager/settings.php [new file with mode: 0644]
admin/tool/licensemanager/tests/behat/delete_license.feature [new file with mode: 0644]
admin/tool/licensemanager/tests/behat/edit_license.feature [new file with mode: 0644]
admin/tool/licensemanager/tests/behat/license_manager.feature [new file with mode: 0644]
admin/tool/licensemanager/tests/helper_test.php [new file with mode: 0644]
admin/tool/licensemanager/tests/manager_test.php [new file with mode: 0644]
admin/tool/licensemanager/version.php [new file with mode: 0644]
admin/tool/usertours/amd/build/usertours.min.js
admin/tool/usertours/amd/build/usertours.min.js.map
admin/tool/usertours/amd/src/usertours.js
admin/tool/usertours/classes/external/tour.php
admin/tool/usertours/lang/en/tool_usertours.php
admin/tool/usertours/upgrade.txt
analytics/tests/behat/manage_models.feature
auth/email/tests/external_test.php
availability/condition/grade/tests/behat/availability_grade.feature
backup/moodle2/restore_qtype_plugin.class.php
backup/tests/quiz_restore_decode_links_test.php [new file with mode: 0644]
badges/amd/build/backpackactions.min.js [new file with mode: 0644]
badges/amd/build/backpackactions.min.js.map [new file with mode: 0644]
badges/amd/build/selectors.min.js [new file with mode: 0644]
badges/amd/build/selectors.min.js.map [new file with mode: 0644]
badges/amd/src/backpackactions.js [new file with mode: 0644]
badges/amd/src/selectors.js [new file with mode: 0644]
badges/backpacks.php
badges/classes/form/badge.php
badges/classes/form/external_backpack.php
badges/classes/helper.php [new file with mode: 0644]
badges/classes/output/external_backpacks_page.php
badges/newbadge.php
badges/templates/external_backpacks_page.mustache
badges/tests/badgeslib_test.php
badges/tests/behat/add_badge.feature
badges/tests/behat/backpack.feature
badges/tests/privacy_test.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lib.php
blocks/myoverview/templates/nav-sort-selector.mustache
blocks/myoverview/templates/view-list.mustache
blocks/myoverview/templates/view-summary.mustache
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/privacy_test.php
blocks/tests/externallib_test.php
calendar/amd/build/view_manager.min.js
calendar/amd/build/view_manager.min.js.map
calendar/amd/src/view_manager.js
calendar/templates/header.mustache
calendar/templates/minicalendar_day_link.mustache
calendar/templates/month_detailed.mustache
completion/tests/behat/restrict_activity_by_grade.feature
completion/tests/behat/restrict_section_availability.feature
config-dist.php
contentbank/amd/build/sort.min.js
contentbank/amd/build/sort.min.js.map
contentbank/amd/src/sort.js
contentbank/classes/contentbank.php
contentbank/classes/contenttype.php
contentbank/classes/form/edit_content.php [new file with mode: 0644]
contentbank/classes/output/bankcontent.php
contentbank/classes/output/viewcontent.php [new file with mode: 0644]
contentbank/classes/privacy/provider.php
contentbank/contenttype/h5p/classes/contenttype.php
contentbank/contenttype/h5p/classes/form/editor.php [new file with mode: 0644]
contentbank/contenttype/h5p/db/access.php
contentbank/contenttype/h5p/lang/en/contenttype_h5p.php
contentbank/contenttype/h5p/version.php
contentbank/edit.php [new file with mode: 0644]
contentbank/index.php
contentbank/lib.php [new file with mode: 0644]
contentbank/templates/bankcontent.mustache
contentbank/templates/bankcontent/toolbar.mustache
contentbank/templates/bankcontent/toolbar_dropdown.mustache [new file with mode: 0644]
contentbank/templates/viewcontent.mustache [new file with mode: 0644]
contentbank/templates/viewcontent/toolbarview.mustache [new file with mode: 0644]
contentbank/tests/behat/edit_content.feature [new file with mode: 0644]
contentbank/tests/behat/sort_content.feature
contentbank/tests/behat/view_preferences.feature [new file with mode: 0644]
contentbank/tests/contentbank_test.php
contentbank/tests/fixtures/testable_contenttype.php
contentbank/tests/generator/lib.php
contentbank/tests/privacy_test.php
contentbank/view.php
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/src/activitychooser.js
course/classes/category.php
course/classes/management/helper.php
course/classes/management_renderer.php
course/format/upgrade.txt
course/lib.php
course/renderer.php
course/templates/activitychooser.mustache
course/templates/coursecard.mustache
course/templates/local/activitychooser/help.mustache
course/templates/local/activitychooser/item.mustache
course/templates/local/activitychooser/search_results.mustache
course/tests/behat/behat_course.php
course/tests/behat/category_management.feature
course/tests/behat/rename_roles.feature
course/tests/category_test.php
course/tests/courselib_test.php
course/upgrade.txt
dataformat/html/classes/writer.php
dataformat/json/classes/writer.php
dataformat/pdf/classes/writer.php
dataformat/pdf/tests/writer_test.php [new file with mode: 0644]
dataformat/upgrade.txt
enrol/manual/tests/behat/quickenrolment.feature
files/renderer.php
files/tests/behat/license_help_modal.feature [new file with mode: 0644]
filter/displayh5p/filter.php
grade/edit/letter/index.php
grade/edit/tree/index.php
grade/edit/tree/lib.php
grade/grading/form/guide/tests/behat/edit_guide.feature
grade/grading/tests/behat/behat_grading.php
grade/import/direct/styles.css [deleted file]
grade/lib.php
grade/report/grader/lib.php
grade/report/grader/styles.css
grade/report/lib.php
grade/report/singleview/classes/local/screen/grade.php
grade/report/singleview/classes/local/screen/tablelike.php
grade/report/singleview/classes/local/screen/user.php
grade/report/singleview/templates/text_attribute.mustache
grade/report/singleview/tests/behat/bulk_insert_grades.feature
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_item_duplication.feature [new file with mode: 0644]
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_single_item_scales.feature
group/tests/behat/create_groups.feature
group/tests/behat/group_description.feature
h5p/amd/build/editor_display.min.js
h5p/amd/build/editor_display.min.js.map
h5p/amd/src/editor_display.js
h5p/classes/api.php
h5p/classes/editor_ajax.php
h5p/classes/framework.php
h5p/classes/helper.php
h5p/classes/player.php
h5p/h5plib/v124/lang/en/h5plib_v124.php
h5p/tests/api_test.php
h5p/tests/external_test.php
h5p/tests/generator/lib.php
h5p/tests/helper_test.php
install/lang/hat/admin.php [new file with mode: 0644]
install/lang/mwl/langconfig.php [new file with mode: 0644]
lang/en/admin.php
lang/en/auth.php
lang/en/backup.php
lang/en/badges.php
lang/en/cache.php
lang/en/contentbank.php
lang/en/deprecated.txt
lang/en/grades.php
lang/en/license.php
lang/en/moodle.php
lang/en/question.php
lang/en/repository.php
lang/en/role.php
lang/en/user.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-autocomplete.min.js.map
lib/amd/build/notification.min.js
lib/amd/build/notification.min.js.map
lib/amd/build/tag.min.js
lib/amd/build/tag.min.js.map
lib/amd/build/templates.min.js
lib/amd/build/templates.min.js.map
lib/amd/build/userfeedback.min.js [new file with mode: 0644]
lib/amd/build/userfeedback.min.js.map [new file with mode: 0644]
lib/amd/src/form-autocomplete.js
lib/amd/src/notification.js
lib/amd/src/tag.js
lib/amd/src/templates.js
lib/amd/src/userfeedback.js [new file with mode: 0644]
lib/badgeslib.php
lib/behat/classes/behat_core_generator.php
lib/behat/classes/partial_named_selector.php
lib/behat/core_behat_file_helper.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_filemanager.php
lib/classes/dataformat/base.php
lib/classes/dataformat/spout_base.php
lib/classes/event/userfeedback_give.php [new file with mode: 0644]
lib/classes/event/userfeedback_remind.php [new file with mode: 0644]
lib/classes/external/userfeedback/generate_url.php [new file with mode: 0644]
lib/classes/external/userfeedback/record_action.php [new file with mode: 0644]
lib/classes/notification.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/plugin_manager.php
lib/classes/user.php
lib/classes/userfeedback.php [new file with mode: 0644]
lib/db/access.php
lib/db/caches.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/deprecatedlib.php
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js
lib/editor/atto/plugins/media/yui/src/button/js/button.js
lib/form/dateselector.php
lib/form/datetimeselector.php
lib/form/filemanager.js
lib/form/filemanager.php
lib/form/templates/element-advcheckbox-inline.mustache
lib/form/templates/element-checkbox-inline.mustache
lib/form/templates/element-filemanager.mustache
lib/form/templates/element-filepicker.mustache
lib/form/templates/element-group-inline.mustache
lib/form/templates/element-group.mustache
lib/form/templates/element-password-inline.mustache
lib/form/templates/element-password.mustache
lib/form/templates/element-template.mustache
lib/form/templates/element-text-inline.mustache
lib/form/templates/element-text.mustache
lib/form/templates/element-url.mustache
lib/form/tests/behat/modgrade_validation.feature
lib/grade/grade_item.php
lib/grade/tests/grade_item_test.php
lib/gradelib.php
lib/grouplib.php
lib/licenselib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/questionlib.php
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/repository.js
lib/table/classes/external/dynamic/get.php [moved from lib/table/classes/external/dynamic/fetch.php with 98% similarity]
lib/table/tests/external/dynamic/get_test.php [moved from lib/table/tests/external/dynamic/fetch_test.php with 94% similarity]
lib/tablelib.php
lib/templates/action_menu_link.mustache
lib/templates/action_menu_trigger.mustache
lib/templates/campaign_content.mustache [new file with mode: 0644]
lib/templates/filemanager_chooselicense.mustache [new file with mode: 0644]
lib/templates/filemanager_default_searchform.mustache
lib/templates/filemanager_fileselect.mustache
lib/templates/filemanager_licenselinks.mustache [new file with mode: 0644]
lib/templates/filemanager_modal_generallayout.mustache
lib/templates/filemanager_page_generallayout.mustache
lib/templates/filemanager_selectlayout.mustache
lib/templates/filemanager_uploadform.mustache
lib/templates/form_autocomplete_input.mustache
lib/templates/form_autocomplete_layout.mustache [new file with mode: 0644]
lib/templates/inplace_editable.mustache
lib/templates/local/modal/alert.mustache
lib/templates/local/notification/cta.mustache [new file with mode: 0644]
lib/templates/local/toast/message.mustache
lib/templates/loginform.mustache
lib/templates/userfeedback_footer_link.mustache [new file with mode: 0644]
lib/testing/generator/data_generator.php
lib/tests/behat/app_behat_runtime.js
lib/tests/behat/behat_app.php
lib/tests/behat/behat_general.php
lib/tests/external/userfeedback/generate_url_test.php [new file with mode: 0644]
lib/tests/external/userfeedback/record_action_test.php [new file with mode: 0644]
lib/tests/gradelib_test.php
lib/tests/grouplib_test.php
lib/tests/licenselib_test.php [new file with mode: 0644]
lib/tests/questionlib_test.php
lib/tests/rsslib_test.php
lib/tests/upgradelib_test.php
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/notification/js/alert.js
lib/yui/src/notification/js/dialogue.js
message/output/popup/templates/notification_popover.mustache
message/templates/message_drawer_view_settings_body_content.mustache
message/templates/message_index.mustache
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/feedback/editpdf/tests/behat/group_annotations.feature
mod/assign/feedback/editpdf/tests/behat/view_previous_annotations.feature
mod/assign/feedback/file/tests/behat/feedback_file.feature
mod/assign/tests/behat/allow_another_attempt.feature
mod/assign/tests/behat/comment_inline.feature
mod/assign/tests/behat/display_grade.feature
mod/assign/tests/behat/edit_previous_feedback.feature
mod/assign/tests/behat/filter_by_marker.feature
mod/assign/tests/behat/grading_status.feature
mod/assign/tests/behat/group_submission.feature
mod/assign/tests/behat/hide_grader.feature
mod/assign/tests/behat/outcome_grading.feature
mod/assign/tests/behat/quickgrading.feature
mod/assign/tests/behat/rescale_grades.feature
mod/assign/tests/behat/steps_blind_marking.feature
mod/assign/tests/behat/submission_comments.feature
mod/choice/report.php
mod/forum/export.php
mod/forum/report/summary/tests/behat/course_summary.feature
mod/forum/report/summary/tests/behat/summary_data_post_dates.feature
mod/forum/templates/forum_discussion_nested_v2_first_post.mustache
mod/h5pactivity/classes/external/get_h5pactivities_by_courses.php [new file with mode: 0644]
mod/h5pactivity/classes/external/get_results.php [new file with mode: 0644]
mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php [new file with mode: 0644]
mod/h5pactivity/classes/local/manager.php
mod/h5pactivity/classes/local/report/results.php
mod/h5pactivity/classes/output/attempt.php
mod/h5pactivity/classes/output/result/sequencing.php
mod/h5pactivity/classes/output/result/truefalse.php
mod/h5pactivity/db/services.php
mod/h5pactivity/lang/en/h5pactivity.php
mod/h5pactivity/pix/icon.svg
mod/h5pactivity/tests/behat/add_h5pactivity.feature
mod/h5pactivity/tests/external/get_h5pactivities_by_courses_test.php [new file with mode: 0644]
mod/h5pactivity/tests/external/get_results_test.php [new file with mode: 0644]
mod/h5pactivity/tests/generator/lib.php
mod/h5pactivity/tests/local/manager_test.php
mod/h5pactivity/version.php
mod/h5pactivity/view.php
mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php
mod/quiz/accessrule/seb/tests/behat/edit_form.feature
mod/quiz/accessrule/seb/tests/event_test.php
mod/quiz/accessrule/seb/tests/rule_test.php
mod/quiz/attemptlib.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/styles.css
my/index.php
pix/i/bullhorn.svg [new file with mode: 0644]
privacy/classes/local/request/moodle_content_writer.php
privacy/tests/moodle_content_writer_test.php
question/classes/bank/action_column_base.php
question/classes/bank/copy_action_column.php
question/classes/bank/edit_action_column.php
question/classes/bank/export_xml_action_column.php
question/classes/bank/preview_action_column.php
question/engine/bank.php
question/lib.php
question/tests/behat/delete_questions.feature
question/tests/behat/edit_question_tags.feature [new file with mode: 0644]
question/tests/behat/edit_questions.feature
question/type/ddimageortext/amd/build/question.min.js
question/type/ddimageortext/amd/build/question.min.js.map
question/type/ddimageortext/amd/src/question.js
question/type/ddwtos/amd/build/ddwtos.min.js
question/type/ddwtos/amd/build/ddwtos.min.js.map
question/type/ddwtos/amd/src/ddwtos.js
question/type/ddwtos/tests/helper.php
question/type/ddwtos/tests/questiontype_test.php
question/type/gapselect/questiontypebase.php
question/type/gapselect/tests/helper.php
question/type/gapselect/tests/question_test.php
question/type/gapselect/tests/questiontype_test.php
question/type/gapselect/tests/walkthrough_test.php
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/questiontypebase.php
question/upgrade.txt
repository/contentbank/classes/contentbank_search.php [new file with mode: 0644]
repository/contentbank/classes/helper.php
repository/contentbank/lib.php
repository/contentbank/tests/behat/search_content.feature [new file with mode: 0644]
repository/contentbank/tests/behat/select_content.feature
repository/contentbank/tests/search_test.php [new file with mode: 0644]
repository/draftfiles_ajax.php
repository/filepicker.js
repository/lib.php
repository/tests/behat/behat_filepicker.php
repository/tests/behat/cancel_add_file.feature
repository/tests/behat/delete_files.feature
repository/tests/behat/select_file.feature
repository/upload/tests/behat/behat_repository_upload.php
theme/boost/amd/build/alert.min.js [deleted file]
theme/boost/amd/build/alert.min.js.map [deleted file]
theme/boost/amd/build/bootstrap/alert.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/alert.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/button.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/button.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/carousel.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/carousel.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/collapse.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/collapse.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/dropdown.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/dropdown.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/index.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/index.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/modal.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/modal.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/popover.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/popover.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/scrollspy.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/scrollspy.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tab.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tab.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/toast.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/toast.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tools/sanitizer.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tools/sanitizer.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tooltip.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tooltip.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/util.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/util.min.js.map [new file with mode: 0644]
theme/boost/amd/build/button.min.js [deleted file]
theme/boost/amd/build/button.min.js.map [deleted file]
theme/boost/amd/build/carousel.min.js [deleted file]
theme/boost/amd/build/carousel.min.js.map [deleted file]
theme/boost/amd/build/collapse.min.js [deleted file]
theme/boost/amd/build/collapse.min.js.map [deleted file]
theme/boost/amd/build/dropdown.min.js [deleted file]
theme/boost/amd/build/dropdown.min.js.map [deleted file]
theme/boost/amd/build/index.min.js [deleted file]
theme/boost/amd/build/index.min.js.map [deleted file]
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/build/modal.min.js [deleted file]
theme/boost/amd/build/modal.min.js.map [deleted file]
theme/boost/amd/build/pending.min.js
theme/boost/amd/build/pending.min.js.map
theme/boost/amd/build/popover.min.js
theme/boost/amd/build/popover.min.js.map
theme/boost/amd/build/sanitizer.min.js [deleted file]
theme/boost/amd/build/sanitizer.min.js.map [deleted file]
theme/boost/amd/build/scrollspy.min.js [deleted file]
theme/boost/amd/build/scrollspy.min.js.map [deleted file]
theme/boost/amd/build/tab.min.js [deleted file]
theme/boost/amd/build/tab.min.js.map [deleted file]
theme/boost/amd/build/tether.min.js [deleted file]
theme/boost/amd/build/tether.min.js.map [deleted file]
theme/boost/amd/build/toast.min.js
theme/boost/amd/build/toast.min.js.map
theme/boost/amd/build/tooltip.min.js [deleted file]
theme/boost/amd/build/tooltip.min.js.map [deleted file]
theme/boost/amd/build/util.min.js [deleted file]
theme/boost/amd/build/util.min.js.map [deleted file]
theme/boost/amd/src/alert.js [deleted file]
theme/boost/amd/src/bootstrap/alert.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/button.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/carousel.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/collapse.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/dropdown.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/index.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/modal.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/popover.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/scrollspy.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/tab.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/toast.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/tools/sanitizer.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/tooltip.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/util.js [new file with mode: 0644]
theme/boost/amd/src/button.js [deleted file]
theme/boost/amd/src/carousel.js [deleted file]
theme/boost/amd/src/collapse.js [deleted file]
theme/boost/amd/src/dropdown.js [deleted file]
theme/boost/amd/src/index.js [deleted file]
theme/boost/amd/src/loader.js
theme/boost/amd/src/modal.js [deleted file]
theme/boost/amd/src/pending.js
theme/boost/amd/src/popover.js
theme/boost/amd/src/sanitizer.js [deleted file]
theme/boost/amd/src/scrollspy.js [deleted file]
theme/boost/amd/src/tab.js [deleted file]
theme/boost/amd/src/tether.js [deleted file]
theme/boost/amd/src/toast.js
theme/boost/amd/src/tooltip.js [deleted file]
theme/boost/amd/src/util.js [deleted file]
theme/boost/readme_moodle.txt
theme/boost/scss/bootstrap/_badge.scss
theme/boost/scss/bootstrap/_breadcrumb.scss
theme/boost/scss/bootstrap/_button-group.scss
theme/boost/scss/bootstrap/_buttons.scss
theme/boost/scss/bootstrap/_card.scss
theme/boost/scss/bootstrap/_carousel.scss
theme/boost/scss/bootstrap/_close.scss
theme/boost/scss/bootstrap/_code.scss
theme/boost/scss/bootstrap/_custom-forms.scss
theme/boost/scss/bootstrap/_dropdown.scss
theme/boost/scss/bootstrap/_forms.scss
theme/boost/scss/bootstrap/_functions.scss
theme/boost/scss/bootstrap/_grid.scss
theme/boost/scss/bootstrap/_images.scss
theme/boost/scss/bootstrap/_input-group.scss
theme/boost/scss/bootstrap/_list-group.scss
theme/boost/scss/bootstrap/_mixins.scss
theme/boost/scss/bootstrap/_modal.scss
theme/boost/scss/bootstrap/_nav.scss
theme/boost/scss/bootstrap/_navbar.scss
theme/boost/scss/bootstrap/_pagination.scss
theme/boost/scss/bootstrap/_popover.scss
theme/boost/scss/bootstrap/_print.scss
theme/boost/scss/bootstrap/_progress.scss
theme/boost/scss/bootstrap/_reboot.scss
theme/boost/scss/bootstrap/_root.scss
theme/boost/scss/bootstrap/_spinners.scss
theme/boost/scss/bootstrap/_tables.scss
theme/boost/scss/bootstrap/_type.scss
theme/boost/scss/bootstrap/_utilities.scss
theme/boost/scss/bootstrap/_variables.scss
theme/boost/scss/bootstrap/bootstrap-grid.scss
theme/boost/scss/bootstrap/bootstrap-reboot.scss
theme/boost/scss/bootstrap/bootstrap.scss
theme/boost/scss/bootstrap/mixins/_background-variant.scss
theme/boost/scss/bootstrap/mixins/_badge.scss
theme/boost/scss/bootstrap/mixins/_border-radius.scss
theme/boost/scss/bootstrap/mixins/_buttons.scss
theme/boost/scss/bootstrap/mixins/_caret.scss
theme/boost/scss/bootstrap/mixins/_float.scss
theme/boost/scss/bootstrap/mixins/_forms.scss
theme/boost/scss/bootstrap/mixins/_grid-framework.scss
theme/boost/scss/bootstrap/mixins/_grid.scss
theme/boost/scss/bootstrap/mixins/_hover.scss
theme/boost/scss/bootstrap/mixins/_image.scss
theme/boost/scss/bootstrap/mixins/_list-group.scss
theme/boost/scss/bootstrap/mixins/_lists.scss
theme/boost/scss/bootstrap/mixins/_nav-divider.scss
theme/boost/scss/bootstrap/mixins/_reset-text.scss
theme/boost/scss/bootstrap/mixins/_screen-reader.scss
theme/boost/scss/bootstrap/mixins/_table-row.scss
theme/boost/scss/bootstrap/mixins/_text-emphasis.scss
theme/boost/scss/bootstrap/mixins/_transition.scss
theme/boost/scss/bootstrap/utilities/_background.scss
theme/boost/scss/bootstrap/utilities/_interactions.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_text.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/buttons.scss
theme/boost/scss/moodle/contentbank.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/filemanager.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/icons.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/moodle/user.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/navbar.mustache
theme/boost/templates/secure.mustache
theme/boost/thirdpartylibs.xml
theme/classic/scss/classic/post.scss
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
theme/classic/templates/columns.mustache
theme/classic/templates/contentonly.mustache
theme/classic/templates/navbar.mustache
theme/classic/templates/secure.mustache
theme/classic/tests/behat/behat_theme_classic_behat_repository_upload.php
user/amd/build/local/participantsfilter/filter.min.js [new file with mode: 0644]
user/amd/build/local/participantsfilter/filter.min.js.map [new file with mode: 0644]
user/amd/build/local/participantsfilter/filtertypes/courseid.min.js [new file with mode: 0644]
user/amd/build/local/participantsfilter/filtertypes/courseid.min.js.map [new file with mode: 0644]
user/amd/build/local/participantsfilter/filtertypes/keyword.min.js [new file with mode: 0644]
user/amd/build/local/participantsfilter/filtertypes/keyword.min.js.map [new file with mode: 0644]
user/amd/build/local/participantsfilter/selectors.min.js [new file with mode: 0644]
user/amd/build/local/participantsfilter/selectors.min.js.map [new file with mode: 0644]
user/amd/build/participantsfilter.min.js [new file with mode: 0644]
user/amd/build/participantsfilter.min.js.map [new file with mode: 0644]
user/amd/build/unified_filter.min.js.map
user/amd/build/unified_filter_datasource.min.js.map
user/amd/src/local/participantsfilter/filter.js [new file with mode: 0644]
user/amd/src/local/participantsfilter/filtertypes/courseid.js [new file with mode: 0644]
user/amd/src/local/participantsfilter/filtertypes/keyword.js [new file with mode: 0644]
user/amd/src/local/participantsfilter/selectors.js [new file with mode: 0644]
user/amd/src/participantsfilter.js [new file with mode: 0644]
user/amd/src/unified_filter.js
user/amd/src/unified_filter_datasource.js
user/classes/output/participants_filter.php [new file with mode: 0644]
user/classes/output/unified_filter.php
user/classes/table/participants_search.php
user/index.php
user/lib.php
user/renderer.php
user/templates/local/participantsfilter/autocomplete_layout.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/autocomplete_selection.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/autocomplete_selection_items.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/filterrow.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/filtertype.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/filtertypes.mustache [new file with mode: 0644]
user/templates/participantsfilter.mustache [new file with mode: 0644]
user/templates/unified_filter.mustache
user/tests/behat/filter_participants.feature
user/tests/behat/filter_participants_showall.feature
user/tests/behat/view_participants_groups.feature
user/tests/table/participants_search_test.php
user/tests/userlib_test.php
user/upgrade.txt
version.php

index b9c0b6a..1bca50c 100644 (file)
@@ -74,19 +74,19 @@ media/player/videojs/videojs/video-js.swf
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
 theme/boost/scss/bootstrap/
-theme/boost/amd/src/alert.js
-theme/boost/amd/src/button.js
-theme/boost/amd/src/carousel.js
-theme/boost/amd/src/collapse.js
-theme/boost/amd/src/dropdown.js
-theme/boost/amd/src/index.js
-theme/boost/amd/src/modal.js
-theme/boost/amd/src/popover.js
-theme/boost/amd/src/sanitizer.js
-theme/boost/amd/src/scrollspy.js
-theme/boost/amd/src/tab.js
-theme/boost/amd/src/toast.js
-theme/boost/amd/src/tooltip.js
-theme/boost/amd/src/util.js
+theme/boost/amd/src/bootstrap/alert.js
+theme/boost/amd/src/bootstrap/button.js
+theme/boost/amd/src/bootstrap/carousel.js
+theme/boost/amd/src/bootstrap/collapse.js
+theme/boost/amd/src/bootstrap/dropdown.js
+theme/boost/amd/src/bootstrap/index.js
+theme/boost/amd/src/bootstrap/modal.js
+theme/boost/amd/src/bootstrap/popover.js
+theme/boost/amd/src/bootstrap/tools/sanitizer.js
+theme/boost/amd/src/bootstrap/scrollspy.js
+theme/boost/amd/src/bootstrap/tab.js
+theme/boost/amd/src/bootstrap/toast.js
+theme/boost/amd/src/bootstrap/tooltip.js
+theme/boost/amd/src/bootstrap/util.js
 theme/boost/amd/src/tether.js
 theme/boost/scss/fontawesome/
\ No newline at end of file
index 5d9e5c1..c0de22f 100644 (file)
@@ -75,19 +75,19 @@ media/player/videojs/videojs/video-js.swf
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
 theme/boost/scss/bootstrap/
-theme/boost/amd/src/alert.js
-theme/boost/amd/src/button.js
-theme/boost/amd/src/carousel.js
-theme/boost/amd/src/collapse.js
-theme/boost/amd/src/dropdown.js
-theme/boost/amd/src/index.js
-theme/boost/amd/src/modal.js
-theme/boost/amd/src/popover.js
-theme/boost/amd/src/sanitizer.js
-theme/boost/amd/src/scrollspy.js
-theme/boost/amd/src/tab.js
-theme/boost/amd/src/toast.js
-theme/boost/amd/src/tooltip.js
-theme/boost/amd/src/util.js
+theme/boost/amd/src/bootstrap/alert.js
+theme/boost/amd/src/bootstrap/button.js
+theme/boost/amd/src/bootstrap/carousel.js
+theme/boost/amd/src/bootstrap/collapse.js
+theme/boost/amd/src/bootstrap/dropdown.js
+theme/boost/amd/src/bootstrap/index.js
+theme/boost/amd/src/bootstrap/modal.js
+theme/boost/amd/src/bootstrap/popover.js
+theme/boost/amd/src/bootstrap/tools/sanitizer.js
+theme/boost/amd/src/bootstrap/scrollspy.js
+theme/boost/amd/src/bootstrap/tab.js
+theme/boost/amd/src/bootstrap/toast.js
+theme/boost/amd/src/bootstrap/tooltip.js
+theme/boost/amd/src/bootstrap/util.js
 theme/boost/amd/src/tether.js
 theme/boost/scss/fontawesome/
\ No newline at end of file
index ee3b2e3..5660318 100644 (file)
@@ -899,6 +899,9 @@ if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || i
 // Check if the site is being foced onto ssl.
 $overridetossl = !empty($CFG->overridetossl);
 
+// Check if moodle campaign content setting is enabled or not.
+$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
+
 admin_externalpage_setup('adminnotifications');
 
 $output = $PAGE->get_renderer('core', 'admin');
@@ -906,4 +909,5 @@ $output = $PAGE->get_renderer('core', 'admin');
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
                                        $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
                                        $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
-                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent);
+                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent,
+                                       $showcampaigncontent);
index 45f46a5..cda9ed6 100644 (file)
@@ -282,6 +282,7 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param bool $overridetossl Whether or not ssl is being forced.
      * @param bool $invalidforgottenpasswordurl Whether the forgotten password URL does not link to a valid URL.
      * @param bool $croninfrequent If true, warn that cron hasn't run in the past few minutes
+     * @param bool $showcampaigncontent Whether the campaign content should be visible or not.
      *
      * @return string HTML to output.
      */
@@ -289,7 +290,9 @@ class core_admin_renderer extends plugin_renderer_base {
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
             $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
             $themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
-            $overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false) {
+            $overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false,
+            $showcampaigncontent = false) {
+
         global $CFG;
         $output = '';
 
@@ -312,6 +315,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->registration_warning($registered);
         $output .= $this->mobile_configuration_warning($mobileconfigured);
         $output .= $this->forgotten_password_url_warning($invalidforgottenpasswordurl);
+        $output .= $this->campaign_content($showcampaigncontent);
 
         //////////////////////////////////////////////////////////////////////////////////////////////////
         ////  IT IS ILLEGAL AND A VIOLATION OF THE GPL TO HIDE, REMOVE OR MODIFY THIS COPYRIGHT NOTICE ///
@@ -878,6 +882,20 @@ class core_admin_renderer extends plugin_renderer_base {
         return $output;
     }
 
+    /**
+     * Display campaign content.
+     *
+     * @param bool $showcampaigncontent Whether the campaign content should be visible or not.
+     * @return string the campaign content raw html.
+     */
+    protected function campaign_content(bool $showcampaigncontent): string {
+        if (!$showcampaigncontent) {
+            return '';
+        }
+
+        return $this->render_from_template('core/campaign_content', ['lang' => current_language()]);
+    }
+
     /**
      * Display a warning about the forgotten password URL not linking to a valid URL.
      *
diff --git a/admin/settings/license.php b/admin/settings/license.php
new file mode 100644 (file)
index 0000000..bd9ad8f
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file defines the settings pages for licenses.
+ *
+ * @package    core
+ * @copyright  2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/licenselib.php');
+
+if ($hassiteconfig) {
+
+    $temp = new admin_settingpage('licensesettings', new lang_string('licensesettings', 'admin'));
+
+    $licenses = license_manager::get_active_licenses_as_array();
+
+    $temp->add(new admin_setting_configselect('sitedefaultlicense',
+        new lang_string('configsitedefaultlicense', 'admin'),
+        new lang_string('configsitedefaultlicensehelp', 'admin'),
+        'unknown',
+        $licenses));
+    $temp->add(new admin_setting_configcheckbox('rememberuserlicensepref',
+        new lang_string('rememberuserlicensepref', 'admin'),
+        new lang_string('rememberuserlicensepref_help', 'admin'),
+        1));
+    $ADMIN->add('license', $temp);
+}
index 278d4f3..4eb6ec4 100644 (file)
@@ -182,20 +182,6 @@ if ($hassiteconfig) {
         $plugin->load_settings($ADMIN, 'mlbackendsettings', $hassiteconfig);
     }
 
-/// License types
-    $ADMIN->add('modules', new admin_category('licensesettings', new lang_string('licenses')));
-    $temp = new admin_settingpage('managelicenses', new lang_string('managelicenses', 'admin'));
-
-    require_once($CFG->libdir . '/licenselib.php');
-    $licenses = array();
-    $array = explode(',', $CFG->licenses);
-    foreach ($array as $value) {
-        $licenses[$value] = new lang_string($value, 'license');
-    }
-    $temp->add(new admin_setting_configselect('sitedefaultlicense', new lang_string('configsitedefaultlicense','admin'), new lang_string('configsitedefaultlicensehelp','admin'), 'allrightsreserved', $licenses));
-    $temp->add(new admin_setting_managelicenses());
-    $ADMIN->add('licensesettings', $temp);
-
 /// Filter plugins
     $ADMIN->add('modules', new admin_category('filtersettings', new lang_string('managefilters')));
 
index 8c922d1..992436b 100644 (file)
@@ -20,6 +20,9 @@ $moodleservices = new admin_settingpage('moodleservices', new lang_string('moodl
     'admin'));
 $ADMIN->add('root', $moodleservices);
 
+$userfeedback = new admin_settingpage('userfeedback', new lang_string('feedbacksettings', 'admin'));
+$ADMIN->add('root', $userfeedback);
+
 if ($hassiteconfig) {
     $optionalsubsystems = new admin_settingpage('optionalsubsystems', new lang_string('advancedfeatures', 'admin'));
     $ADMIN->add('root', $optionalsubsystems);
@@ -32,6 +35,7 @@ $ADMIN->add('root', new admin_category('analytics', new lang_string('analytics',
 $ADMIN->add('root', new admin_category('competencies', new lang_string('competencies', 'core_competency')));
 $ADMIN->add('root', new admin_category('badges', new lang_string('badges'), empty($CFG->enablebadges)));
 $ADMIN->add('root', new admin_category('h5p', new lang_string('h5p', 'core_h5p')));
+$ADMIN->add('root', new admin_category('license', new lang_string('license')));
 $ADMIN->add('root', new admin_category('location', new lang_string('location','admin')));
 $ADMIN->add('root', new admin_category('language', new lang_string('language')));
 $ADMIN->add('root', new admin_category('messaging', new lang_string('messagingcategory', 'admin')));
diff --git a/admin/settings/userfeedback.php b/admin/settings/userfeedback.php
new file mode 100644 (file)
index 0000000..b7f3128
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains call to feedback settings
+ *
+ * @package    core
+ * @copyright  2020 Shamim Rezaie <shamim@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig) {
+
+    $userfeedback->add(new admin_setting_configcheckbox('enableuserfeedback',
+            new lang_string('enableuserfeedback', 'admin'),
+            new lang_string('enableuserfeedback_desc', 'admin'), 1, 1, 0));
+
+    $options = [
+        core_userfeedback::REMIND_AFTER_UPGRADE => new lang_string('userfeedbackafterupgrade', 'admin'),
+        core_userfeedback::REMIND_PERIODICALLY => new lang_string('userfeedbackperiodically', 'admin'),
+        core_userfeedback::REMIND_NEVER => new lang_string('never'),
+    ];
+    $userfeedback->add(new admin_setting_configselect('userfeedback_nextreminder',
+            new lang_string('userfeedbacknextreminder', 'admin'),
+            new lang_string('userfeedbacknextreminder_desc', 'admin'), 1, $options));
+    $userfeedback->hide_if('userfeedback_nextreminder', 'enableuserfeedback');
+
+    $userfeedback->add(new admin_setting_configtext('userfeedback_remindafter',
+            new lang_string('userfeedbackremindafter', 'admin'),
+            new lang_string('userfeedbackremindafter_desc', 'admin'), 90, PARAM_INT));
+    $userfeedback->hide_if('userfeedback_remindafter', 'enableuserfeedback');
+    $userfeedback->hide_if('userfeedback_remindafter', 'userfeedback_nextreminder', 'eq', 3);
+
+}
index a3a3a22..498aa2c 100644 (file)
 }}
 <div class="form-item row" id="{{id}}">
     <div class="form-label col-sm-3 text-sm-right">
-        <label {{#labelfor}}for="{{labelfor}}"{{/labelfor}}>
-            {{{title}}}
-            {{#override}}
-                <div class="alert alert-info">{{override}}</div>
-            {{/override}}
-            {{#warning}}
-                <div class="alert alert-warning">{{warning}}</div>
-            {{/warning}}
-        </label>
+        {{#customcontrol}}
+            <p {{#labelfor}}id="{{labelfor}}_label"{{/labelfor}}>
+                {{{title}}}
+                {{#override}}
+                    <div class="alert alert-info">{{override}}</div>
+                {{/override}}
+                {{#warning}}
+                    <div class="alert alert-warning">{{warning}}</div>
+                {{/warning}}
+            </p>
+        {{/customcontrol}}
+        {{^customcontrol}}
+            <label {{#labelfor}}for="{{labelfor}}"{{/labelfor}}>
+                {{{title}}}
+                {{#override}}
+                    <div class="alert alert-info">{{override}}</div>
+                {{/override}}
+                {{#warning}}
+                    <div class="alert alert-warning">{{warning}}</div>
+                {{/warning}}
+            </label>
+        {{/customcontrol}}
         <span class="form-shortname d-block small text-muted">{{{name}}}</span>
     </div>
     <div class="form-setting col-sm-9">
         {{#dependenton}}<div class="form-dependenton mb-4 text-muted">{{{.}}}</div>{{/dependenton}}
     </div>
 </div>
+{{#customcontrol}}
+    {{#js}}
+        require(['jquery'], function($) {
+            $('#{{id}}_label').css('cursor', 'default');
+            $('#{{id}}_label').click(function() {
+                $('#{{id}}')
+                    .find('button, a, input:not([type="hidden"]), select, textarea, [tabindex]')
+                    .filter(':not([disabled]):not([tabindex="0"]):not([tabindex="-1"])')
+                    .first().focus();
+            });
+        });
+    {{/js}}
+{{/customcontrol}}
index f0664c5..eea7b51 100644 (file)
@@ -24,7 +24,7 @@
 
 $string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically. Step definitions available for use in .feature files are listed below.';
 $string['allavailablesteps'] = 'All available step definitions';
-$string['errorapproot'] = '$CFG->behat_ionic_dirroot is not pointing to a valid Moodle Mobile developer install.';
+$string['errorapproot'] = '$CFG->behat_ionic_dirroot is not pointing to a valid Moodle app developer install.';
 $string['errorbehatcommand'] = 'Error running behat CLI command. Try running "{$a} --help" manually from CLI to find out more about the problem.';
 $string['errorcomposer'] = 'Composer dependencies are not installed.';
 $string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
index c6f7f0e..8384b2e 100644 (file)
@@ -12,7 +12,7 @@ Feature: Manage data categories
     And I press "Add category"
     And I set the field "Name" to "Category 1"
     And I set the field "Description" to "Category 1 description"
-    When I click on "Save" "button" in the "Delete category" "dialogue"
+    When I click on "Save" "button" in the "Add category" "dialogue"
     Then I should see "Category 1" in the "List of data categories" "table"
     And I should see "Category 1 description" in the "Category 1" "table_row"
 
@@ -30,5 +30,5 @@ Feature: Manage data categories
     And I choose "Delete" in the open action menu
     And I should see "Delete category"
     And I should see "Are you sure you want to delete the category 'Category 1'?"
-    When I click on "Delete" "button" in the "Confirm" "dialogue"
+    When I click on "Delete" "button" in the "Delete category" "dialogue"
     Then I should not see "Category 1" in the "List of data categories" "table"
index 772c590..dcd0c2a 100644 (file)
@@ -52,5 +52,5 @@ Feature: Manage data storage purposes
     And I choose "Delete" in the open action menu
     And I should see "Delete purpose"
     And I should see "Are you sure you want to delete the purpose 'Purpose 1'?"
-    When I click on "Delete" "button" in the "Confirm" "dialogue"
+    When I click on "Delete" "button" in the "Delete purpose" "dialogue"
     Then I should not see "Purpose 1" in the "List of data purposes" "table"
diff --git a/admin/tool/licensemanager/amd/build/delete_license.min.js b/admin/tool/licensemanager/amd/build/delete_license.min.js
new file mode 100644 (file)
index 0000000..17dd2af
Binary files /dev/null and b/admin/tool/licensemanager/amd/build/delete_license.min.js differ
diff --git a/admin/tool/licensemanager/amd/build/delete_license.min.js.map b/admin/tool/licensemanager/amd/build/delete_license.min.js.map
new file mode 100644 (file)
index 0000000..caee9ae
Binary files /dev/null and b/admin/tool/licensemanager/amd/build/delete_license.min.js.map differ
diff --git a/admin/tool/licensemanager/amd/src/delete_license.js b/admin/tool/licensemanager/amd/src/delete_license.js
new file mode 100644 (file)
index 0000000..6abdcec
--- /dev/null
@@ -0,0 +1,51 @@
+// 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/>.
+
+/**
+ * Modal for confirming deletion of a custom license.
+ *
+ * @module     tool_licensemanager/delete_license
+ * @class      delete_license
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/modal_factory', 'core/modal_events', 'core/url', 'core/str'],
+    function($, ModalFactory, ModalEvents, Url, String) {
+
+        var trigger = $('.delete-license');
+        ModalFactory.create({
+            type: ModalFactory.types.SAVE_CANCEL,
+            title: String.get_string('deletelicense', 'tool_licensemanager'),
+            body: String.get_string('deletelicenseconfirmmessage', 'tool_licensemanager'),
+            preShowCallback: function(triggerElement, modal) {
+                triggerElement = $(triggerElement);
+                let params = {
+                    'action': 'delete',
+                    'license': triggerElement.data('license')
+                };
+                modal.deleteURL = Url.relativeUrl('/admin/tool/licensemanager/index.php', params, true);
+            },
+            large: true,
+        }, trigger)
+            .done(function(modal) {
+                modal.getRoot().on(ModalEvents.save, function(e) {
+                    // Stop the default save button behaviour which is to close the modal.
+                    e.preventDefault();
+                    // Redirect to delete url.
+                    window.location.href = modal.deleteURL;
+                });
+            });
+    });
diff --git a/admin/tool/licensemanager/classes/form/edit_license.php b/admin/tool/licensemanager/classes/form/edit_license.php
new file mode 100644 (file)
index 0000000..624ce80
--- /dev/null
@@ -0,0 +1,124 @@
+<?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/>.
+
+/**
+ * Form for creating/updating a custom license.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_licensemanager\form;
+
+use moodleform;
+use tool_licensemanager\helper;
+use tool_licensemanager\manager;
+
+defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.');
+
+global $CFG;
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Form for creating/updating a custom license.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class edit_license extends moodleform {
+
+    /**
+     * @var string the action form is taking.
+     */
+    private $action;
+
+    /**
+     * @var string license shortname if editing or empty string if creating license.
+     */
+    private $licenseshortname;
+
+    /**
+     * edit_license constructor.
+     *
+     * @param string $action the license_manager action to be taken by form.
+     * @param string $licenseshortname the shortname of the license to edit.
+     */
+    public function __construct(string $action, string $licenseshortname) {
+        $this->action = $action;
+        $this->licenseshortname = $licenseshortname;
+
+        if ($action == manager::ACTION_UPDATE && !empty($licenseshortname)) {
+            parent::__construct(helper::get_update_license_url($licenseshortname));
+        } else {
+            parent::__construct(helper::get_create_license_url());
+        }
+    }
+
+    /**
+     * Form definition for creation and editing of licenses.
+     */
+    public function definition() {
+
+        $mform = $this->_form;
+
+        $mform->addElement('text', 'shortname', get_string('shortname', 'tool_licensemanager'));
+        $mform->setType('shortname', PARAM_ALPHANUMEXT);
+        // Shortname is only editable when user is creating a license.
+        if ($this->action != manager::ACTION_CREATE) {
+            $mform->freeze('shortname');
+        } else {
+            $mform->addRule('shortname', get_string('shortnamerequirederror', 'tool_licensemanager'), 'required');
+        }
+
+        $mform->addElement('text', 'fullname', get_string('fullname', 'tool_licensemanager'));
+        $mform->setType('fullname', PARAM_TEXT);
+        $mform->addRule('fullname', get_string('fullnamerequirederror', 'tool_licensemanager'), 'required');
+
+        $mform->addElement('text', 'source', get_string('source', 'tool_licensemanager'));
+        $mform->setType('source', PARAM_URL);
+        $mform->addHelpButton('source', 'source', 'tool_licensemanager');
+        $mform->addRule('source', get_string('sourcerequirederror', 'tool_licensemanager'), 'required');
+
+        $mform->addElement('date_selector', 'version', get_string('version', 'tool_licensemanager'), get_string('from'));
+        $mform->addHelpButton('version', 'version', 'tool_licensemanager');
+
+        $this->add_action_buttons();
+    }
+
+    /**
+     * Validate form data and return errors (if any).
+     *
+     * @param array $data array of ("fieldname"=>value) of submitted data
+     * @param array $files array of uploaded files "element_name"=>tmp_file_path
+     * @return array of "element_name"=>"error_description" if there are errors,
+     *         or an empty array if everything is OK (true allowed for backwards compatibility too).
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        if (array_key_exists('source', $data)  && !filter_var($data['source'], FILTER_VALIDATE_URL)) {
+            $errors['source'] = get_string('invalidurl', 'tool_licensemanager');
+        }
+
+        if (array_key_exists('version', $data) && $data['version'] > time()) {
+            $errors['version'] = get_string('versioncannotbefuture', 'tool_licensemanager');
+        }
+
+        return $errors;
+    }
+}
diff --git a/admin/tool/licensemanager/classes/helper.php b/admin/tool/licensemanager/classes/helper.php
new file mode 100644 (file)
index 0000000..b71a9aa
--- /dev/null
@@ -0,0 +1,154 @@
+<?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/>.
+
+/**
+ * License manager helper class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_licensemanager;
+
+use moodle_url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * License manager helper class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+
+    /**
+     * Moodle relative path to the licenses manager.
+     */
+    const MANAGER_PATH = '/admin/tool/licensemanager/index.php';
+
+    /**
+     * Get the URL for viewing the license manager interface.
+     *
+     * @return \moodle_url
+     */
+    public static function get_licensemanager_url() : moodle_url {
+        global $CFG;
+
+        $url = new moodle_url($CFG->wwwroot . self::MANAGER_PATH,
+            ['sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL for endpoint enabling a license.
+     *
+     * @param string $licenseshortname the shortname of license to enable.
+     *
+     * @return \moodle_url
+     */
+    public static function get_enable_license_url(string $licenseshortname) : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_ENABLE, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL for endpoint disabling a license.
+     *
+     * @param string $licenseshortname the shortname of license to disable.
+     *
+     * @return \moodle_url
+     */
+    public static function get_disable_license_url(string $licenseshortname) : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_DISABLE, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL endpoint to create a new license.
+     *
+     * @return \moodle_url
+     */
+    public static function get_create_license_url() : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_CREATE, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL endpoint to update an existing license.
+     *
+     * @param string $licenseshortname the shortname of license to update.
+     *
+     * @return \moodle_url
+     */
+    public static function get_update_license_url(string $licenseshortname) : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_UPDATE, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL endpoint to move a license up order.
+     *
+     * @param string $licenseshortname the shortname of license to move up.
+     *
+     * @return \moodle_url
+     */
+    public static function get_moveup_license_url(string $licenseshortname) : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_MOVE_UP, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL endpoint to move a license down order.
+     *
+     * @param string $licenseshortname the shortname of license to move down.
+     *
+     * @return \moodle_url
+     */
+    public static function get_movedown_license_url(string $licenseshortname) : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_MOVE_DOWN, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Convert a license version number string to a UNIX epoch.
+     *
+     * @param string $version
+     *
+     * @return int $epoch
+     */
+    public static function convert_version_to_epoch(string $version) : int {
+        $date = substr($version, 0, 8);
+        $epoch = strtotime($date);
+
+        return $epoch;
+    }
+}
diff --git a/admin/tool/licensemanager/classes/manager.php b/admin/tool/licensemanager/classes/manager.php
new file mode 100644 (file)
index 0000000..2f3ad2c
--- /dev/null
@@ -0,0 +1,247 @@
+<?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/>.
+
+/**
+ * License manager.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_licensemanager;
+
+use tool_licensemanager\form\edit_license;
+use license_manager;
+use stdClass;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * License manager, main controller for tool_licensemanager.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager {
+
+    /**
+     * Action for creating a new custom license.
+     */
+    const ACTION_CREATE = 'create';
+
+    /**
+     * Action for updating a custom license's details.
+     */
+    const ACTION_UPDATE = 'update';
+
+    /**
+     * Action for deleting a custom license.
+     */
+    const ACTION_DELETE = 'delete';
+
+    /**
+     * Action for disabling a custom license.
+     */
+    const ACTION_DISABLE = 'disable';
+
+    /**
+     * Action for enabling a custom license.
+     */
+    const ACTION_ENABLE = 'enable';
+
+    /**
+     * Action for displaying the license list view.
+     */
+    const ACTION_VIEW_LICENSE_MANAGER = 'viewlicensemanager';
+
+    /**
+     * Action for moving a license up order.
+     */
+    const ACTION_MOVE_UP = 'moveup';
+
+    /**
+     * Action for moving a license down order.
+     */
+    const ACTION_MOVE_DOWN = 'movedown';
+
+    /**
+     * Entry point for internal license manager.
+     *
+     * @param string $action the api action to carry out.
+     * @param string|object $license the license object or shortname of license to carry action out on.
+     */
+    public function execute(string $action, $license) : void {
+
+        admin_externalpage_setup('licensemanager');
+
+        // Convert license to a string if it's a full license object.
+        if (is_object($license)) {
+            $license = $license->shortname;
+        }
+
+        $viewmanager = true;
+
+        switch ($action) {
+            case self::ACTION_DISABLE:
+                license_manager::disable($license);
+                break;
+
+            case self::ACTION_ENABLE:
+                license_manager::enable($license);
+                break;
+
+            case self::ACTION_DELETE:
+                license_manager::delete($license);
+                break;
+
+            case self::ACTION_CREATE:
+            case self::ACTION_UPDATE:
+                $viewmanager = $this->edit($action, $license);
+                break;
+
+            case self::ACTION_MOVE_UP:
+            case self::ACTION_MOVE_DOWN:
+                $this->change_license_order($action, $license);
+                break;
+
+            case self::ACTION_VIEW_LICENSE_MANAGER:
+            default:
+                break;
+        }
+        if ($viewmanager) {
+            $this->view_license_manager();
+        }
+    }
+
+    /**
+     * Edit an existing license or create a new license.
+     *
+     * @param string $action the form action to carry out.
+     * @param string $licenseshortname the shortname of the license to edit.
+     *
+     * @return bool true if license editing complete, false otherwise.
+     */
+    private function edit(string $action, string $licenseshortname) : bool {
+
+        if ($action != self::ACTION_CREATE && $action != self::ACTION_UPDATE) {
+            throw new \coding_exception('license edit actions are limited to create and update');
+        }
+
+        $form = new form\edit_license($action, $licenseshortname);
+
+        if ($form->is_cancelled()) {
+            return true;
+        } else if ($data = $form->get_data()) {
+
+            $license = new stdClass();
+            if ($action == self::ACTION_CREATE) {
+                // Check that license shortname isn't already in use.
+                if (!empty(license_manager::get_license_by_shortname($data->shortname))) {
+                    print_error('duplicatelicenseshortname', 'tool_licensemanager',
+                        helper::get_licensemanager_url(),
+                        $data->shortname);
+                }
+                $license->shortname = $data->shortname;
+            } else {
+                if (empty(license_manager::get_license_by_shortname($licenseshortname))) {
+                    print_error('licensenotfoundshortname', 'license',
+                        helper::get_licensemanager_url(),
+                        $licenseshortname);
+                }
+                $license->shortname = $licenseshortname;
+            }
+            $license->fullname = $data->fullname;
+            $license->source = $data->source;
+            // Legacy date format maintained to prevent breaking on upgrade.
+            $license->version = date('Ymd', $data->version) . '00';
+
+            license_manager::save($license);
+
+            return true;
+        } else {
+            $this->view_license_editor($action, $licenseshortname, $form);
+
+            return false;
+        }
+    }
+
+    /**
+     * Change license order by moving up or down license order.
+     *
+     * @param string $direction which direction to move, up or down.
+     * @param string $licenseshortname the shortname of the license to move up or down order.
+     */
+    private function change_license_order(string $direction, string $licenseshortname) : void {
+
+        if (!empty($licenseshortname)) {
+            if ($direction == self::ACTION_MOVE_UP) {
+                license_manager::change_license_sortorder(license_manager::LICENSE_MOVE_UP, $licenseshortname);
+            } else if ($direction == self::ACTION_MOVE_DOWN) {
+                license_manager::change_license_sortorder(license_manager::LICENSE_MOVE_DOWN, $licenseshortname);
+            }
+        }
+    }
+
+    /**
+     * View the license editor to create or edit a license.
+     *
+     * @param string $action
+     * @param string $licenseshortname the shortname of the license to create/edit.
+     * @param \tool_licensemanager\form\edit_license $form the form for submitting edit data.
+     */
+    private function view_license_editor(string $action, string $licenseshortname, edit_license $form) : void {
+        global $PAGE;
+
+        $renderer = $PAGE->get_renderer('tool_licensemanager');
+
+        if ($action == self::ACTION_UPDATE && $license = license_manager::get_license_by_shortname($licenseshortname)) {
+            $return = $renderer->render_edit_licence_headers($licenseshortname);
+
+            $form->set_data(['shortname' => $license->shortname]);
+            $form->set_data(['fullname' => $license->fullname]);
+            $form->set_data(['source' => $license->source]);
+            $form->set_data(['version' => helper::convert_version_to_epoch($license->version)]);
+
+        } else {
+            $return = $renderer->render_create_licence_headers();
+        }
+        $return .= $form->render();
+        $return .= $renderer->footer();
+
+        echo $return;
+    }
+
+    /**
+     * View the license manager.
+     */
+    private function view_license_manager() : void {
+        global $PAGE;
+
+        $PAGE->requires->js_call_amd('tool_licensemanager/delete_license');
+
+        $renderer = $PAGE->get_renderer('tool_licensemanager');
+        $html = $renderer->header();
+        $html .= $renderer->heading(get_string('licensemanager', 'tool_licensemanager'));
+
+        $table = new \tool_licensemanager\output\table();
+        $html .= $renderer->render($table);
+        $html .= $renderer->footer();
+
+        echo $html;
+    }
+}
diff --git a/admin/tool/licensemanager/classes/output/renderer.php b/admin/tool/licensemanager/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..19ac8d4
--- /dev/null
@@ -0,0 +1,96 @@
+<?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/>.
+
+/**
+ * Renderer for 'tool_licensemanager' component.
+ *
+ * @package    tool_licensemanager
+ * @copyright  Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_licensemanager\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use license_manager;
+use plugin_renderer_base;
+use tool_licensemanager\helper;
+
+/**
+ * Renderer class for 'tool_licensemanager' component.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+    /**
+     * Render the headers for create license form.
+     *
+     * @return string html fragment for display.
+     */
+    public function render_create_licence_headers() : string {
+
+        $this->page->navbar->add(get_string('createlicense', 'tool_licensemanager'),
+            helper::get_create_license_url());
+
+        $return = $this->header();
+        $return .= $this->heading(get_string('createlicense', 'tool_licensemanager'));
+
+        return $return;
+    }
+
+    /**
+     * Render the headers for edit license form.
+     *
+     * @param string $licenseshortname the shortname of license to edit.
+     *
+     * @return string html fragment for display.
+     */
+    public function render_edit_licence_headers(string $licenseshortname) : string {
+
+        $this->page->navbar->add(get_string('editlicense', 'tool_licensemanager'),
+            helper::get_update_license_url($licenseshortname));
+
+        $return = $this->header();
+        $return .= $this->heading(get_string('editlicense', 'tool_licensemanager'));
+
+        return $return;
+    }
+
+    /**
+     * Render the license manager table.
+     *
+     * @param \renderable $table the renderable.
+     *
+     * @return string HTML.
+     */
+    public function render_table(\renderable $table) {
+        $licenses = license_manager::get_licenses();
+
+        // Add the create license button.
+        $html = $table->create_license_link();
+
+        // Add the table containing licenses for management.
+        $html .= $this->box_start('generalbox editorsui');
+        $html .= $table->create_license_manager_table($licenses, $this);
+        $html .= $this->box_end();
+
+        return $html;
+    }
+}
diff --git a/admin/tool/licensemanager/classes/output/table.php b/admin/tool/licensemanager/classes/output/table.php
new file mode 100644 (file)
index 0000000..17144fc
--- /dev/null
@@ -0,0 +1,185 @@
+<?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/>.
+
+/**
+ * Renderable for display of license manager table.
+ *
+ * @package   tool_licensemanager
+ * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_licensemanager\output;
+
+use html_table;
+use html_table_cell;
+use html_table_row;
+use html_writer;
+use license_manager;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Renderable for display of license manager table.
+ *
+ * @package   tool_licensemanager
+ * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class table implements \renderable {
+
+    /**
+     * 'Create License' link.
+     *
+     * @return string HTML string.
+     */
+    public function create_license_link() {
+        $link = html_writer::link(\tool_licensemanager\helper::get_create_license_url(),
+            get_string('createlicensebuttontext', 'tool_licensemanager'),
+            ['class' => 'btn btn-secondary mb-3']);
+
+        return $link;
+    }
+
+    /**
+     * Create the HTML table for license management.
+     *
+     * @param array $licenses
+     * @param \renderer_base $output
+     *
+     * @return string HTML for license manager table.
+     */
+    public function create_license_manager_table(array $licenses, \renderer_base $output) {
+        $table = new html_table();
+        $table->head  = [
+            get_string('enable'),
+            get_string('license', 'tool_licensemanager'),
+            get_string('version'),
+            get_string('order'),
+            get_string('edit'),
+            get_string('delete'),
+        ];
+        $table->colclasses = [
+            'text-center',
+            'text-left',
+            'text-left',
+            'text-center',
+            'text-center',
+            'text-center',
+        ];
+        $table->id = 'manage-licenses';
+        $table->attributes['class'] = 'admintable generaltable';
+        $table->data  = [];
+
+        $rownumber = 0;
+        $rowcount = count($licenses);
+
+        foreach ($licenses as $key => $value) {
+            $canmoveup = $rownumber > 0;
+            $canmovedown = $rownumber < $rowcount - 1;
+            $table->data[] = $this->get_license_table_row_data($value, $canmoveup, $canmovedown, $output);
+            $rownumber++;
+        }
+
+        $html = html_writer::table($table);
+
+        return $html;
+    }
+
+    /**
+     * Get table row data for a license.
+     *
+     * @param object $license the license to populate row data for.
+     * @param bool $canmoveup can this row move up.
+     * @param bool $canmovedown can this row move down.
+     * @param \renderer_base $output the renderer
+     *
+     * @return \html_table_row of columns values for row.
+     */
+    protected function get_license_table_row_data($license, bool $canmoveup, bool $canmovedown, \renderer_base $output) {
+        global $CFG;
+
+        $summary = $license->fullname . ' ('. $license->shortname . ')';
+        if (!empty($license->source)) {
+            $summary .= html_writer::empty_tag('br');
+            $summary .= html_writer::link($license->source, $license->source, ['target' => '_blank']);
+        }
+        $summarycell = new html_table_cell($summary);
+        $summarycell->attributes['class'] = 'license-summary';
+        $versioncell = new html_table_cell($license->version);
+        $versioncell->attributes['class'] = 'license-version';
+
+        $deletelicense = '';
+        if ($license->shortname == $CFG->sitedefaultlicense) {
+            $hideshow = $output->pix_icon('t/locked', get_string('sitedefaultlicenselock', 'tool_licensemanager'));
+        } else {
+            if ($license->enabled == license_manager::LICENSE_ENABLED) {
+                $hideshow = html_writer::link(\tool_licensemanager\helper::get_disable_license_url($license->shortname),
+                    $output->pix_icon('t/hide', get_string('disablelicensename', 'tool_licensemanager', $license->fullname)));
+            } else {
+                $hideshow = html_writer::link(\tool_licensemanager\helper::get_enable_license_url($license->shortname),
+                    $output->pix_icon('t/show', get_string('enablelicensename', 'tool_licensemanager', $license->fullname)));
+            }
+
+            if ($license->custom == license_manager::CUSTOM_LICENSE) {
+                // Link url is added by the JS `delete_license` modal used for confirmation of deletion, to avoid
+                // link being usable before JavaScript loads on page.
+                $deletelicense = html_writer::link('#', $output->pix_icon('i/trash',
+                    get_string('deletelicensename', 'tool_licensemanager', $license->fullname)),
+                    ['class' => 'delete-license', 'data-license' => $license->shortname]);
+            }
+        }
+        $hideshowcell = new html_table_cell($hideshow);
+        $hideshowcell->attributes['class'] = 'license-status';
+
+        if ($license->custom == license_manager::CUSTOM_LICENSE) {
+            $editlicense = html_writer::link(\tool_licensemanager\helper::get_update_license_url($license->shortname),
+                $output->pix_icon('t/editinline', get_string('editlicensename', 'tool_licensemanager', $license->fullname)),
+                ['class' => 'edit-license']);
+        } else {
+            $editlicense = '';
+        }
+        $editlicensecell = new html_table_cell($editlicense);
+        $editlicensecell->attributes['class'] = 'edit-license';
+
+        $spacer = $output->pix_icon('spacer', '', 'moodle', ['class' => 'iconsmall']);
+        $updown = '';
+        if ($canmoveup) {
+            $updown .= html_writer::link(\tool_licensemanager\helper::get_moveup_license_url($license->shortname),
+                    $output->pix_icon('t/up', get_string('movelicenseupname', 'tool_licensemanager', $license->fullname),
+                        'moodle', ['class' => 'iconsmall']),
+                    ['class' => 'move-up']) . '';
+        } else {
+            $updown .= $spacer;
+        }
+
+        if ($canmovedown) {
+            $updown .= '&nbsp;'.html_writer::link(\tool_licensemanager\helper::get_movedown_license_url($license->shortname),
+                    $output->pix_icon('t/down', get_string('movelicensedownname', 'tool_licensemanager', $license->fullname),
+                        'moodle', ['class' => 'iconsmall']),
+                    ['class' => 'move-down']);
+        } else {
+            $updown .= $spacer;
+        }
+        $updowncell = new html_table_cell($updown);
+        $updowncell->attributes['class'] = 'license-order';
+
+        $row = new html_table_row([$hideshowcell, $summarycell, $versioncell, $updowncell, $editlicensecell, $deletelicense]);
+        $row->attributes['data-license'] = $license->shortname;
+        $row->attributes['class'] = strtolower(get_string('license', 'tool_licensemanager'));
+
+        return $row;
+    }
+}
diff --git a/admin/tool/licensemanager/classes/privacy/provider.php b/admin/tool/licensemanager/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..b4eb611
--- /dev/null
@@ -0,0 +1,47 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for tool_licensemanager implementing null_provider.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_licensemanager\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem implementation for tool_licensemanager implementing null_provider.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
similarity index 55%
rename from admin/licenses.php
rename to admin/tool/licensemanager/index.php
index 820e775..123dd9d 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Allows admin to configure licenses.
+ * License manager page.
+ *
+ * @package   tool_licensemanager
+ * @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require_once('../config.php');
-require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/licenselib.php');
+require_once('../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+require_once($CFG->libdir . '/licenselib.php');
 
 require_admin();
 
-$returnurl = "$CFG->wwwroot/$CFG->admin/settings.php?section=managelicenses";
+$returnurl = \tool_licensemanager\helper::get_licensemanager_url();
 
 $action = optional_param('action', '', PARAM_ALPHANUMEXT);
 $license = optional_param('license', '', PARAM_SAFEDIR);
 
-////////////////////////////////////////////////////////////////////////////////
-// process actions
-
 if (!confirm_sesskey()) {
     redirect($returnurl);
 }
 
-$return = true;
-switch ($action) {
-    case 'disable':
-        license_manager::disable($license);
-        break;
-
-    case 'enable':
-        license_manager::enable($license);
-        break;
+// Route via the manager.
+$licensemanager = new \tool_licensemanager\manager();
+$PAGE->set_context(context_system::instance());
+$PAGE->set_url(\tool_licensemanager\helper::get_licensemanager_url());
+$PAGE->set_title(get_string('licensemanager', 'tool_licensemanager'));
 
-    default:
-        break;
-}
-
-if ($return) {
-    redirect ($returnurl);
-}
+$licensemanager->execute($action, $license);
diff --git a/admin/tool/licensemanager/lang/en/tool_licensemanager.php b/admin/tool/licensemanager/lang/en/tool_licensemanager.php
new file mode 100644 (file)
index 0000000..97e372a
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * Strings for component 'tool_licensemanager', language 'en'
+ *
+ * @package   tool_licensemanager
+ * @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+$string['pluginname'] = 'License manager';
+$string['createlicense'] = 'Create custom licence';
+$string['createlicensebuttontext'] = 'Create licence';
+$string['deletelicense'] = 'Delete licence';
+$string['deletelicenseconfirmmessage'] = 'Are you sure you want to delete this licence?';
+$string['deletelicensename'] = 'Delete license \'{$a}\'';
+$string['disablelicensename'] = 'Disable licence \'{$a}\'';
+$string['duplicatelicenseshortname'] = 'Licence shortname must be unique, duplicate value found.';
+$string['editlicense'] = 'Edit licence';
+$string['editlicensename'] = 'Edit licence \'{$a}\'';
+$string['enablelicensename'] = 'Enable licence \'{$a}\'';
+$string['fullname'] = 'Licence full name';
+$string['fullnamerequirederror'] = 'You must enter a full name for the licence.';
+$string['invalidurl'] = 'Invalid source URL';
+$string['license'] = 'Licence';
+$string['licensemanager'] = 'Licence manager';
+$string['movelicensedownname'] = 'Move \'{$a}\' license down order';
+$string['movelicenseupname'] = 'Move \'{$a}\' license up order';
+$string['privacy:metadata'] = 'The tool_licensemanager plugin stores no personal data.';
+$string['shortname'] = 'Licence short name';
+$string['sitedefaultlicenselock'] = 'This is the site default license. It cannot be disabled.';
+$string['shortnamerequirederror'] = 'You must enter a short name for the licence.';
+$string['source'] = 'Licence source';
+$string['source_help'] = 'The URL (with http:// or https:// prefix) where the licence terms and conditions can be found.';
+$string['sourcerequirederror'] = 'You must enter a valid URL for licence source.';
+$string['version'] = 'Licence version';
+$string['versioncannotbefuture'] = 'Licence version cannot be set to a future date.';
+$string['version_help'] = 'Publication date of the licence version being utilised.';
+
diff --git a/admin/tool/licensemanager/settings.php b/admin/tool/licensemanager/settings.php
new file mode 100644 (file)
index 0000000..ffb61e7
--- /dev/null
@@ -0,0 +1,33 @@
+<?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/>.
+
+/**
+ * Settings page.
+ *
+ * @package   tool_licensemanager
+ * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($hassiteconfig) {
+    $temp = new admin_externalpage('licensemanager',
+        get_string('licensemanager', 'tool_licensemanager'),
+        \tool_licensemanager\helper::get_licensemanager_url());
+
+    $ADMIN->add('license', $temp);
+}
diff --git a/admin/tool/licensemanager/tests/behat/delete_license.feature b/admin/tool/licensemanager/tests/behat/delete_license.feature
new file mode 100644 (file)
index 0000000..9bab27b
--- /dev/null
@@ -0,0 +1,28 @@
+@tool @tool_licensemanager
+Feature: Delete custom licenses
+  In order to manage custom licenses
+  As an admin
+  I need to be able to delete custom licenses but not standard Moodle licenses
+
+  @javascript
+  Scenario: I can delete a custom license
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    And I click on "Create licence" "link"
+    And I set the following fields to these values:
+    | shortname      | MIT                                 |
+    | fullname       | MIT Licence                         |
+    | source         | https://opensource.org/licenses/MIT |
+    | version[day]   | 1                                   |
+    | version[month] | March                               |
+    | version[year]  | 2019                                |
+    And I press "Save changes"
+    And I click on "Delete" "icon" in the "MIT" "table_row"
+    When I click on "Save changes" "button" in the "Delete licence" "dialogue"
+    Then I should not see "MIT Licence" in the "manage-licenses" "table"
+
+  Scenario: I cannot delete a standard license
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    Then I should see "Licence not specified" in the "unknown" "table_row"
+    And I should not see "Delete" in the "unknown" "table_row"
diff --git a/admin/tool/licensemanager/tests/behat/edit_license.feature b/admin/tool/licensemanager/tests/behat/edit_license.feature
new file mode 100644 (file)
index 0000000..d4d73ff
--- /dev/null
@@ -0,0 +1,79 @@
+@tool @tool_licensemanager
+Feature: Custom licences
+  In order to use custom licences
+  As an admin
+  I need to be able to add custom licences
+
+  Scenario: I am able to create custom licences
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    And I click on "Create licence" "link"
+    And I set the following fields to these values:
+      | shortname      | MIT                                 |
+      | fullname       | MIT Licence                         |
+      | source         | https://opensource.org/licenses/MIT |
+      | version[day]   | 1                                   |
+      | version[month] | January                             |
+      | version[year]  | 2020                                |
+    When I press "Save changes"
+    Then I should see "Licence manager"
+    And I should see "MIT Licence" in the "MIT" "table_row"
+    And I should see "https://opensource.org/licenses/MIT" in the "MIT" "table_row"
+
+  Scenario: I am only be able to make custom license with a valid url source (including scheme).
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    And I click on "Create licence" "link"
+    And I set the following fields to these values:
+      | shortname      | MIT                                 |
+      | fullname       | MIT Licence                         |
+      | source         | opensource.org/licenses/MIT         |
+      | version[day]   | 1                                   |
+      | version[month] | January                             |
+      | version[year]  | 2020                                |
+    When I press "Save changes"
+    Then I should see "Invalid source URL"
+    And I set the following fields to these values:
+      | source         | mailto:tomdickman@catalyst-au.net   |
+    And I press "Save changes"
+    And I should see "Invalid source URL"
+    And I set the following fields to these values:
+      | source         | https://opensource.org/licenses/MIT |
+    And I press "Save changes"
+    And I should see "Licence manager"
+    And I should see "MIT Licence" in the "MIT" "table_row"
+    And I should see "https://opensource.org/licenses/MIT" in the "MIT" "table_row"
+
+  Scenario: Custom license version format must be YYYYMMDD00
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    And I click on "Create licence" "link"
+    And I set the following fields to these values:
+      | shortname      | MIT                                 |
+      | fullname       | MIT Licence                         |
+      | source         | https://opensource.org/licenses/MIT |
+      | version[day]   | 1                                   |
+      | version[month] | March                               |
+      | version[year]  | 2019                                |
+    When I press "Save changes"
+    Then I should see "Licence manager"
+    And I should see "2019030100" in the "MIT" "table_row"
+
+  @javascript
+  Scenario: Custom license short name should not be editable after first creation
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    And I click on "Create licence" "link"
+    And I set the following fields to these values:
+      | shortname      | MIT                                 |
+      | fullname       | MIT Licence                         |
+      | source         | https://opensource.org/licenses/MIT |
+      | version[day]   | 1                                   |
+      | version[month] | March                               |
+      | version[year]  | 2019                                |
+    And I press "Save changes"
+    And I should see "Licence manager"
+    And I should see "MIT Licence" in the "MIT" "table_row"
+    When I click on "Edit" "icon" in the "MIT" "table_row"
+    Then I should see "Edit licence"
+    And the "shortname" "field" should be disabled
diff --git a/admin/tool/licensemanager/tests/behat/license_manager.feature b/admin/tool/licensemanager/tests/behat/license_manager.feature
new file mode 100644 (file)
index 0000000..2977d12
--- /dev/null
@@ -0,0 +1,35 @@
+@tool @tool_licensemanager
+Feature: License manager
+  In order to manage licenses
+  As an admin
+  I need to be able to view and alter licence preferences in the license manager.
+
+  Scenario: I should be able to see the default Moodle licences.
+    Given I log in as "admin"
+    When I navigate to "Licence > Licence manager" in site administration
+    Then I should see "Licence not specified" in the "unknown" "table_row"
+    And I should see "All rights reserved" in the "allrightsreserved" "table_row"
+    And I should see "Public domain" in the "public" "table_row"
+    And I should see "Creative Commons" in the "cc" "table_row"
+    And I should see "Creative Commons - NoDerivs" in the "cc-nd" "table_row"
+    And I should see "Creative Commons - No Commercial NoDerivs" in the "cc-nc-nd" "table_row"
+    And I should see "Creative Commons - No Commercial" in the "cc-nc" "table_row"
+    And I should see "Creative Commons - No Commercial ShareAlike" in the "cc-nc-sa" "table_row"
+    And I should see "Creative Commons - ShareAlike" in the "cc-sa" "table_row"
+
+  Scenario: I should be able to enable and disable licenses
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence settings" in site administration
+    When I set the field "Default site licence" to "Public domain"
+    And I press "Save changes"
+    And I navigate to "Licence > Licence manager" in site administration
+    Then "This is the site default license" "icon" should exist in the "public" "table_row"
+    And "Enable license" "icon" should not exist in the "public" "table_row"
+    And "This is the site default license" "icon" should not exist in the "cc" "table_row"
+    And I navigate to "Licence > Licence settings" in site administration
+    And I set the field "Default site licence" to "Creative Commons"
+    And I press "Save changes"
+    And I navigate to "Licence > Licence manager" in site administration
+    And "This is the site default license" "icon" should exist in the "cc" "table_row"
+    And "Enable license" "icon" should not exist in the "cc" "table_row"
+    And "This is the site default license" "icon" should not exist in the "public" "table_row"
diff --git a/admin/tool/licensemanager/tests/helper_test.php b/admin/tool/licensemanager/tests/helper_test.php
new file mode 100644 (file)
index 0000000..14c34c6
--- /dev/null
@@ -0,0 +1,44 @@
+<?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/>.
+
+/**
+ * Tests for tool_licensemanager helper class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2020 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests for tool_licensemanager helper class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2020 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @group      tool_licensemanager
+ */
+class helper_test extends advanced_testcase {
+
+    public function test_convert_version_to_epoch() {
+
+        $version = '2020010100';
+        $expected = strtotime(20200101);
+
+        $this->assertEquals($expected, \tool_licensemanager\helper::convert_version_to_epoch($version));
+    }
+}
diff --git a/admin/tool/licensemanager/tests/manager_test.php b/admin/tool/licensemanager/tests/manager_test.php
new file mode 100644 (file)
index 0000000..e86cf68
--- /dev/null
@@ -0,0 +1,199 @@
+<?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/>.
+
+/**
+ * Tests for tool_licensemanager manager class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2020 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/formslib.php');
+require_once($CFG->libdir . '/licenselib.php');
+
+/**
+ * Tests for tool_licensemanager manager class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2020 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @group      tool_licensemanager
+ */
+class manager_test extends advanced_testcase {
+
+    /**
+     * Test editing a license.
+     */
+    public function test_edit_existing_license() {
+        $this->resetAfterTest();
+
+        // Create initial custom license to edit.
+        $testlicense = new stdClass();
+        $testlicense->shortname = 'my-lic';
+        $testlicense->fullname = 'My License';
+        $testlicense->source = 'https://fakeurl.net';
+        $testlicense->version = date('Ymd', time()) . '00';
+        $testlicense->custom = license_manager::CUSTOM_LICENSE;
+
+        license_manager::save($testlicense);
+        license_manager::enable($testlicense->shortname);
+
+        $manager = new \tool_licensemanager\manager();
+
+        // Attempt to submit form data with altered details.
+        $formdata = [
+            'shortname' => 'new-value',
+            'fullname' => 'New License Name',
+            'source' => 'https://updatedfakeurl.net',
+            'version' => time()
+        ];
+
+        // Attempt to submit form data with an altered shortname.
+        \tool_licensemanager\form\edit_license::mock_submit($formdata);
+
+        // We're testing a private method, so we need to setup reflector magic.
+        $method = new ReflectionMethod('\tool_licensemanager\manager', 'edit');
+        $method->setAccessible(true); // Allow accessing of private method.
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_UPDATE, $testlicense->shortname);
+
+        // Should not create a new license when updating an existing license.
+        $this->assertEmpty(license_manager::get_license_by_shortname($formdata['shortname']));
+
+        $actual = license_manager::get_license_by_shortname('my-lic');
+        // Should not be able to update the shortname of the license.
+        $this->assertNotSame($formdata['shortname'], $actual->shortname);
+        // Should be able to update other details of the license.
+        $this->assertSame($formdata['fullname'], $actual->fullname);
+        $this->assertSame($formdata['source'], $actual->source);
+        $this->assertSame(date('Ymd', $formdata['version']) . '00', $actual->version);
+    }
+
+    public function test_edit_license_not_exists() {
+        $manager = new \tool_licensemanager\manager();
+
+        // We're testing a private method, so we need to setup reflector magic.
+        $method = new ReflectionMethod('\tool_licensemanager\manager', 'edit');
+        $method->setAccessible(true); // Allow accessing of private method.
+
+        // Attempt to update a license that doesn't exist.
+        $formdata = [
+            'shortname' => 'new-value',
+            'fullname' => 'New License Name',
+            'source' => 'https://updatedfakeurl.net',
+            'version' => time()
+        ];
+        \tool_licensemanager\form\edit_license::mock_submit($formdata);
+
+        // Should not be able to update a license with a shortname that doesn't exist.
+        $this->expectException('moodle_exception');
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_UPDATE, $formdata['shortname']);
+    }
+
+    public function test_edit_license_no_shortname() {
+        $manager = new \tool_licensemanager\manager();
+
+        // We're testing a private method, so we need to setup reflector magic.
+        $method = new ReflectionMethod('\tool_licensemanager\manager', 'edit');
+        $method->setAccessible(true); // Allow accessing of private method.
+
+        // Attempt to update a license without passing license shortname.
+        $formdata = [
+            'fullname' => 'New License Name',
+            'source' => 'https://updatedfakeurl.net',
+            'version' => time()
+        ];
+        \tool_licensemanager\form\edit_license::mock_submit($formdata);
+
+        // Should not be able to update empty license shortname.
+        $this->expectException('moodle_exception');
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_UPDATE, '');
+    }
+
+    /**
+     * Test creating a new license.
+     */
+    public function test_edit_create_license() {
+        $this->resetAfterTest();
+
+        $licensecount = count(license_manager::get_licenses());
+
+        $manager = new \tool_licensemanager\manager();
+
+        $formdata = [
+            'shortname' => 'new-value',
+            'fullname' => 'My License',
+            'source' => 'https://fakeurl.net',
+            'version' => time()
+        ];
+
+        // Attempt to submit form data for a new license.
+        \tool_licensemanager\form\edit_license::mock_submit($formdata);
+
+        // We're testing a private method, so we need to setup reflector magic.
+        $method = new ReflectionMethod('\tool_licensemanager\manager', 'edit');
+        $method->setAccessible(true); // Allow accessing of private method.
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_CREATE, $formdata['shortname']);
+
+        // Should create a new license in database.
+        $this->assertCount($licensecount + 1, license_manager::get_licenses());
+        $actual = license_manager::get_license_by_shortname($formdata['shortname']);
+        $this->assertSame($formdata['shortname'], $actual->shortname);
+        $this->assertSame($formdata['fullname'], $actual->fullname);
+        $this->assertSame($formdata['source'], $actual->source);
+        $this->assertSame(date('Ymd', $formdata['version']) . '00', $actual->version);
+
+        // Attempt to submit form data for a duplicate license.
+        \tool_licensemanager\form\edit_license::mock_submit($formdata);
+
+        // Should not be able to create duplicate licenses.
+        $this->expectException('moodle_exception');
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_CREATE, $formdata['shortname']);
+    }
+
+    /**
+     * Test changing the order of licenses.
+     */
+    public function test_change_license_order() {
+        $this->resetAfterTest();
+
+        $licenseorder = array_keys(license_manager::get_licenses());
+        $initialposition = array_search('cc-nc', $licenseorder);
+
+        $manager = new tool_licensemanager\manager();
+
+        // We're testing a private method, so we need to setup reflector magic.
+        $method = new ReflectionMethod('\tool_licensemanager\manager', 'change_license_order');
+        $method->setAccessible(true); // Allow accessing of private method.
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_MOVE_UP, 'cc-nc');
+
+        $licenseorder = array_keys(license_manager::get_licenses());
+        $newposition = array_search('cc-nc', $licenseorder);
+
+        $this->assertLessThan($initialposition, $newposition);
+
+        $initialposition = array_search('allrightsreserved', $licenseorder);
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_MOVE_DOWN, 'allrightsreserved');
+        $licenseorder = array_keys(license_manager::get_licenses());
+        $newposition = array_search('cc-nc', $licenseorder);
+
+        $this->assertGreaterThan($initialposition, $newposition);
+    }
+
+}
diff --git a/admin/tool/licensemanager/version.php b/admin/tool/licensemanager/version.php
new file mode 100644 (file)
index 0000000..faf9c51
--- /dev/null
@@ -0,0 +1,31 @@
+<?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/>.
+
+/**
+ * Version details for component 'tool_licensemanager'.
+ *
+ * @package    tool_licensemanager
+ * @copyright  Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2020050600;
+$plugin->requires  = 2020050200;         // Requires this Moodle version.
+$plugin->component = 'tool_licensemanager';
+
+$plugin->maturity = MATURITY_STABLE;
index 6895e75..6b16a51 100644 (file)
Binary files a/admin/tool/usertours/amd/build/usertours.min.js and b/admin/tool/usertours/amd/build/usertours.min.js differ
index ebfcda0..9f614ad 100644 (file)
Binary files a/admin/tool/usertours/amd/build/usertours.min.js.map and b/admin/tool/usertours/amd/build/usertours.min.js.map differ
index 70bd057..4bb1050 100644 (file)
@@ -69,6 +69,11 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
                 templates.render('tool_usertours/tourstep', {})
             )
             .then(function(response, template) {
+                // If we don't have any tour config (because it doesn't need showing for the current user), return early.
+                if (!response.hasOwnProperty('tourconfig')) {
+                    return;
+                }
+
                 return usertours.startBootstrapTour(tourId, template[0], response.tourconfig);
             })
             .always(function() {
index d24c684..c16c5a0 100644 (file)
@@ -70,9 +70,9 @@ class tour extends external_api {
 
         \tool_usertours\event\tour_started::create([
             'contextid' => $context->id,
-            'objectid'  => $tourid,
+            'objectid'  => $tour->get_id(),
             'other'     => [
-                'pageurl' => $pageurl,
+                'pageurl' => $params['pageurl'],
             ],
         ])->trigger();
 
@@ -104,7 +104,7 @@ class tour extends external_api {
             'tourconfig'    => new external_single_structure([
                 'name'      => new external_value(PARAM_RAW, 'Tour Name'),
                 'steps'     => new external_multiple_structure(self::step_structure_returns()),
-            ])
+            ], 'Tour config', VALUE_OPTIONAL)
         ]);
     }
 
index 2ce495b..3d86e56 100644 (file)
@@ -210,7 +210,7 @@ These actions only affect your view.
 
 You can also choose to display the courses in a list, or with summary information, or the default \'card\' view.';
 $string['tour3_title_displayoptions'] = 'Display options';
-$string['tour3_content_displayoptions'] = 'Courses may be sorted by course name or by last access date.
+$string['tour3_content_displayoptions'] = 'Courses may be sorted by course name, course short name or last access date.
 
 You can also choose to display the courses in a list, with summary information, or the default \'card\' view.';
 
index c123bd4..cf5ddba 100644 (file)
@@ -1,4 +1,9 @@
 This files describes API changes in the tool_usertours code.
 
+=== 3.9 ===
+* The `tourconfig` property returned by the `tool_usertours_fetch_and_start_tour`
+  external method is now optional, and will be omitted if the tour shouldn't be
+  shown to the current user
+
 === 3.5 ===
 * Third party library Popper.js was moved from this plugin into core (core/popper)
index 9064a0b..816a253 100644 (file)
@@ -158,5 +158,5 @@ Feature: Manage analytics models
   Scenario: Delete model
     When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
     And I choose "Delete" in the open action menu
-    And I click on "Delete" "button" in the "Confirm" "dialogue"
+    And I click on "Delete" "button" in the "Delete" "dialogue"
     Then I should not see "Students at risk of not meeting the course completion conditions"
index dcb2b06..0220640 100644 (file)
@@ -76,9 +76,21 @@ class auth_email_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals(print_password_policy(), $result['passwordpolicy']);
         $this->assertNotContains('recaptchachallengehash', $result);
         $this->assertNotContains('recaptchachallengeimage', $result);
-        $this->assertCount(2, $result['profilefields']);
-        $this->assertEquals('text', $result['profilefields'][0]['datatype']);
-        $this->assertEquals('textarea', $result['profilefields'][1]['datatype']);
+
+        // Whip up a array with named entries to easily check against.
+        $namedarray = array();
+        foreach ($result['profilefields'] as $key => $value) {
+            $namedarray[$value['shortname']] = array(
+                'datatype' => $value['datatype']
+            );
+        }
+
+        // Just check if we have the fields from this test. If a plugin adds fields we'll let it slide.
+        $this->assertArrayHasKey('frogname', $namedarray);
+        $this->assertArrayHasKey('sometext', $namedarray);
+
+        $this->assertEquals('text', $namedarray['frogname']['datatype']);
+        $this->assertEquals('textarea', $namedarray['sometext']['datatype']);
     }
 
     public function test_signup_user() {
index 546e378..622047c 100644 (file)
@@ -116,7 +116,7 @@ Feature: availability_grade
     And I click on "Grade" "link" in the "s@example.com" "table_row"
     And I set the field "Grade out of 100" to "40"
     And I click on "Save changes" "button"
-    And I press "Ok"
+    And I press "OK"
     And I click on "Edit settings" "link"
 
     # Log back in as student.
index 7520aa3..49bd8a1 100644 (file)
@@ -175,6 +175,20 @@ abstract class restore_qtype_plugin extends restore_plugin {
                 }
             }
 
+            $rules = restore_course_task::define_decode_rules();
+            $rulesactivity = restore_quiz_activity_task::define_decode_rules();
+            $rules = array_merge($rules, $rulesactivity);
+
+            $decoder = $this->task->get_decoder();
+            foreach ($rules as $rule) {
+                $decoder->add_rule($rule);
+            }
+
+            $contentdecoded = $decoder->decode_content($data->answertext);
+            if ($contentdecoded) {
+                $data->answertext = $contentdecoded;
+            }
+
             if (!isset($this->questionanswercache[$data->answertext])) {
                 // If we haven't found the matching answer, something has gone really wrong, the question in the DB
                 // is missing answers, throw an exception.
diff --git a/backup/tests/quiz_restore_decode_links_test.php b/backup/tests/quiz_restore_decode_links_test.php
new file mode 100644 (file)
index 0000000..19a142d
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * Decode links quiz restore tests.
+ *
+ * @package    core_backup
+ * @copyright  2020 Ilya Tregubov <mattp@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Include all the needed stuff.
+global $CFG;
+require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+
+/**
+ * restore_decode tests (both rule and content)
+ */
+class restore_quiz_decode_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Test restore_decode_rule class
+     */
+    public function test_restore_quiz_decode_links() {
+        global $DB, $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course(
+            array('format' => 'topics', 'numsections' => 3,
+                'enablecompletion' => COMPLETION_ENABLED),
+            array('createsections' => true));
+        $quiz = $generator->create_module('quiz', array(
+            'course' => $course->id));
+
+        // Create questions.
+
+        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $context = context_course::instance($course->id);
+        $cat = $questiongenerator->create_question_category(array('contextid' => $context->id));
+        $question = $questiongenerator->create_question('multichoice', null, array('category' => $cat->id));
+
+        // Add to the quiz.
+        quiz_add_quiz_question($question->id, $quiz);
+
+        $questiondata = question_bank::load_question_data($question->id);
+
+        $firstanswer = array_shift($questiondata->options->answers);
+        $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/course/view.php?id=' . $course->id,
+            ['id' => $firstanswer->id]);
+
+        $secondanswer = array_shift($questiondata->options->answers);
+        $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid,
+            ['id' => $secondanswer->id]);
+
+        $thirdanswer = array_shift($questiondata->options->answers);
+        $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/grade/report/index.php?id=' . $quiz->cmid,
+            ['id' => $thirdanswer->id]);
+
+        $fourthanswer = array_shift($questiondata->options->answers);
+        $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/mod/quiz/index.php?id=' . $quiz->cmid,
+            ['id' => $fourthanswer->id]);
+
+        $newcm = duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid));
+
+        $sql = "SELECT qa.answer
+                  FROM {quiz} q
+             LEFT JOIN {quiz_slots} qs ON qs.quizid = q.id
+             LEFT JOIN {question_answers} qa ON qa.question = qs.questionid
+                 WHERE q.id = :quizid";
+        $params = array('quizid' => $newcm->instance);
+        $answers = $DB->get_fieldset_sql($sql, $params);
+        $this->assertEquals($CFG->wwwroot . '/course/view.php?id=' . $course->id, $answers[0]);
+        $this->assertEquals($CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, $answers[1]);
+        $this->assertEquals($CFG->wwwroot . '/grade/report/index.php?id=' . $quiz->cmid, $answers[2]);
+        $this->assertEquals($CFG->wwwroot . '/mod/quiz/index.php?id=' . $quiz->cmid, $answers[3]);
+    }
+}
diff --git a/badges/amd/build/backpackactions.min.js b/badges/amd/build/backpackactions.min.js
new file mode 100644 (file)
index 0000000..659e07c
Binary files /dev/null and b/badges/amd/build/backpackactions.min.js differ
diff --git a/badges/amd/build/backpackactions.min.js.map b/badges/amd/build/backpackactions.min.js.map
new file mode 100644 (file)
index 0000000..52bdc23
Binary files /dev/null and b/badges/amd/build/backpackactions.min.js.map differ
diff --git a/badges/amd/build/selectors.min.js b/badges/amd/build/selectors.min.js
new file mode 100644 (file)
index 0000000..dbe7b36
Binary files /dev/null and b/badges/amd/build/selectors.min.js differ
diff --git a/badges/amd/build/selectors.min.js.map b/badges/amd/build/selectors.min.js.map
new file mode 100644 (file)
index 0000000..856297c
Binary files /dev/null and b/badges/amd/build/selectors.min.js.map differ
diff --git a/badges/amd/src/backpackactions.js b/badges/amd/src/backpackactions.js
new file mode 100644 (file)
index 0000000..5730b84
--- /dev/null
@@ -0,0 +1,89 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Action methods related to backpacks.
+ *
+ * @module     core_badges/backpackactions
+ * @package    core_badges
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+import selectors from 'core_badges/selectors';
+import {get_string as getString} from 'core/str';
+import Pending from 'core/pending';
+import ModalFactory from 'core/modal_factory';
+import ModalEvents from 'core/modal_events';
+import Config from 'core/config';
+
+/**
+ * Set up the actions.
+ *
+ * @method init
+ */
+export const init = () => {
+    const pendingPromise = new Pending();
+
+    const root = $(selectors.elements.main);
+    registerListenerEvents(root);
+
+    pendingPromise.resolve();
+};
+
+/**
+ * Register backpack related event listeners.
+ *
+ * @method registerListenerEvents
+ * @param {Object} root The root element.
+ */
+const registerListenerEvents = (root) => {
+
+    root.on('click', selectors.actions.deletebackpack, async(e) => {
+        e.preventDefault();
+
+        const link = $(e.currentTarget);
+        const modal = await buildModal(link);
+
+        displayModal(modal, link);
+    });
+};
+
+const buildModal = async(link) => {
+
+    const backpackurl = link.closest(selectors.elements.backpackurl).attr('data-backpackurl');
+
+    return ModalFactory.create({
+        title: await getString('delexternalbackpack', 'core_badges'),
+        body: await getString('delexternalbackpackconfirm', 'core_badges', backpackurl),
+        type: ModalFactory.types.SAVE_CANCEL,
+    });
+
+};
+
+const displayModal = async(modal, link) => {
+    modal.setSaveButtonText(await getString('delete', 'core'));
+
+    modal.getRoot().on(ModalEvents.save, function() {
+        window.location.href = link.attr('href') + '&sesskey=' + Config.sesskey + '&confirm=1';
+    });
+
+    modal.getRoot().on(ModalEvents.hidden, function() {
+        modal.destroy();
+    });
+
+    modal.show();
+};
diff --git a/badges/amd/src/selectors.js b/badges/amd/src/selectors.js
new file mode 100644 (file)
index 0000000..fea6dfb
--- /dev/null
@@ -0,0 +1,46 @@
+// 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/>.
+
+/**
+ * Define all of the selectors we will be using on the backpack interface.
+ *
+ * @module     core_badges/selectors
+ * @package    core_badges
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * A small helper function to build queryable data selectors.
+ *
+ * @method getDataSelector
+ * @param {String} name
+ * @param {String} value
+ * @return {string}
+ */
+const getDataSelector = (name, value) => {
+    return `[data-${name}="${value}"]`;
+};
+
+export default {
+    actions: {
+        deletebackpack: getDataSelector('action', 'deletebackpack'),
+    },
+    elements: {
+        clearsearch: '.input-group-append .clear-icon',
+        main: '#backpacklist',
+        backpackurl: '[data-backpackurl]',
+    },
+};
index 341d511..ae63fd6 100644 (file)
@@ -34,6 +34,7 @@ $output = $PAGE->get_renderer('core', 'badges');
 
 $id = optional_param('id', 0, PARAM_INT);
 $action = optional_param('action', '', PARAM_ALPHA);
+$confirm = optional_param('confirm', 1, PARAM_BOOL);
 
 $PAGE->set_pagelayout('admin');
 $url = new moodle_url('/badges/backpacks.php');
@@ -45,6 +46,18 @@ if (empty($CFG->badges_allowexternalbackpack)) {
 $PAGE->set_url($url);
 $PAGE->set_title(get_string('managebackpacks', 'badges'));
 $PAGE->set_heading($SITE->fullname);
+
+$msg = '';
+$msgtype = 'error';
+if ($action == 'delete' && $confirm && confirm_sesskey()) {
+    if (badges_delete_site_backpack($id)) {
+        $msg = get_string('sitebackpackdeleted', 'badges');
+        $msgtype = 'notifysuccess';
+    } else {
+        $msg = get_string('sitebackpacknotdeleted', 'badges');
+    }
+}
+
 if ($action == 'edit') {
     $backpack = null;
     if (!empty($id)) {
@@ -71,6 +84,9 @@ if ($action == 'edit') {
     echo $OUTPUT->header();
     echo $output->heading(get_string('managebackpacks', 'badges'));
 
+    if ($msg) {
+        echo $OUTPUT->notification($msg, $msgtype);
+    }
     $page = new \core_badges\output\external_backpacks_page($url);
     echo $output->render($page);
 }
index faa64a6..523403b 100644 (file)
@@ -93,9 +93,10 @@ class badge extends moodleform {
         $mform->setType('imagecaption', PARAM_TEXT);
         $mform->addHelpButton('imagecaption', 'imagecaption', 'badges');
 
-        $mform->addElement('header', 'issuerdetails', get_string('issuerdetails', 'badges'));
 
-        if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) {
+        if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
+            $mform->addElement('header', 'issuerdetails', get_string('issuerdetails', 'badges'));
+
             $mform->addElement('text', 'issuername', get_string('name'), array('size' => '70'));
             $mform->setType('issuername', PARAM_NOTAGS);
             $mform->addRule('issuername', null, 'required');
@@ -115,21 +116,6 @@ class badge extends moodleform {
             $url = parse_url($CFG->wwwroot);
             $mform->addElement('hidden', 'issuerurl', $url['scheme'] . '://' . $url['host']);
             $mform->setType('issuerurl', PARAM_URL);
-
-        } else {
-            $name = $CFG->badges_defaultissuername;
-            $mform->addElement('static', 'issuernamelabel', get_string('name'), $name);
-            $mform->addElement('hidden', 'issuername', $name);
-            $mform->setType('issuername', PARAM_NOTAGS);
-
-            $contact = $CFG->badges_defaultissuercontact;
-            $mform->addElement('static', 'issuercontactlabel', get_string('contact', 'badges'), $contact);
-            $mform->addElement('hidden', 'issuercontact', $contact);
-            $mform->setType('issuercontact', PARAM_RAW);
-
-            $url = parse_url($CFG->wwwroot);
-            $mform->addElement('hidden', 'issuerurl', $url['scheme'] . '://' . $url['host']);
-            $mform->setType('issuerurl', PARAM_URL);
         }
 
         $mform->addElement('header', 'issuancedetails', get_string('issuancedetails', 'badges'));
@@ -212,7 +198,7 @@ class badge extends moodleform {
         global $DB;
         $errors = parent::validation($data, $files);
 
-        if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) {
+        if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
             if (!empty($data['issuercontact']) && !validate_email($data['issuercontact'])) {
                 $errors['issuercontact'] = get_string('invalidemail');
             }
index 1bb90c5..f92febd 100644 (file)
@@ -48,40 +48,39 @@ class external_backpack extends \moodleform {
 
         if (isset($this->_customdata['externalbackpack'])) {
             $backpack = $this->_customdata['externalbackpack'];
-        } else {
-            throw new \coding_exception('backpack is required.');
         }
 
-        $url = $backpack->backpackapiurl;
+        $mform->addElement('hidden', 'action', 'edit');
+        $mform->setType('action', PARAM_ALPHA);
 
-        $mform->addElement('static', 'backpackapiurlinfo', get_string('backpackapiurl', 'core_badges'), $url);
+        if ($backpack) {
+            $mform->addElement('hidden', 'id', $backpack->id);
+            $mform->setType('id', PARAM_INTEGER);
+        }
 
-        $mform->addElement('hidden', 'backpackapiurl', $url);
+        $mform->addElement('text', 'backpackapiurl',  get_string('backpackapiurl', 'core_badges'));
         $mform->setType('backpackapiurl', PARAM_URL);
+        $mform->addRule('backpackapiurl', null, 'required', null, 'client');
+        $mform->addRule('backpackapiurl', get_string('maximumchars', '', 255), 'maxlength', 50, 'client');
 
-        $url = $backpack->backpackweburl;
-        $mform->addElement('static', 'backpackweburlinfo', get_string('backpackweburl', 'core_badges'), $url);
-        $mform->addElement('hidden', 'backpackweburl', $url);
+        $mform->addElement('text', 'backpackweburl', get_string('backpackweburl', 'core_badges'));
         $mform->setType('backpackweburl', PARAM_URL);
+        $mform->addRule('backpackweburl', null, 'required', null, 'client');
+        $mform->addRule('backpackweburl', get_string('maximumchars', '', 255), 'maxlength', 50, 'client');
 
-        $options = badges_get_badge_api_versions();
-        $label = $options[$backpack->apiversion];
-        $mform->addElement('static', 'apiversioninfo', get_string('apiversion', 'core_badges'), $label);
-        $mform->addElement('hidden', 'apiversion', $backpack->apiversion);
+        $apiversions = badges_get_badge_api_versions();
+        $mform->addElement('select', 'apiversion', get_string('apiversion', 'core_badges'), $apiversions);
         $mform->setType('apiversion', PARAM_RAW);
-
-        $mform->addElement('hidden', 'id', $backpack->id);
-        $mform->setType('id', PARAM_INTEGER);
-
-        $mform->addElement('hidden', 'action', 'edit');
-        $mform->setType('action', PARAM_ALPHA);
+        $mform->setDefault('apiversion', OPEN_BADGES_V2P1);
+        $mform->addRule('apiversion', null, 'required', null, 'client');
 
         $issuername = $CFG->badges_defaultissuername;
         $mform->addElement('static', 'issuerinfo', get_string('defaultissuername', 'core_badges'), $issuername);
 
         $issuercontact = $CFG->badges_defaultissuercontact;
         $mform->addElement('static', 'issuerinfo', get_string('defaultissuercontact', 'core_badges'), $issuercontact);
-        if ($backpack->apiversion != OPEN_BADGES_V2P1) {
+
+        if ($backpack && $backpack->apiversion != OPEN_BADGES_V2P1) {
             $mform->addElement('passwordunmask', 'password', get_string('defaultissuerpassword', 'core_badges'));
             $mform->setType('password', PARAM_RAW);
             $mform->addHelpButton('password', 'defaultissuerpassword', 'badges');
@@ -91,7 +90,9 @@ class external_backpack extends \moodleform {
             $mform->addElement('select', 'oauth2_issuerid', get_string('oauth2issuer', 'core_badges'), $oauth2options);
             $mform->setType('oauth2_issuerid', PARAM_INT);
         }
-        $this->set_data($backpack);
+        if ($backpack) {
+            $this->set_data($backpack);
+        }
 
         // Disable short forms.
         $mform->setDisableShortforms();
@@ -99,4 +100,24 @@ class external_backpack extends \moodleform {
         $this->add_action_buttons();
     }
 
+    /**
+     * Validate the data from the form.
+     *
+     * @param  array $data form data
+     * @param  array $files form files
+     * @return array An array of error messages.
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        // Ensure backpackapiurl and  are valid URLs.
+        if (!empty($data['backpackapiurl']) && !preg_match('@^https?://.+@', $data['backpackapiurl'])) {
+            $errors['backpackapiurl'] = get_string('invalidurl', 'badges');
+        }
+        if (!empty($data['backpackweburl']) && !preg_match('@^https?://.+@', $data['backpackweburl'])) {
+            $errors['backpackweburl'] = get_string('invalidurl', 'badges');
+        }
+
+        return $errors;
+    }
 }
diff --git a/badges/classes/helper.php b/badges/classes/helper.php
new file mode 100644 (file)
index 0000000..b744c1e
--- /dev/null
@@ -0,0 +1,75 @@
+<?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/>.
+
+/**
+ * Badge helper library.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_badges;
+
+/**
+ * Badge helper library.
+ *
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+
+    /**
+     * Create a backpack.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    public static function create_fake_backpack(array $params = []) {
+        global $DB;
+
+        $record = (object) array_merge([
+            'userid' => null,
+            'email' => 'test@example.com',
+            'backpackuid' => -1,
+            'autosync' => 0,
+            'password' => '',
+            'externalbackpackid' => 12345,
+        ], $params);
+        $record->id = $DB->insert_record('badge_backpack', $record);
+
+        return $record;
+    }
+
+    /**
+     * Create a user backpack collection.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    public static function create_fake_backpack_collection(array $params = []) {
+        global $DB;
+
+        $record = (object) array_merge([
+            'backpackid' => 12345,
+            'collectionid' => -1,
+            'entityid' => random_string(20),
+        ], $params);
+        $record->id = $DB->insert_record('badge_external', $record);
+
+        return $record;
+    }
+}
index 839a731..f9258b3 100644 (file)
@@ -56,6 +56,10 @@ class external_backpacks_page implements \renderable {
      * @return stdClass
      */
     public function export_for_template(\renderer_base $output) {
+        global $CFG, $PAGE;
+
+        $PAGE->requires->js_call_amd('core_badges/backpackactions', 'init');
+
         $data = new \stdClass();
         $data->baseurl = $this->url;
         $data->backpacks = array();
@@ -68,6 +72,8 @@ class external_backpacks_page implements \renderable {
             } else {
                 $backpack->canedit = false;
             }
+            $backpack->iscurrent = ($backpack->id == $CFG->badges_site_backpack);
+
             $data->backpacks[] = $backpack;
         }
         $data->warning = badges_verify_site_backpack();
index 9087b2d..07785f3 100644 (file)
@@ -87,7 +87,7 @@ if ($form->is_cancelled()) {
     $fordb->usercreated = $USER->id;
     $fordb->usermodified = $USER->id;
 
-    if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) {
+    if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
         $fordb->issuername = $data->issuername;
         $fordb->issuerurl = $data->issuerurl;
         $fordb->issuercontact = $data->issuercontact;
index 2d84733..aab0f2f 100644 (file)
         "warning": "<span class='text-warning'>Could not login</span>"
     }
 }}
-<table class="generaltable fullwidth">
+
+<form action="{{baseurl}}" method="get" id="createbackpack">
+   <input type="hidden" name="action" value="edit"/>
+   <button type="submit" class="btn btn-secondary">{{#str}}newbackpack, core_badges{{/str}}</button>
+</form>
+
+<table class="generaltable fullwidth" id="backpacklist">
     <caption>{{#str}}listbackpacks, core_badges{{/str}}</caption>
     <thead>
         <tr>
     </thead>
     <tbody>
         {{#backpacks}}
-        <tr>
+        <tr data-backpackurl="{{{backpackweburl}}}">
             <td> {{{backpackweburl}}} </td>
             <td> {{#sitebackpack}}Yes{{/sitebackpack}} </td>
             <td>
             {{#canedit}}
-                <a href="{{baseurl}}?id={{id}}&action=edit">
-                    {{#str}}editsettings, core_badges{{/str}}
-                </a>
+                <a href="{{baseurl}}?id={{id}}&action=edit">{{#pix}}t/edit, core,{{#str}}editsettings{{/str}}{{/pix}}</a>
             {{/canedit}}
+            {{^iscurrent}}
+                <a href="{{baseurl}}?id={{id}}&action=delete" role="button" data-action="deletebackpack">
+                    {{#pix}}t/delete, core,{{#str}}delete{{/str}}{{/pix}}
+                </a>
+            {{/iscurrent}}
             </td>
         </tr>
         {{/backpacks}}
index d3d3541..596c67e 100644 (file)
@@ -30,6 +30,8 @@ global $CFG;
 require_once($CFG->libdir . '/badgeslib.php');
 require_once($CFG->dirroot . '/badges/lib.php');
 
+use core_badges\helper;
+
 class core_badges_badgeslib_testcase extends advanced_testcase {
     protected $badgeid;
     protected $course;
@@ -855,4 +857,58 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $badge->delete_alignment($alignments1[$newid2]->id);
         $this->assertCount(1, $badge->get_alignments());
     }
+
+    /**
+     * Test badges_delete_site_backpack().
+     *
+     */
+    public function test_badges_delete_site_backpack(): void {
+        global $DB;
+
+        $this->setAdminUser();
+
+        // Create one backpack.
+        $total = $DB->count_records('badge_external_backpack');
+        $this->assertEquals(1, $total);
+
+        $data = new \stdClass();
+        $data->apiversion = OPEN_BADGES_V2P1;
+        $data->backpackapiurl = 'https://dc.imsglobal.org/obchost/ims/ob/v2p1';
+        $data->backpackweburl = 'https://dc.imsglobal.org';
+        badges_create_site_backpack($data);
+        $backpack = $DB->get_record('badge_external_backpack', ['backpackweburl' => $data->backpackweburl]);
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        // User1 is connected to the backpack to be removed and has 2 collections.
+        $backpackuser1 = helper::create_fake_backpack(['userid' => $user1->id, 'externalbackpackid' => $backpack->id]);
+        helper::create_fake_backpack_collection(['backpackid' => $backpackuser1->id]);
+        helper::create_fake_backpack_collection(['backpackid' => $backpackuser1->id]);
+        // User2 is connected to a different backpack and has 1 collection.
+        $backpackuser2 = helper::create_fake_backpack(['userid' => $user2->id]);
+        helper::create_fake_backpack_collection(['backpackid' => $backpackuser2->id]);
+
+        $total = $DB->count_records('badge_external_backpack');
+        $this->assertEquals(2, $total);
+        $total = $DB->count_records('badge_backpack');
+        $this->assertEquals(2, $total);
+        $total = $DB->count_records('badge_external');
+        $this->assertEquals(3, $total);
+
+        // Remove the backpack created previously.
+        $result = badges_delete_site_backpack($backpack->id);
+        $this->assertTrue($result);
+
+        $total = $DB->count_records('badge_external_backpack');
+        $this->assertEquals(1, $total);
+
+        $total = $DB->count_records('badge_backpack');
+        $this->assertEquals(1, $total);
+
+        $total = $DB->count_records('badge_external');
+        $this->assertEquals(1, $total);
+
+        // Try to remove an non-existent backpack.
+        $result = badges_delete_site_backpack($backpack->id);
+        $this->assertFalse($result);
+    }
 }
index 81ecdb7..09db662 100644 (file)
@@ -8,18 +8,6 @@ Feature: Add badges to the system
     Given I am on homepage
     And I log in as "admin"
 
-  @javascript
-  Scenario: Setting badges settings
-    Given I navigate to "Badges > Badges settings" in site administration
-    And I set the field "Badge issuer name" to "Test Badge Site"
-    And I set the field "Badge issuer email address" to "testuser@example.com"
-    And I press "Save changes"
-    And I follow "Badges"
-    When I follow "Add a new badge"
-    And I press "Issuer details"
-    Then I should see "testuser@example.com"
-    And I should see "Test Badge Site"
-
   @javascript
   Scenario: Accessing the badges
     And I press "Customise this page"
@@ -31,7 +19,11 @@ Feature: Add badges to the system
 
   @javascript @_file_upload
   Scenario: Add a badge
-    Given I navigate to "Badges > Add a new badge" in site administration
+    Given I navigate to "Badges > Badges settings" in site administration
+    And I set the field "Badge issuer name" to "Test Badge Site"
+    And I set the field "Badge issuer email address" to "testuser@example.com"
+    And I press "Save changes"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Test badge with 'apostrophe' and other friends (<>&@#) |
       | Version | v1 |
@@ -47,6 +39,11 @@ Feature: Add badges to the system
     And I should see "Related badges (0)"
     And I should see "Alignments (0)"
     And I should not see "Create badge"
+    And I should not see "Issuer details"
+    And I follow "Overview"
+    And I should see "Issuer details"
+    And I should see "Test Badge Site"
+    And I should see "testuser@example.com"
     And I follow "Manage badges"
     And I should see "Number of badges available: 1"
     And I should not see "There are no badges available."
index 7c66ac7..67376f0 100644 (file)
@@ -11,6 +11,11 @@ Feature: Backpack badges
     And the following "users" exist:
       | username | firstname | lastname | email                |
       | student1 | Student   | 1        | student1@example.com |
+    And I log in as "admin"
+    And I navigate to "Badges > Badges settings" in site administration
+    And I set the field "Badge issuer name" to "Test Badge Site"
+    And I set the field "Badge issuer email address" to "testuser@example.com"
+    And I log out
 
   @javascript
   Scenario: Verify backback settings
@@ -29,8 +34,6 @@ Feature: Backpack badges
       | Description   | Test badge description     |
       | Image author  | http://author.example.com  |
       | Image caption | Test caption image         |
-      | issuername    | Test Badge Site            |
-      | issuercontact | testuser@example.com       |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -67,8 +70,6 @@ Feature: Backpack badges
       | Description    | Test badge description     |
       | Image author   | http://author.example.com  |
       | Image caption  | Test caption image         |
-      | issuername     | Test Badge Site            |
-      | issuercontact  | testuser@example.com       |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -92,3 +93,30 @@ Feature: Backpack badges
     And I follow "Manage badges"
     And I should see "Test badge verify backpack"
     And "Add to backpack" "link" should exist
+
+  @javascript
+  Scenario: Add a new site backpack
+    Given I am on homepage
+    And I log in as "admin"
+    And I navigate to "Badges > Manage backpacks" in site administration
+    When I press "Add a new backpack"
+    And I set the field "backpackapiurl" to "http://backpackapiurl.cat"
+    And I set the field "backpackweburl" to "aaa"
+    And I press "Save changes"
+    And I should see "Invalid URL"
+    And I set the field "backpackweburl" to "http://backpackweburl.cat"
+    And I press "Save changes"
+    Then I should see "http://backpackweburl.cat"
+    And "Delete" "button" should exist
+
+  @javascript
+  Scenario: Remove a site backpack
+    Given I am on homepage
+    And I log in as "admin"
+    And I navigate to "Badges > Manage backpacks" in site administration
+    When I click on "Delete" "link" in the "https://dc.imsglobal.org" "table_row"
+    And I should see "Delete site backpack 'https://dc.imsglobal.org'?"
+    And I click on "Delete" "button" in the "Delete site backpack" "dialogue"
+    Then I should see "The site backpack has been deleted."
+    And I should not see "https://dc.imsglobal.org"
+    And "Delete" "button" should not exist
index fb75770..8067b4e 100644 (file)
@@ -33,6 +33,7 @@ use core_privacy\local\request\transform;
 use core_privacy\local\request\writer;
 use core_badges\privacy\provider;
 use core_privacy\local\request\approved_userlist;
+use core_badges\helper;
 
 require_once($CFG->libdir . '/badgeslib.php');
 
@@ -142,7 +143,7 @@ class core_badges_privacy_testcase extends provider_testcase {
         $b1 = $this->create_badge();
         $b2 = $this->create_badge(['type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]);
 
-        $this->create_backpack(['userid' => $u1->id]);
+        helper::create_fake_backpack(['userid' => $u1->id]);
         $this->create_manual_award(['recipientid' => $u2->id, 'badgeid' => $b1->id]);
         $this->create_issued(['badgeid' => $b2->id, 'userid' => $u3->id]);
 
@@ -182,8 +183,8 @@ class core_badges_privacy_testcase extends provider_testcase {
         $b2 = $this->create_badge(['usercreated' => $u2->id, 'usermodified' => $u1->id,
             'type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]);
 
-        $this->create_backpack(['userid' => $u1->id]);
-        $this->create_backpack(['userid' => $u2->id]);
+        helper::create_fake_backpack(['userid' => $u1->id]);
+        helper::create_fake_backpack(['userid' => $u2->id]);
         $this->create_manual_award(['recipientid' => $u1->id, 'badgeid' => $b1->id]);
         $this->create_manual_award(['recipientid' => $u2->id, 'badgeid' => $b1->id, 'issuerid' => $u1->id]);
         $this->create_issued(['badgeid' => $b2->id, 'userid' => $u1->id]);
@@ -240,8 +241,8 @@ class core_badges_privacy_testcase extends provider_testcase {
         $b2 = $this->create_badge(['usercreated' => $u2->id, 'usermodified' => $u1->id,
             'type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]);
 
-        $this->create_backpack(['userid' => $u1->id]);
-        $this->create_backpack(['userid' => $u2->id]);
+        helper::create_fake_backpack(['userid' => $u1->id]);
+        helper::create_fake_backpack(['userid' => $u2->id]);
         $this->create_manual_award(['recipientid' => $u1->id, 'badgeid' => $b1->id]);
         $this->create_manual_award(['recipientid' => $u2->id, 'badgeid' => $b1->id, 'issuerid' => $u1->id]);
         $this->create_issued(['badgeid' => $b2->id, 'userid' => $u1->id]);
@@ -317,14 +318,14 @@ class core_badges_privacy_testcase extends provider_testcase {
 
         // Create things for user 2, to check it's not exported it.
         $this->create_issued(['badgeid' => $b4->id, 'userid' => $u2->id]);
-        $this->create_backpack(['userid' => $u2->id, 'email' => $u2->email]);
+        helper::create_fake_backpack(['userid' => $u2->id, 'email' => $u2->email]);
         $this->create_manual_award(['badgeid' => $b1->id, 'recipientid' => $u2->id, 'issuerid' => $u3->id]);
 
         // Create a set of stuff for u1.
         $this->create_issued(['badgeid' => $b1->id, 'userid' => $u1->id, 'uniquehash' => 'yoohoo']);
         $this->create_manual_award(['badgeid' => $b2->id, 'recipientid' => $u1->id, 'issuerid' => $u3->id]);
         $b3crit->mark_complete($u1->id);
-        $this->create_backpack(['userid' => $u1->id, 'email' => $u1->email]);
+        helper::create_fake_backpack(['userid' => $u1->id, 'email' => $u1->email]);
 
         // Check u1.
         writer::reset();
@@ -482,7 +483,7 @@ class core_badges_privacy_testcase extends provider_testcase {
         $this->create_manual_award(['recipientid' => $user3->id, 'issuerid' => $user2->id, 'badgeid' => $badge1->id]);
         $this->create_manual_award(['recipientid' => $user1->id, 'issuerid' => $user2->id, 'badgeid' => $badge2->id]);
 
-        $this->create_backpack(['userid' => $user2->id]);
+        helper::create_fake_backpack(['userid' => $user2->id]);
         $this->create_issued(['badgeid' => $badge2->id, 'userid' => $user3->id]);
 
         $crit = $this->create_criteria_manual($badge1->id);
@@ -538,7 +539,7 @@ class core_badges_privacy_testcase extends provider_testcase {
         $this->create_manual_award(['recipientid' => $user3->id, 'issuerid' => $user2->id, 'badgeid' => $badge1->id]);
         $this->create_manual_award(['recipientid' => $user1->id, 'issuerid' => $user2->id, 'badgeid' => $badge2->id]);
 
-        $this->create_backpack(['userid' => $user2->id]);
+        helper::create_fake_backpack(['userid' => $user2->id]);
         $this->create_issued(['badgeid' => $badge2->id, 'userid' => $user3->id]);
 
         $crit = $this->create_criteria_manual($badge1->id);
@@ -708,26 +709,6 @@ class core_badges_privacy_testcase extends provider_testcase {
         return $record;
     }
 
-    /**
-     * Create a backpack.
-     *
-     * @param array $params Parameters.
-     * @return object
-     */
-    protected function create_backpack(array $params = []) {
-        global $DB;
-        $record = (object) array_merge([
-            'userid' => null,
-            'email' => 'test@example.com',
-            'backpackurl' => "http://here.there.com",
-            'backpackuid' => "12345",
-            'autosync' => 0,
-            'password' => '',
-        ], $params);
-        $record->id = $DB->insert_record('badge_backpack', $record);
-        return $record;
-    }
-
     /**
      * Create a criteria of type badge.
      *
index 96e18fb..93478ab 100644 (file)
@@ -164,6 +164,7 @@ class main implements renderable, templatable {
      * @throws \dml_exception
      */
     public function __construct($grouping, $sort, $view, $paging, $customfieldvalue = null) {
+        global $CFG;
         // Get plugin config.
         $config = get_config('block_myoverview');
 
@@ -185,7 +186,18 @@ class main implements renderable, templatable {
         $this->customfieldvalue = $customfieldvalue;
 
         // Check and remember the given sorting.
-        $this->sort = $sort ? $sort : BLOCK_MYOVERVIEW_SORTING_TITLE;
+        if ($sort) {
+            $this->sort = $sort;
+        } else if ($CFG->courselistshortnames) {
+            $this->sort = BLOCK_MYOVERVIEW_SORTING_SHORTNAME;
+        } else {
+            $this->sort = BLOCK_MYOVERVIEW_SORTING_TITLE;
+        }
+        // In case sorting remembered is shortname and display extended course names not checked,
+        // we should revert sorting to title.
+        if (!$CFG->courselistshortnames && $sort == BLOCK_MYOVERVIEW_SORTING_SHORTNAME) {
+            $this->sort = BLOCK_MYOVERVIEW_SORTING_TITLE;
+        }
 
         // Check and remember the given view.
         $this->view = $view ? $view : BLOCK_MYOVERVIEW_VIEW_CARD;
@@ -236,7 +248,6 @@ class main implements renderable, templatable {
         }
         unset ($displaygroupingselectors, $displaygroupingselectorscount);
     }
-
     /**
      * Determine the most sensible fallback grouping to use (in cases where the stored selection
      * is no longer available).
@@ -393,7 +404,7 @@ class main implements renderable, templatable {
      *
      */
     public function export_for_template(renderer_base $output) {
-        global $USER;
+        global $CFG, $USER;
 
         $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
 
@@ -422,12 +433,18 @@ class main implements renderable, templatable {
         }
         $preferences = $this->get_preferences_as_booleans();
         $availablelayouts = $this->get_formatted_available_layouts_for_export();
+        $sort = '';
+        if ($this->sort == BLOCK_MYOVERVIEW_SORTING_SHORTNAME) {
+            $sort = 'shortname';
+        } else {
+            $sort = $this->sort == BLOCK_MYOVERVIEW_SORTING_TITLE ? 'fullname' : 'ul.timeaccess desc';
+        }
 
         $defaultvariables = [
             'totalcoursecount' => count(enrol_get_all_users_courses($USER->id, true)),
             'nocoursesimg' => $nocoursesurl,
             'grouping' => $this->grouping,
-            'sort' => $this->sort == BLOCK_MYOVERVIEW_SORTING_TITLE ? 'fullname' : 'ul.timeaccess desc',
+            'sort' => $sort,
             // If the user preference display option is not available, default to first available layout.
             'view' => in_array($this->view, $this->layouts) ? $this->view : reset($this->layouts),
             'paging' => $this->paging,
@@ -447,6 +464,7 @@ class main implements renderable, templatable {
             'customfieldvalue' => $this->customfieldvalue,
             'customfieldvalues' => $customfieldvalues,
             'selectedcustomfield' => $selectedcustomfield,
+            'showsortbyshortname' => $CFG->courselistshortnames,
         ];
         return array_merge($defaultvariables, $preferences);
 
index 76f83fb..9763156 100644 (file)
@@ -44,6 +44,7 @@ $string['aria:list'] = 'Switch to list view';
 $string['aria:title'] = 'Sort courses by course name';
 $string['aria:past'] = 'Show past courses';
 $string['aria:removefromfavourites'] = 'Remove star for';
+$string['aria:shortname'] = 'Sort courses by course short name';
 $string['aria:summary'] = 'Switch to summary view';
 $string['aria:sortingdropdown'] = 'Sorting drop-down menu';
 $string['availablegroupings'] = 'Available filters';
@@ -73,6 +74,7 @@ $string['privacy:metadata:overviewviewpreference'] = 'The Course overview block
 $string['privacy:metadata:overviewgroupingpreference'] = 'The Course overview block grouping preference.';
 $string['privacy:metadata:overviewpagingpreference'] = 'The Course overview block paging preference.';
 $string['removefromfavourites'] = 'Unstar this course';
+$string['shortname'] = 'Short name';
 $string['summary'] = 'Summary';
 $string['title'] = 'Course name';
 $string['aria:hidecourse'] = 'Remove {$a} from view';
index 7af8cbd..7080d73 100644 (file)
@@ -47,6 +47,7 @@ define('BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY', -1);
  */
 define('BLOCK_MYOVERVIEW_SORTING_TITLE', 'title');
 define('BLOCK_MYOVERVIEW_SORTING_LASTACCESSED', 'lastaccessed');
+define('BLOCK_MYOVERVIEW_SORTING_SHORTNAME', 'shortname');
 
 /**
  * Constants for the user preferences view options
@@ -104,7 +105,8 @@ function block_myoverview_user_preferences() {
         'type' => PARAM_ALPHA,
         'choices' => array(
             BLOCK_MYOVERVIEW_SORTING_TITLE,
-            BLOCK_MYOVERVIEW_SORTING_LASTACCESSED
+            BLOCK_MYOVERVIEW_SORTING_LASTACCESSED,
+            BLOCK_MYOVERVIEW_SORTING_SHORTNAME
         )
     );
     $preferences['block_myoverview_user_view_preference'] = array(
index 3289ea5..55a425d 100644 (file)
@@ -33,6 +33,7 @@
             <span class="d-sm-inline-block" data-active-item-text>
                 {{#title}}{{#str}} title, block_myoverview {{/str}}{{/title}}
                 {{#lastaccessed}}{{#str}} lastaccessed, block_myoverview {{/str}}{{/lastaccessed}}
+                {{#shortname}}{{#str}} shortname, block_myoverview {{/str}}{{/shortname}}
             </span>
         </button>
         <ul class="dropdown-menu" data-show-active-item aria-labelledby="sortingdropdown">
                     {{#str}} title, block_myoverview {{/str}}
                 </a>
             </li>
+            {{#showsortbyshortname}}
+            <li>
+                <a class="dropdown-item {{#shortname}}active{{/shortname}}" href="#" data-filter="sort" data-pref="shortname" data-value="shortname" aria-label="{{#str}} aria:shortname, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                    {{#str}} shortname, block_myoverview {{/str}}
+                </a>
+            </li>
+             {{/showsortbyshortname}}
             <li>
                 <a class="dropdown-item {{#lastaccessed}}active{{/lastaccessed}}" href="#" data-filter="sort" data-pref="lastaccessed" data-value="ul.timeaccess desc" aria-label="{{#str}} aria:lastaccessed, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
                     {{#str}} lastaccessed, block_myoverview {{/str}}
@@ -48,4 +56,4 @@
             </li>
         </ul>
     </div>
-</div>
\ No newline at end of file
+</div>
index c4d527c..f32e7e8 100644 (file)
@@ -63,7 +63,7 @@
                         <div>{{{shortname}}}</div>
                         {{/showshortname}}
                     </div>
-                    <a href="{{viewurl}}" class="coursename">
+                    <a href="{{viewurl}}" class="aalink coursename">
                         {{> core_course/favouriteicon }}
                         <span class="sr-only">
                             {{#str}}aria:coursename, core_course{{/str}}
index 9f92990..02dd284 100644 (file)
@@ -73,7 +73,7 @@
                     {{/showshortname}}
                 </div>
                 <div class="d-flex mb-1">
-                    <a href="{{viewurl}}" class="coursename">
+                    <a href="{{viewurl}}" class="aalink coursename">
                         {{> core_course/favouriteicon }}
                         <span class="sr-only">
                             {{#str}}aria:coursename, core_course{{/str}}
index 72d2f2c..10a32fd 100644 (file)
@@ -182,6 +182,20 @@ Feature: The my overview block allows users to easily access their courses
     Then I should see "Last accessed" in the "Course overview" "block"
     And "[data-sort='ul.timeaccess desc']" "css_element" in the "Course overview" "block" should be visible
 
+  Scenario: Short name sort persistence
+    Given I log in as "student1"
+    When I click on "sortingdropdown" "button" in the "Course overview" "block"
+    Then I should not see "Short name" in the "Course overview" "block"
+    When the following config values are set as admin:
+      | config               | value |
+      | courselistshortnames | 1     |
+    And I reload the page
+    And I click on "sortingdropdown" "button" in the "Course overview" "block"
+    And I click on "Short name" "link" in the "Course overview" "block"
+    And I reload the page
+    Then I should see "Short name" in the "Course overview" "block"
+    And "[data-sort='shortname']" "css_element" in the "Course overview" "block" should be visible
+
   Scenario: View inprogress courses with hide persistent functionality
     Given I log in as "student1"
     And I click on "All (except removed from view)" "button" in the "Course overview" "block"
index 6dc9b94..90c549e 100644 (file)
@@ -72,6 +72,7 @@ class block_myoverview_privacy_testcase extends \core_privacy\tests\provider_tes
         return array(
             array('block_myoverview_user_sort_preference', 'lastaccessed', ''),
             array('block_myoverview_user_sort_preference', 'title', ''),
+            array('block_myoverview_user_sort_preference', 'shortname', ''),
             array('block_myoverview_user_grouping_preference', 'allincludinghidden', ''),
             array('block_myoverview_user_grouping_preference', 'all', ''),
             array('block_myoverview_user_grouping_preference', 'inprogress', ''),
@@ -104,4 +105,4 @@ class block_myoverview_privacy_testcase extends \core_privacy\tests\provider_tes
             $blockpreferences->{$name}->description
         );
     }
-}
\ No newline at end of file
+}
index bd815e6..ee1b61b 100644 (file)
@@ -267,9 +267,11 @@ class core_block_externallib_testcase extends externallib_advanced_testcase {
             $returnedblocks[] = $block['name'];
             // Check the configuration returned for this default block.
             if ($block['name'] == 'recentlyaccessedcourses') {
-                $this->assertEquals('displaycategories', $block['configs'][0]['name']);
-                $this->assertEquals(json_encode('0'), $block['configs'][0]['value']);
-                $this->assertEquals('plugin', $block['configs'][0]['type']);
+                // Convert config to associative array to avoid DB sorting randomness.
+                $config = array_column($block['configs'], null, 'name');
+                $this->assertArrayHasKey('displaycategories', $config);
+                $this->assertEquals(json_encode('0'), $config['displaycategories']['value']);
+                $this->assertEquals('plugin', $config['displaycategories']['type']);
             }
         }
         // Remove lp block.
index 0e7cb63..f69895d 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
index a0e67dc..9106713 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js.map and b/calendar/amd/build/view_manager.min.js.map differ
index e5edf31..37fca71 100644 (file)
@@ -162,7 +162,7 @@ export const refreshMonthContent = (root, year, month, courseId, categoryId, tar
             return Templates.replaceNode(target, html, js);
         })
         .then(() => {
-            document.querySelector('body').dispatchEvent(new Event(CalendarEvents.viewUpdated));
+            document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
             return;
         })
         .always(() => {
@@ -248,7 +248,7 @@ export const refreshDayContent = (root, year, month, day, courseId, categoryId,
             return Templates.replaceNode(target, html, js);
         })
         .then(() => {
-            document.querySelector('body').dispatchEvent(new Event(CalendarEvents.viewUpdated));
+            document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
             return;
         })
         .always(() => {
@@ -355,7 +355,7 @@ export const reloadCurrentUpcoming = (root, courseId = 0, categoryId = 0, target
             return Templates.replaceNode(target, html, js);
         })
         .then(() => {
-            document.querySelector('body').dispatchEvent(new Event(CalendarEvents.viewUpdated));
+            document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
             return;
         })
         .always(function() {
index ce7c76f..6bfc798 100644 (file)
@@ -31,7 +31,7 @@
     {
     }
 }}
-<div class="header d-flex flex-wrap">
+<div class="header d-flex flex-wrap p-1">
     {{> core_calendar/view_selector}}
     {{#filter_selector}}
         {{{filter_selector}}}
index 8772356..13ac37b 100644 (file)
@@ -49,7 +49,7 @@
 </div>
 {{#js}}
 require(['jquery'], function($) {
-    require(['theme_boost/popover'], function() {
+    require(['theme_boost/bootstrap/popover'], function() {
         var target = $("#calendar-day-popover-link-{{courseid}}-{{year}}-{{yday}}-{{uniqid}}");
         target.popover({
             content: function() {
index 11608cd..a88f65e 100644 (file)
@@ -72,7 +72,7 @@
                         data-new-event-timestamp="{{neweventtimestamp}}">
                         <div class="d-none d-md-block hidden-phone text-xs-center">
                             {{#hasevents}}
-                                <a data-action="view-day-link" href="#" class="day" aria-label="{{viewdaylinktitle}}"
+                                <a data-action="view-day-link" href="#" class="aalink day" aria-label="{{viewdaylinktitle}}"
                                     data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
                                     data-courseid="{{courseid}}" data-categoryid="{{categoryid}}"
                                     data-timestamp="{{timestamp}}">{{mday}}</a>
                         </div>
                         <div class="d-md-none hidden-desktop hidden-tablet">
                             {{#hasevents}}
-                                <a data-action="view-day-link" href="#" class="day" aria-label="{{viewdaylinktitle}}"
+                                <a data-action="view-day-link" href="#" class="day aalink" aria-label="{{viewdaylinktitle}}"
                                     data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
                                     data-courseid="{{courseid}}" data-categoryid="{{categoryid}}"
                                     data-timestamp="{{timestamp}}">{{mday}}</a>
index 3d6a6cd..2cd824c 100644 (file)
@@ -58,7 +58,7 @@ Feature: Restrict activity availability through grade conditions
     And I set the following fields to these values:
       | Grade | 21 |
     And I press "Save changes"
-    And I press "Ok"
+    And I press "OK"
     And I follow "Edit settings"
     And I log out
     And I log in as "student1"
index 63d8422..6785e90 100644 (file)
@@ -91,7 +91,7 @@ Feature: Restrict sections availability through completion or grade conditions
     And I set the following fields to these values:
       | Grade | 21 |
     And I press "Save changes"
-    And I press "Ok"
+    And I press "OK"
     And I follow "Edit settings"
     And I log out
     And I log in as "student1"
index 96eb509..995e0df 100644 (file)
@@ -981,8 +981,8 @@ $CFG->admin = 'admin';
 // Example:
 //   define('BEHAT_DISABLE_HISTOGRAM', true);
 //
-// Mobile app Behat testing requires this option, pointing to a developer Moodle Mobile directory:
-//   $CFG->behat_ionic_dirroot = '/where/I/keep/my/git/checkouts/moodlemobile2';
+// Mobile app Behat testing requires this option, pointing to a developer Moodle app directory:
+//   $CFG->behat_ionic_dirroot = '/where/I/keep/my/git/checkouts/moodleapp';
 //
 // The following option can be used to indicate a running Ionic server (otherwise Behat will start
 // one automatically for each test run, which is convenient but takes ages):
@@ -1055,6 +1055,15 @@ $CFG->admin = 'admin';
 //      $CFG->alternative_file_system_class = '\\local_myfilestorage\\file_system';
 //
 //=========================================================================
+// 15. CAMPAIGN CONTENT
+//=========================================================================
+//
+// We have added a campaign content to the notifications page, in case you want to hide that from your site you just
+// need to set showcampaigncontent setting to false.
+//
+//      $CFG->showcampaigncontent = true;
+//
+//=========================================================================
 // ALL DONE!  To continue installation, visit your main page with a browser
 //=========================================================================
 
index 5d0babf..bd655b7 100644 (file)
Binary files a/contentbank/amd/build/sort.min.js and b/contentbank/amd/build/sort.min.js differ
index 4609a9e..663a438 100644 (file)
Binary files a/contentbank/amd/build/sort.min.js.map and b/contentbank/amd/build/sort.min.js.map differ
index bfa61c8..24f4f79 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-import selectors from 'core_contentbank/selectors';
+import selectors from './selectors';
 import {get_string as getString} from 'core/str';
 import Prefetch from 'core/prefetch';
+import Ajax from 'core/ajax';
+import Notification from 'core/notification';
 
 /**
  * Set up the contentbank views.
@@ -59,6 +61,7 @@ const registerListenerEvents = (contentBank) => {
         contentBank.classList.add('view-grid');
         viewGrid.classList.add('active');
         viewList.classList.remove('active');
+        setViewListPreference(false);
     });
 
     viewList.addEventListener('click', () => {
@@ -66,6 +69,7 @@ const registerListenerEvents = (contentBank) => {
         contentBank.classList.add('view-list');
         viewList.classList.add('active');
         viewGrid.classList.remove('active');
+        setViewListPreference(true);
     });
 
     // Sort by file name alphabetical
@@ -97,6 +101,35 @@ const registerListenerEvents = (contentBank) => {
     });
 };
 
+
+/**
+ * Set the contentbank user preference in list view
+ *
+ * @param  {Bool} viewList view ContentBank as list.
+ * @return {Promise} Repository promise.
+ */
+const setViewListPreference = function(viewList) {
+
+    // If the given status is not hidden, the preference has to be deleted with a null value.
+    if (viewList === false) {
+        viewList = null;
+    }
+
+    const request = {
+        methodname: 'core_user_update_user_preferences',
+        args: {
+            preferences: [
+                {
+                    type: 'core_contentbank_view_list',
+                    value: viewList
+                }
+            ]
+        }
+    };
+
+    return Ajax.call([request])[0].catch(Notification.exception);
+};
+
 /**
  * Update the sort button view.
  *
index 8393f6a..326e304 100644 (file)
@@ -24,6 +24,7 @@
 
 namespace core_contentbank;
 
+use core_plugin_manager;
 use stored_file;
 use context;
 
@@ -35,6 +36,8 @@ use context;
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class contentbank {
+    /** @var array Enabled content types. */
+    private $enabledcontenttypes = null;
 
     /**
      * Obtains the list of core_contentbank_content objects currently active.
@@ -44,16 +47,20 @@ class contentbank {
      * @return string[] Array of contentbank contenttypes.
      */
     public function get_enabled_content_types(): array {
+        if (!is_null($this->enabledcontenttypes)) {
+            return $this->enabledcontenttypes;
+        }
+
         $enabledtypes = \core\plugininfo\contenttype::get_enabled_plugins();
         $types = [];
         foreach ($enabledtypes as $name) {
             $contenttypeclassname = "\\contenttype_$name\\contenttype";
             $contentclassname = "\\contenttype_$name\\content";
             if (class_exists($contenttypeclassname) && class_exists($contentclassname)) {
-                $types[] = $name;
+                $types[$contenttypeclassname] = $name;
             }
         }
-        return $types;
+        return $this->enabledcontenttypes = $types;
     }
 
     /**
@@ -292,4 +299,37 @@ class contentbank {
         }
         return $result;
     }
+
+    /**
+     * Get the list of content types that have the requested feature.
+     *
+     * @param string $feature Feature code e.g CAN_UPLOAD.
+     * @param null|\context $context Optional context to check the permission to use the feature.
+     * @param bool $enabled Whether check only the enabled content types or all of them.
+     *
+     * @return string[] List of content types where the user has permission to access the feature.
+     */
+    public function get_contenttypes_with_capability_feature(string $feature, \context $context = null, bool $enabled = true): array {
+        $contenttypes = [];
+        // Check enabled content types or all of them.
+&nbs