Merge branch 'MDL-68244' of https://github.com/Chocolate-lightning/moodle
authorSara Arjona <sara@moodle.com>
Thu, 28 May 2020 10:42:39 +0000 (12:42 +0200)
committerSara Arjona <sara@moodle.com>
Thu, 28 May 2020 10:42:39 +0000 (12:42 +0200)
439 files changed:
.eslintignore
.stylelintignore
admin/settings/license.php [new file with mode: 0644]
admin/settings/plugins.php
admin/settings/top.php
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
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
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
config-dist.php
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/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/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/contentbank_test.php
contentbank/tests/fixtures/testable_contenttype.php
contentbank/view.php
course/classes/category.php
course/classes/management_renderer.php
course/lib.php
course/renderer.php
course/templates/coursecard.mustache
course/templates/local/activitychooser/item.mustache
course/tests/behat/behat_course.php
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]
grade/edit/letter/index.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
h5p/classes/api.php
h5p/classes/helper.php
h5p/classes/player.php
h5p/tests/api_test.php
h5p/tests/external_test.php
h5p/tests/generator/lib.php
h5p/tests/helper_test.php
lang/en/admin.php
lang/en/badges.php
lang/en/cache.php
lang/en/contentbank.php
lang/en/deprecated.txt
lang/en/license.php
lang/en/repository.php
lang/en/role.php
lang/en/user.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/src/form-autocomplete.js
lib/amd/src/notification.js
lib/amd/src/tag.js
lib/amd/src/templates.js
lib/badgeslib.php
lib/behat/classes/behat_core_generator.php
lib/behat/classes/partial_named_selector.php
lib/classes/dataformat/base.php
lib/classes/dataformat/spout_base.php
lib/classes/plugin_manager.php
lib/db/access.php
lib/db/caches.php
lib/db/install.xml
lib/db/upgrade.php
lib/db/upgradelib.php
lib/form/dateselector.php
lib/form/datetimeselector.php
lib/form/filemanager.js
lib/form/filemanager.php
lib/form/templates/element-group-inline.mustache
lib/gradelib.php
lib/grouplib.php
lib/licenselib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/questionlib.php
lib/table/classes/external/dynamic/fetch.php
lib/table/classes/local/filter/filter.php
lib/table/classes/local/filter/filterset.php
lib/tablelib.php
lib/templates/action_menu_link.mustache
lib/templates/action_menu_trigger.mustache
lib/templates/filemanager_chooselicense.mustache [new file with mode: 0644]
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/toast/message.mustache
lib/templates/loginform.mustache
lib/testing/generator/data_generator.php
lib/tests/behat/app_behat_runtime.js
lib/tests/behat/behat_app.php
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
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/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/db/services.php
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/attemptlib.php
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/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/questiontypebase.php
question/upgrade.txt
repository/contentbank/tests/behat/select_content.feature
repository/draftfiles_ajax.php
repository/filepicker.js
repository/lib.php
repository/tests/behat/delete_files.feature
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/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/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/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/modules.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
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/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/classes/output/participants_filter.php [new file with mode: 0644]
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/tests/table/participants_search_test.php
user/tests/userlib_test.php
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
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..8717843 100644 (file)
@@ -32,6 +32,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')));
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"
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..3cb8e78 100644 (file)
@@ -92,3 +92,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 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 96eb509..ccc760a 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):
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.
+        if ($enabled) {
+            $contenttypestocheck = $this->get_enabled_content_types();
+        } else {
+            $plugins = core_plugin_manager::instance()->get_plugins_of_type('contenttype');
+            foreach ($plugins as $plugin) {
+                $contenttypeclassname = "\\{$plugin->type}_{$plugin->name}\\contenttype";
+                $contenttypestocheck[$contenttypeclassname] = $plugin->name;
+            }
+        }
+
+        foreach ($contenttypestocheck as $classname => $name) {
+            $contenttype = new $classname($context);
+            // The method names that check the features permissions must follow the pattern can_feature.
+            if ($contenttype->{"can_$feature"}()) {
+                $contenttypes[$classname] = $name;
+            }
+        }
+
+        return $contenttypes;
+    }
 }
index e2c1940..ba9442b 100644 (file)
@@ -41,7 +41,10 @@ abstract class contenttype {
     /** Plugin implements uploading feature */
     const CAN_UPLOAD = 'upload';
 
-    /** @var context This contenttype's context. **/
+    /** Plugin implements edition feature */
+    const CAN_EDIT = 'edit';
+
+    /** @var \context This contenttype's context. **/
     protected $context = null;
 
     /**
@@ -59,7 +62,7 @@ abstract class contenttype {
     /**
      * Fills content_bank table with appropiate information.
      *
-     * @param stdClass $record An optional content record compatible object (default null)
+     * @param \stdClass $record An optional content record compatible object (default null)
      * @return content  Object with content bank information.
      */
     public function create_content(\stdClass $record = null): ?content {
@@ -127,7 +130,7 @@ abstract class contenttype {
      * This method can be overwritten by the plugins if they need to change some other specific information.
      *
      * @param  content $content The content to rename.
-     * @param string $name  The name of the content.
+     * @param  string $name  The name of the content.
      * @return boolean true if the content has been renamed; false otherwise.
      */
     public function rename_content(content $content, string $name): bool {
@@ -139,7 +142,7 @@ abstract class contenttype {
      * This method can be overwritten by the plugins if they need to change some other specific information.
      *
      * @param  content $content The content to rename.
-     * @param context $context  The new context.
+     * @param  \context $context  The new context.
      * @return boolean true if the content has been renamed; false otherwise.
      */
     public function move_content(content $content, \context $context): bool {
@@ -325,6 +328,37 @@ abstract class contenttype {
         return true;
     }
 
+    /**
+     * Returns whether or not the user has permission to use the editor.
+     *
+     * @return bool     True if the user can edit content. False otherwise.
+     */
+    final public function can_edit(): bool {
+        if (!$this->is_feature_supported(self::CAN_EDIT)) {
+            return false;
+        }
+
+        if (!$this->can_access()) {
+            return false;
+        }
+
+        $classname = 'contenttype/'.$this->get_plugin_name();
+
+        $editioncap = $classname.':useeditor';
+        $hascapabilities = has_all_capabilities(['moodle/contentbank:useeditor', $editioncap], $this->context);
+        return $hascapabilities && $this->is_edit_allowed();
+    }
+
+    /**
+     * Returns plugin allows edition.
+     *
+     * @return bool     True if plugin allows edition. False otherwise.
+     */
+    protected function is_edit_allowed(): bool {
+        // Plugins can overwrite this function to add any check they need.
+        return true;
+    }
+
     /**
      * Returns the plugin supports the feature.
      *
@@ -348,4 +382,17 @@ abstract class contenttype {
      * @return array
      */
     abstract public function get_manageable_extensions(): array;
+
+    /**
+     * Returns the list of different types of the given content type.
+     *
+     * A content type can have one or more options for creating content. This method will report all of them or only the content
+     * type itself if it has no other options.
+     *
+     * @return array An object for each type:
+     *     - string typename: descriptive name of the type.
+     *     - string typeeditorparams: params required by this content type editor.
+     *     - url typeicon: this type icon.
+     */
+    abstract public function get_contenttype_types(): array;
 }
diff --git a/contentbank/classes/form/edit_content.php b/contentbank/classes/form/edit_content.php
new file mode 100644 (file)
index 0000000..e75b731
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+/**
+ * Provides {@see \core_contentbank\form\edit_content} class.
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank\form;
+
+use moodleform;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Defines the form for editing a content.
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class edit_content extends moodleform {
+
+    /** @var int Context the content belongs to. */
+    protected $contextid;
+
+    /** @var string Content type plugin name. */
+    protected $plugin;
+
+    /** @var int Content id in the content bank. */
+    protected $id;
+
+    /**
+     * Constructor.
+     *
+     * @param string $action The action attribute for the form.
+     * @param array $customdata Data to set during instance creation.
+     * @param string $method Form method.
+     */
+    public function __construct(string $action = null, array $customdata = null, string $method = 'post') {
+        parent::__construct($action, $customdata, $method);
+        $this->contextid = $customdata['contextid'];
+        $this->plugin = $customdata['plugin'];
+        $this->id = $customdata['id'];
+
+        $mform =& $this->_form;
+        $mform->addElement('hidden', 'contextid', $this->contextid);
+        $this->_form->setType('contextid', PARAM_INT);
+
+        $mform->addElement('hidden', 'plugin', $this->plugin);
+        $this->_form->setType('plugin', PARAM_PLUGIN);
+
+        $mform->addElement('hidden', 'id', $this->id);
+        $this->_form->setType('id', PARAM_INT);
+    }
+
+    /**
+     * Overrides formslib's add_action_buttons() method.
+     *
+     *
+     * @param bool $cancel
+     * @param string|null $submitlabel
+     *
+     * @return void
+     */
+    public function add_action_buttons($cancel = true, $submitlabel = null): void {
+        if (is_null($submitlabel)) {
+            $submitlabel = get_string('save');
+        }
+        parent::add_action_buttons($cancel, $submitlabel);
+    }
+}
index 6574b04..b851222 100644 (file)
@@ -98,7 +98,56 @@ class bankcontent implements renderable, templatable {
             );
         }
         $data->contents = $contentdata;
-        $data->tools = $this->toolbar;
+        // The tools are displayed in the action bar on the index page.
+        foreach ($this->toolbar as $tool) {
+            // Customize the output of a tool, like dropdowns.
+            $method = 'export_tool_'.$tool['name'];
+            if (method_exists($this, $method)) {
+                $this->$method($tool);
+            }
+            $data->tools[] = $tool;
+        }
+
         return $data;
     }
+
+    /**
+     * Adds the content type items to display to the Add dropdown.
+     *
+     * Each content type is represented as an object with the properties:
+     *     - name: the name of the content type.
+     *     - baseurl: the base content type editor URL.
+     *     - types: different types of the content type to display as dropdown items.
+     *
+     * @param array $tool Data for rendering the Add dropdown, including the editable content types.
+     */
+    private function export_tool_add(array &$tool) {
+        $editabletypes = $tool['contenttypes'];
+
+        $addoptions = [];
+        foreach ($editabletypes as $class => $type) {
+            $contentype = new $class($this->context);
+            // Get the creation options of each content type.
+            $types = $contentype->get_contenttype_types();
+            if ($types) {
+                // Add a text describing the content type as first option. This will be displayed in the drop down to
+                // separate the options for the different content types.
+                $contentdesc = new stdClass();
+                $contentdesc->typename = get_string('description', $contentype->get_contenttype_name());
+                array_unshift($types, $contentdesc);
+                // Context data for the template.
+                $addcontenttype = new stdClass();
+                // Content type name.
+                $addcontenttype->name = $type;
+                // Content type editor base URL.
+                $tool['link']->param('plugin', $type);
+                $addcontenttype->baseurl = $tool['link']->out();
+                // Different types of the content type.
+                $addcontenttype->types = $types;
+                $addoptions[] = $addcontenttype;
+            }
+        }
+
+        $tool['contenttypes'] = $addoptions;
+    }
 }
diff --git a/contentbank/classes/output/viewcontent.php b/contentbank/classes/output/viewcontent.php
new file mode 100644 (file)
index 0000000..efb403e
--- /dev/null
@@ -0,0 +1,94 @@
+<?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/>.
+
+/**
+ * Class containing data for a content view.
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank\output;
+
+use core_contentbank\content;
+use core_contentbank\contenttype;
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+
+/**
+ * Class containing data for the content view.
+ *
+ * @copyright  2020 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class viewcontent implements renderable, templatable {
+    /**
+     * @var contenttype Content bank content type.
+     */
+    private $contenttype;
+
+    /**
+     * @var stdClass Record of the contentbank_content table.
+     */
+    private $content;
+
+    /**
+     * Construct this renderable.
+     *
+     * @param contenttype $contenttype Content bank content type.
+     * @param content $content Record of the contentbank_content table.
+     */
+    public function __construct(contenttype $contenttype, content $content) {
+        $this->contenttype = $contenttype;
+        $this->content = $content;
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     *
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output): stdClass {
+        $data = new stdClass();
+
+        // Get the content type html.
+        $contenthtml = $this->contenttype->get_view_content($this->content);
+        $data->contenthtml = $contenthtml;
+
+        // Check if the user can edit this content type.
+        if ($this->contenttype->can_edit()) {
+            $data->usercanedit = true;
+            $urlparams = [
+                'contextid' => $this->content->get_contextid(),
+                'plugin' => $this->contenttype->get_plugin_name(),
+                'id' => $this->content->get_id()
+            ];
+            $editcontenturl = new moodle_url('/contentbank/edit.php', $urlparams);
+            $data->editcontenturl = $editcontenturl->out(false);
+        }
+
+        $closeurl = new moodle_url('/contentbank/index.php', ['contextid' => $this->content->get_contextid()]);
+        $data->closeurl = $closeurl->out(false);
+
+        return $data;
+    }
+}
index d48941d..1c1f2ea 100644 (file)
 namespace contenttype_h5p;
 
 use core\event\contentbank_content_viewed;
-use html_writer;
+use stdClass;
+use core_h5p\editor_ajax;
+use core_h5p\file_storage;
+use core_h5p\local\library\autoloader;
+use H5PCore;
 
 /**
  * H5P content bank manager class
@@ -65,8 +69,7 @@ class contenttype extends \core_contentbank\contenttype {
         $event->trigger();
 
         $fileurl = $content->get_file_url();
-        $html = html_writer::tag('h2', $content->get_name());
-        $html .= \core_h5p\player::display($fileurl, new \stdClass(), true);
+        $html = \core_h5p\player::display($fileurl, new \stdClass(), true);
         return $html;
     }
 
@@ -107,7 +110,7 @@ class contenttype extends \core_contentbank\contenttype {
      * @return array
      */
     protected function get_implemented_features(): array {
-        return [self::CAN_UPLOAD];
+        return [self::CAN_UPLOAD, self::CAN_EDIT];
     }
 
     /**
@@ -127,4 +130,42 @@ class contenttype extends \core_contentbank\contenttype {
     protected function is_access_allowed(): bool {
         return true;
     }
+
+    /**
+     * Returns the list of different H5P content types the user can create.
+     *
+     * @return array An object for each H5P content type:
+     *     - string typename: descriptive name of the H5P content type.
+     *     - string typeeditorparams: params required by the H5P editor.
+     *     - url typeicon: H5P content type icon.
+     */
+    public function get_contenttype_types(): array {
+        // Get the H5P content types available.
+        autoloader::register();
+        $editorajax = new editor_ajax();
+        $h5pcontenttypes = $editorajax->getLatestLibraryVersions();
+
+        $types = [];
+        $h5pfilestorage = new file_storage();
+        foreach ($h5pcontenttypes as $h5pcontenttype) {
+            $library = [
+                'name' => $h5pcontenttype->machine_name,
+                'majorVersion' => $h5pcontenttype->major_version,
+                'minorVersion' => $h5pcontenttype->minor_version,
+            ];
+            $key = H5PCore::libraryToString($library);
+            $type = new stdClass();
+            $type->key = $key;
+            $type->typename = $h5pcontenttype->title;
+            $type->typeeditorparams = 'library=' . $key;
+            $type->typeicon = $h5pfilestorage->get_icon_url(
+                $h5pcontenttype->id,
+                $h5pcontenttype->machine_name,
+                $h5pcontenttype->major_version,
+                $h5pcontenttype->minor_version);
+            $types[] = $type;
+        }
+
+        return $types;
+    }
 }
diff --git a/contentbank/contenttype/h5p/classes/form/editor.php b/contentbank/contenttype/h5p/classes/form/editor.php
new file mode 100644 (file)
index 0000000..b7229b9
--- /dev/null
@@ -0,0 +1,152 @@
+<?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/>.
+
+/**
+ * Provides the class that defines the form for the H5P authoring tool.
+ *
+ * @package    contenttype_h5p
+ * @copyright  2020 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace contenttype_h5p\form;
+
+use contenttype_h5p\content;
+use contenttype_h5p\contenttype;
+use core_contentbank\form\edit_content;
+use core_h5p\api;
+use core_h5p\editor as h5peditor;
+use core_h5p\factory;
+use stdClass;
+
+/**
+ * Defines the form for editing an H5P content.
+ *
+ * @copyright 2020 Victor Deniz <victor@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class editor extends edit_content {
+
+    /** @var $h5peditor H5P editor object */
+    private $h5peditor;
+
+    /** @var $content The content being edited */
+    private $content;
+
+    /**
+     * Defines the form fields.
+     */
+    protected function definition() {
+        global $DB;
+
+        $mform = $this->_form;
+
+        // Id of the content to edit.
+        $id = $this->_customdata['id'];
+        // H5P content type to create.
+        $library = optional_param('library', null, PARAM_TEXT);
+
+        if (empty($id) && empty($library)) {
+            $returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $this->_customdata['contextid']]);
+            print_error('invalidcontentid', 'error', $returnurl);
+        }
+
+        $this->h5peditor = new h5peditor();
+
+        if ($id) {
+            // The H5P editor needs the H5P content id (h5p table).
+            $record = $DB->get_record('contentbank_content', ['id' => $id]);
+            $this->content = new content($record);
+            $file = $this->content->get_file();
+
+            $h5p = api::get_content_from_pathnamehash($file->get_pathnamehash());
+            $mform->addElement('hidden', 'h5pid', $h5p->id);
+            $mform->setType('h5pid', PARAM_INT);
+            $this->h5peditor->set_content($h5p->id);
+        } else {
+            // The H5P editor needs the H5P content type library name for a new content.
+            $mform->addElement('hidden', 'library', $library);
+            $mform->setType('library', PARAM_TEXT);
+            $this->h5peditor->set_library($library, $this->_customdata['contextid'], 'contentbank', 'public');
+        }
+
+        $mformid = 'coolh5peditor';
+        $mform->setAttributes(array('id' => $mformid) + $mform->getAttributes());
+
+        $this->add_action_buttons();
+
+        $this->h5peditor->add_editor_to_form($mform);
+
+        $this->add_action_buttons();
+    }
+
+    /**
+     * Modify or create an H5P content from the form data.
+     *
+     * @param stdClass $data Form data to create or modify an H5P content.
+     *
+     * @return int The id of the edited or created content.
+     */
+    public function save_content(stdClass $data): int {
+        global $DB;
+
+        // The H5P libraries expect data->id as the H5P content id.
+        // The method \H5PCore::saveContent throws an error if id is set but empty.
+        if (empty($data->id)) {
+            unset($data->id);
+        } else {
+            // The H5P libraries save in $data->id the H5P content id (h5p table), so the content id is saved in another var.
+            $contentid = $data->id;
+        }
+
+        $h5pcontentid = $this->h5peditor->save_content($data);
+
+        $factory = new factory();
+        $h5pfs = $factory->get_framework();
+
+        // Needs the H5P file id to create or update the content bank record.
+        $h5pcontent = $h5pfs->loadContent($h5pcontentid);
+        $fs = get_file_storage();
+        $file = $fs->get_file_by_hash($h5pcontent['pathnamehash']);
+        // Creating new content.
+        if (!isset($data->h5pid)) {
+            // The initial name of the content is the title of the H5P content.
+            $cbrecord = new stdClass();
+            $cbrecord->name = json_decode($data->h5pparams)->metadata->title;
+            $context = \context::instance_by_id($data->contextid, MUST_EXIST);
+            // Create entry in content bank.
+            $contenttype = new contenttype($context);
+            $newcontent = $contenttype->create_content($cbrecord);
+            if ($file && $newcontent) {
+                $updatedfilerecord = new stdClass();
+                $updatedfilerecord->id = $file->get_id();
+                $updatedfilerecord->itemid = $newcontent->get_id();
+                // As itemid changed, the pathnamehash has to be updated in the file table.
+                $pathnamehash = \file_storage::get_pathname_hash($file->get_contextid(), $file->get_component(),
+                    $file->get_filearea(), $updatedfilerecord->itemid, $file->get_filepath(), $file->get_filename());
+                $updatedfilerecord->pathnamehash = $pathnamehash;
+                $DB->update_record('files', $updatedfilerecord);
+                // The pathnamehash in the h5p table must match the file pathnamehash.
+                $h5pfs->updateContentFields($h5pcontentid, ['pathnamehash' => $pathnamehash]);
+            }
+        } else {
+            // Update content.
+            $this->content->update_content();
+        }
+
+        return $contentid ?? $newcontent->get_id();
+    }
+}
index 95db4aa..97076ae 100644 (file)
@@ -44,4 +44,14 @@ $capabilities = [
             'editingteacher' => CAP_ALLOW,
         ]
     ],
+    'contenttype/h5p:useeditor' => [
+        'riskbitmask' => RISK_SPAM,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => [
+            'manager' => CAP_ALLOW,
+            'coursecreator' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+        ]
+    ],
 ];
index 179e80f..7e2e49e 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['description'] = 'H5P Interactive Content';
 $string['pluginname'] = 'H5P';
 $string['pluginname_help'] = 'Content bank to upload and share H5P content';
 $string['privacy:metadata'] = 'The H5P content bank plugin does not store any personal data.';
 $string['h5p:access'] = 'Access H5P content in the content bank';
 $string['h5p:upload'] = 'Upload new H5P content';
+$string['h5p:useeditor'] = 'Create or edit content using the H5P editor';
index 548ef13..fe0e68b 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2020041500.00;         // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2020051500.01;         // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2020041500.00;         // Requires this Moodle version
 $plugin->component = 'contenttype_h5p'; // Full name of the plugin (used for diagnostics).
diff --git a/contentbank/edit.php b/contentbank/edit.php
new file mode 100644 (file)
index 0000000..832f1c2
--- /dev/null
@@ -0,0 +1,110 @@
+<?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/>.
+
+/**
+ * Create or update contents through the specific content type editor
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require('../config.php');
+
+require_login();
+
+$contextid = required_param('contextid', PARAM_INT);
+$pluginname = required_param('plugin', PARAM_PLUGIN);
+$id = optional_param('id', null, PARAM_INT);
+$context = context::instance_by_id($contextid, MUST_EXIST);
+require_capability('moodle/contentbank:access', $context);
+
+$returnurl = new \moodle_url('/contentbank/view.php', ['id' => $id]);
+
+if (!empty($id)) {
+    $record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST);
+    $contentclass = "$record->contenttype\\content";
+    $content = new $contentclass($record);
+    // Set the heading title.
+    $heading = $content->get_name();
+    // The content type of the content overwrites the pluginname param value.
+    $contenttypename = $content->get_content_type();
+} else {
+    $contenttypename = "contenttype_$pluginname";
+    $heading = get_string('addinganew', 'moodle', get_string('description', $contenttypename));
+}
+
+// Check plugin is enabled.
+$plugin = core_plugin_manager::instance()->get_plugin_info($contenttypename);
+if (!$plugin || !$plugin->is_enabled()) {
+    print_error('unsupported', 'core_contentbank', $returnurl);
+}
+
+// Create content type instance.
+$contenttypeclass = "$contenttypename\\contenttype";
+if (class_exists($contenttypeclass)) {
+    $contenttype = new $contenttypeclass($context);
+} else {
+    print_error('unsupported', 'core_contentbank', $returnurl);
+}
+
+// Checks the user can edit this content type.
+if (!$contenttype->can_edit()) {
+    print_error('contenttypenoedit', 'core_contentbank', $returnurl, $contenttype->get_plugin_name());
+}
+
+$values = [
+    'contextid' => $contextid,
+    'plugin' => $pluginname,
+    'id' => $id
+];
+
+$title = get_string('contentbank');
+\core_contentbank\helper::get_page_ready($context, $title, true);
+if ($PAGE->course) {
+    require_login($PAGE->course->id);
+}
+
+$PAGE->set_url(new \moodle_url('/contentbank/edit.php', $values));
+$PAGE->set_context($context);
+$PAGE->navbar->add(get_string('edit'));
+$PAGE->set_title($title);
+
+$PAGE->set_heading($heading);
+
+// Instantiate the content type form.
+$editorclass = "$contenttypename\\form\\editor";
+if (!class_exists($editorclass)) {
+    print_error('noformdesc');
+}
+
+$editorform = new $editorclass(null, $values);
+
+if ($editorform->is_cancelled()) {
+    if (empty($id)) {
+        $returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]);
+    }
+    redirect($returnurl);
+} else if ($data = $editorform->get_data()) {
+    $id = $editorform->save_content($data);
+    // Just in case we've created a new content.
+    $returnurl->param('id', $id);
+    redirect($returnurl);
+}
+
+echo $OUTPUT->header();
+$editorform->display();
+echo $OUTPUT->footer();
index f4f101b..e4bb6d6 100644 (file)
@@ -62,12 +62,25 @@ $foldercontents = $cb->search_contents($search, $contextid, $contenttypes);
 
 // Get the toolbar ready.
 $toolbar = array ();
+
+// Place the Add button in the toolbar.
+if (has_capability('moodle/contentbank:useeditor', $context)) {
+    // Get the content types for which the user can use an editor.
+    $editabletypes = $cb->get_contenttypes_with_capability_feature(\core_contentbank\contenttype::CAN_EDIT, $context);
+    if (!empty($editabletypes)) {
+        // Editor base URL.
+        $editbaseurl = new moodle_url('/contentbank/edit.php', ['contextid' => $contextid]);
+        $toolbar[] = ['name' => get_string('add'), 'link' => $editbaseurl, 'dropdown' => true, 'contenttypes' => $editabletypes];
+    }
+}
+
+// Place the Upload button in the toolbar.
 if (has_capability('moodle/contentbank:upload', $context)) {
     // Don' show upload button if there's no plugin to support any file extension.
     $accepted = $cb->get_supported_extensions_as_string($context);
     if (!empty($accepted)) {
         $importurl = new moodle_url('/contentbank/upload.php', ['contextid' => $contextid]);
-        $toolbar[] = array('name' => 'Upload', 'link' => $importurl, 'icon' => 'i/upload');
+        $toolbar[] = array('name' => get_string('upload', 'contentbank'), 'link' => $importurl, 'icon' => 'i/upload');
     }
 }
 
index 0826a65..020b905 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core_contentbank/list
+    @template core_contentbank/bankcontent
 
     Example context (json):
     {
             },
             {
                 "name": "resume.pdf",
+                "title": "resume",
+                "timemodified": 1589792039,
+                "size": "699.3KB",
+                "bytes": 716126,
+                "type": "Archive (PDF)",
                 "icon": "http://something/theme/image.php/boost/core/1584597850/f/pdf-64"
             }
         ],
         "tools": [
+            {
+                "name": "Add",
+                "dropdown": true,
+                "link": "http://something/contentbank/edit.php?contextid=1",
+                "contenttypes": [
+                    {
+                        "name": "H5P Interactive Content",
+                        "baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
+                        "types": [
+                            {
+                                "typename": "H5P Interactive Content"
+                            },
+                            {
+                                "typename": "Accordion",
+                                "typeeditorparams": "library=Accordion-1.4",
+                                "typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
+                            }
+                        ]
+                    }
+                ]
+            },
             {
                 "name": "Upload",
                 "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
index 04c762a..242fa1a 100644 (file)
     Example context (json):
     {
         "tools": [
+            {
+                "name": "Add",
+                "dropdown": true,
+                "link": "http://something/contentbank/edit.php?contextid=1",
+                "contenttypes": [
+                    {
+                        "name": "h5p",
+                        "baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
+                        "types": [
+                            {
+                                "typename": "H5P Interactive Content"
+                            },
+                            {
+                                "typename": "Accordion",
+                                "typeeditorparams": "library=Accordion-1.4",
+                                "typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
+                            }
+                        ]
+                    }
+                ]
+            },
             {
                 "name": "Upload",
                 "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
 }}
 
 {{#tools}}
-    <a href="{{{ link }}}" class="icon-no-margin btn btn-secondary" title="{{{ name }}}">
-        {{#pix}} {{{ icon }}} {{/pix}} {{{ name }}}
-    </a>
+    {{#dropdown}}
+        {{>core_contentbank/bankcontent/toolbar_dropdown}}
+    {{/dropdown}}
+    {{^dropdown}}
+        <a href="{{{ link }}}" class="icon-no-margin btn btn-secondary" title="{{{ name }}}">
+            {{#pix}} {{{ icon }}} {{/pix}} {{{ name }}}
+        </a>
+    {{/dropdown}}
 {{/tools}}
 <button class="icon-no-margin btn btn-secondary active ml-2"
 title="{{#str}}  displayicons, contentbank  {{/str}}"
@@ -47,4 +73,4 @@ data-action="viewgrid">
 title="{{#str}} displaydetails, contentbank {{/str}}"
 data-action="viewlist">
     {{#pix}}t/viewdetails, core, {{#str}} displaydetails, contentbank {{/str}} {{/pix}}
-</button>
\ No newline at end of file
+</button>
diff --git a/contentbank/templates/bankcontent/toolbar_dropdown.mustache b/contentbank/templates/bankcontent/toolbar_dropdown.mustache
new file mode 100644 (file)
index 0000000..7a2fbf5
--- /dev/null
@@ -0,0 +1,64 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_contentbank/bankcontent/toolbar_dropdown
+
+    Example context (json):
+        {
+            "name": "Add",
+            "dropdown": true,
+            "link": "http://something/contentbank/edit.php?contextid=1",
+            "contenttypes": [
+                {
+                    "name": "h5p",
+                    "baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
+                    "types": [
+                        {
+                            "typename": "H5P Interactive Content"
+                        },
+                        {
+                            "typename": "Accordion",
+                            "typeeditorparams": "library=Accordion-1.4",
+                            "typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
+                        }
+                    ]
+                }
+            ]
+        }
+
+}}
+<div class="btn-group mr-1" role="group">
+    <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" data-action="{{name}}-content"
+            aria-haspopup="true" aria-expanded="false" {{^contenttypes}}title="{{#str}}nocontenttypes, core_contentbank{{/str}}"
+            disabled{{/contenttypes}}>
+        {{#name}} {{name}} {{/name}}
+    </button>
+    <div class="dropdown-menu dropdown-scrollable dropdown-menu-right">
+        {{#contenttypes}}
+            {{#types}}
+                {{^typeeditorparams}}
+                    <h6 class="dropdown-header">{{ typename }}</h6>
+                {{/typeeditorparams}}
+                {{#typeeditorparams}}
+                    <a class="dropdown-item icon-size-4" href="{{{ baseurl }}}&{{{ typeeditorparams }}}">
+                        <img alt="" class="icon" src="{{{ typeicon }}}"> {{ typename }}
+                    </a>
+                {{/typeeditorparams}}
+            {{/types}}
+        {{/contenttypes}}
+    </div>
+</div>
diff --git a/contentbank/templates/viewcontent.mustache b/contentbank/templates/viewcontent.mustache
new file mode 100644 (file)
index 0000000..7c7d5c0
--- /dev/null
@@ -0,0 +1,52 @@
+{{!
+    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 comments.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_contentbank/view_content
+
+    View content page.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * contenthtml - string - content html.
+    * usercanedit - boolean - whether the user has permission to edit the content.
+    * editcontenturl - string - edit page URL.
+    * closeurl - string - close landing page.
+
+    Example context (json):
+    {
+        "contenthtml" : "<iframe src=\"http://something/h5p/embed.php?url=h5pfileurl\"></iframe>",
+        "usercanedit" : true,
+        "editcontenturl" : "http://something/contentbank/edit.php?contextid=1&plugin=h5p&id=1",
+        "closeurl" : "http://moodle.test/h5pcb/moodle/contentbank/index.php"
+    }
+}}
+<div class="core_contentbank_viewcontent">
+    <div class="d-flex justify-content-end flex-column flex-sm-row">
+        {{>core_contentbank/viewcontent/toolbarview}}
+    </div>
+    <div class="container mt-1 mb-1" data-region="viewcontent-content">
+        {{{ contenthtml }}}
+    </div>
+    <div class="d-flex justify-content-end flex-column flex-sm-row">
+        {{>core_contentbank/viewcontent/toolbarview}}
+    </div>
+</div>
diff --git a/contentbank/templates/viewcontent/toolbarview.mustache b/contentbank/templates/viewcontent/toolbarview.mustache
new file mode 100644 (file)
index 0000000..25eaaa5
--- /dev/null
@@ -0,0 +1,50 @@
+{{!
+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 comments.
+
+You should have received a copy of the GNU General Public License
+along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_contentbank/viewcontent/toolbarview
+
+    Contentbank view toolbar.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * contenthtml - string - content html.
+    * usercanedit - boolean - whether the user has permission to edit the content.
+    * editcontenturl - string - edit page URL.
+    * closeurl - string - close landing page.
+
+    Example context (json):
+    {
+        "usercanedit" : true,
+        "editcontenturl" : "http://something/contentbank/edit.php?contextid=1&plugin=h5p&id=1",
+        "closeurl" : "http://moodle.test/h5pcb/moodle/contentbank/index.php"
+    }
+}}
+{{#usercanedit}}
+<div class="cb-toolbar-container mb-2">
+    <a href="{{editcontenturl}}" class="btn btn-primary" data-action="edit-content">
+        {{#str}}edit{{/str}}
+    </a>
+    <a href="{{closeurl}}" class="btn btn-secondary" data-action="close-content">
+        {{#str}}close, core_contentbank{{/str}}
+    </a>
+</div>
+{{/usercanedit}}
diff --git a/contentbank/tests/behat/edit_content.feature b/contentbank/tests/behat/edit_content.feature
new file mode 100644 (file)
index 0000000..713768c
--- /dev/null
@@ -0,0 +1,99 @@
+@core @core_contentbank @contentbank_h5p @_file_upload @javascript
+Feature: Content bank use editor feature
+  In order to add/edit content
+  As a user
+  I need to be able to access the edition options
+
+  Background:
+    Given I log in as "admin"
+    And I am on site homepage
+    And I turn editing mode on
+    And I add the "Navigation" block if not present
+    And I configure the "Navigation" block
+    And I set the following fields to these values:
+      | Page contexts | Display throughout the entire site |
+    And I press "Save changes"
+
+  Scenario: Users see the Add button disabled if there is no content type available for creation
+    Given I click on "Site pages" "list_item" in the "Navigation" "block"
+    When I click on "Content bank" "link"
+    Then the "[data-action=Add-content]" "css_element" should be disabled
+
+  Scenario: Users can see the Add button if there is content type available for creation
+    Given I follow "Dashboard" in the user menu
+    And I follow "Manage private files..."
+    And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
+    And I click on "Save changes" "button"
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Content bank" "link" in the "Navigation" "block"
+    And I click on "Upload" "link"
+    And I click on "Choose a file..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Select this file" "button"
+    And I click on "Save changes" "button"
+    When I click on "Content bank" "link"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Close" "link"
+    Then I click on "[data-action=Add-content]" "css_element"
+    And I should see "Fill in the Blanks"
+
+  Scenario: Users can edit content if they have the required permission
+    Given I follow "Dashboard" in the user menu
+    And I follow "Manage private files..."
+    And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
+    And I click on "Save changes" "button"
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Content bank" "link" in the "Navigation" "block"
+    And I click on "Upload" "link"
+    And I click on "Choose a file..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Select this file" "button"
+    And I click on "Save changes" "button"
+    When I click on "Content bank" "link"
+    And I click on "filltheblanks.h5p" "link"
+    Then I click on "Edit" "link"
+    And I switch to "h5p-editor-iframe" class iframe
+    And I switch to the main frame
+    And I click on "Cancel" "button"
+    And I should see "filltheblanks.h5p" in the "h1" "css_element"
+
+  Scenario: Users can create new content if they have the required permission
+    Given I navigate to "H5P > Manage H5P content types" in site administration
+    And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
+    And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
+    And I should see "H5P content types uploaded successfully"
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    When I click on "Content bank" "link" in the "Navigation" "block"
+    And I click on "[data-action=Add-content]" "css_element"
+    Then I click on "Fill in the Blanks" "link"
+    And I switch to "h5p-editor-iframe" class iframe
+    And I switch to the main frame
+    And I click on "Cancel" "button"
+
+  Scenario: Users can't edit content if they don't have the required permission
+    Given the following "users" exist:
+      | username | firstname | lastname | email             |
+      | teacher1 | Teacher   | 1        | user1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role              |
+      | teacher1 | C1     | editingteacher    |
+    And I navigate to "H5P > Manage H5P content types" in site administration
+    And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
+    And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
+    And I should see "H5P content types uploaded successfully"
+    And I log out
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Content bank" "link"
+    And "[data-action=Add-content]" "css_element" should exist
+    When the following "permission overrides" exist:
+      | capability                       | permission | role           | contextlevel | reference |
+      | moodle/contentbank:useeditor     | Prohibit   | editingteacher | System       |           |
+    And I reload the page
+    Then "[data-action=Add-content]" "css_element" should not exist
index ebb6888..d347a72 100644 (file)
@@ -507,4 +507,100 @@ class core_contentbank_testcase extends advanced_testcase {
         // Check there's no error when trying to move content context from an empty content bank.
         $this->assertTrue($cb->delete_contents($systemcontext, $coursecontext));
     }
+
+    /**
+     * Data provider for get_contenttypes_with_capability_feature.
+     *
+     * @return  array
+     */
+    public function get_contenttypes_with_capability_feature_provider(): array {
+        return [
+            'no-contenttypes_enabled' => [
+                'contenttypesenabled' => [],
+                'contenttypescanfeature' => [],
+            ],
+            'contenttype_enabled_noeditable' => [
+                'contenttypesenabled' => ['testable'],
+                'contenttypescanfeature' => [],
+            ],
+            'contenttype_enabled_editable' => [
+                'contenttypesenabled' => ['testable'],
+                'contenttypescanfeature' => ['testable'],
+            ],
+            'no-contenttype_enabled_editable' => [
+                'contenttypesenabled' => [],
+                'contenttypescanfeature' => ['testable'],
+            ],
+        ];
+    }
+
+    /**
+     * Tests for get_contenttypes_with_capability_feature() function.
+     *
+     * @dataProvider    get_contenttypes_with_capability_feature_provider
+     * @param   array $contenttypesenabled Content types enabled.
+     * @param   array $contenttypescanfeature Content types the user has the permission to use the feature.
+     *
+     * @covers ::get_contenttypes_with_capability_feature
+     */
+    public function test_get_contenttypes_with_capability_feature(array $contenttypesenabled, array $contenttypescanfeature): void {
+        $this->resetAfterTest();
+
+        $cb = new contentbank();
+
+        $plugins = [];
+
+        // Content types not enabled where the user has permission to use a feature.
+        if (empty($contenttypesenabled) && !empty($contenttypescanfeature)) {
+            $enabled = false;
+
+            // Mock core_plugin_manager class and the method get_plugins_of_type.
+            $pluginmanager = $this->getMockBuilder(\core_plugin_manager::class)
+                ->disableOriginalConstructor()
+                ->setMethods(['get_plugins_of_type'])
+                ->getMock();
+
+            // Replace protected singletoninstance reference (core_plugin_manager property) with mock object.
+            $ref = new \ReflectionProperty(\core_plugin_manager::class, 'singletoninstance');
+            $ref->setAccessible(true);
+            $ref->setValue(null, $pluginmanager);
+
+            // Return values of get_plugins_of_type method.
+            foreach ($contenttypescanfeature as $contenttypepluginname) {
+                $contenttypeplugin = new \stdClass();
+                $contenttypeplugin->name = $contenttypepluginname;
+                $contenttypeplugin->type = 'contenttype';
+                // Add the feature to the fake content type.
+                $classname = "\\contenttype_$contenttypepluginname\\contenttype";
+                $classname::$featurestotest = ['test2'];
+                $plugins[] = $contenttypeplugin;
+            }
+
+            // Set expectations and return values.
+            $pluginmanager->expects($this->once())
+                ->method('get_plugins_of_type')
+                ->with('contenttype')
+                ->willReturn($plugins);
+        } else {
+            $enabled = true;
+            // Get access to private property enabledcontenttypes.
+            $rc = new \ReflectionClass(\core_contentbank\contentbank::class);
+            $rcp = $rc->getProperty('enabledcontenttypes');
+            $rcp->setAccessible(true);
+
+            foreach ($contenttypesenabled as $contenttypename) {
+                $plugins["\\contenttype_$contenttypename\\contenttype"] = $contenttypename;
+                // Add to the testable contenttype the feature to test.
+                if (in_array($contenttypename, $contenttypescanfeature)) {
+                    $classname = "\\contenttype_$contenttypename\\contenttype";
+                    $classname::$featurestotest = ['test2'];
+                }
+            }
+            // Set as enabled content types only those in the test.
+            $rcp->setValue($cb, $plugins);
+        }
+
+        $actual = $cb->get_contenttypes_with_capability_feature('test2', null, $enabled);
+        $this->assertEquals($contenttypescanfeature, array_values($actual));
+    }
 }
index a70bccb..2a5411d 100644 (file)
@@ -37,6 +37,9 @@ class contenttype extends \core_contentbank\contenttype {
     /** Feature for testing */
     const CAN_TEST = 'test';
 
+    /** @var array Additional features for testing */
+    public static $featurestotest;
+
     /**
      * Returns the HTML code to render the icon for content bank contents.
      *
@@ -55,7 +58,13 @@ class contenttype extends \core_contentbank\contenttype {
      * @return array
      */
     protected function get_implemented_features(): array {
-        return [self::CAN_TEST];
+        $features = [self::CAN_TEST];
+
+        if (!empty(self::$featurestotest)) {
+            $features = array_merge($features, self::$featurestotest);
+        }
+
+        return $features;
     }
 
     /**
@@ -66,4 +75,29 @@ class contenttype extends \core_contentbank\contenttype {
     public function get_manageable_extensions(): array {
         return  ['.txt', '.png', '.h5p'];
     }
+
+    /**
+     * Returns the list of different types of the given content type.
+     *
+     * @return array
+     */
+    public function get_contenttype_types(): array {
+        $type = new \stdClass();
+        $type->typename = 'testable';
+
+        return [$type];
+    }
+
+    /**
+     * Returns true, so the user has permission on the feature.
+     *
+     * @return bool     True if content could be edited or created. False otherwise.
+     */
+    final public function can_test2(): bool {
+        if (!$this->is_feature_supported('test2')) {
+            return false;
+        }
+
+        return true;
+    }
 }
index c95d7fd..1cf7500 100644 (file)
@@ -53,7 +53,7 @@ if ($PAGE->course) {
 $PAGE->set_url(new \moodle_url('/contentbank/view.php', ['id' => $id]));
 $PAGE->set_context($context);
 $PAGE->navbar->add($record->name);
-$PAGE->set_heading($title);
+$PAGE->set_heading($record->name);
 $title .= ": ".$record->name;
 $PAGE->set_title($title);
 $PAGE->set_pagetype('contenbank');
@@ -109,7 +109,6 @@ $PAGE->add_header_action(html_writer::div(
 ));
 
 echo $OUTPUT->header();
-echo $OUTPUT->box_start('generalbox');
 
 // If needed, display notifications.
 if ($errormsg !== '') {
@@ -118,8 +117,11 @@ if ($errormsg !== '') {
     echo $OUTPUT->notification($statusmsg, 'notifysuccess');
 }
 if ($contenttype->can_access()) {
-    echo $contenttype->get_view_content($content);
+    $viewcontent = new core_contentbank\output\viewcontent($contenttype, $content);
+    echo $OUTPUT->render($viewcontent);
+} else {
+    $message = get_string('contenttypenoaccess', 'core_contentbank', $record->contenttype);
+    echo $OUTPUT->notification($message, 'error');
 }
 
-echo $OUTPUT->box_end();
 echo $OUTPUT->footer();
index a1e20c2..cbe6878 100644 (file)
@@ -1600,7 +1600,10 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
                 }
                 // Prepare the list of core_course_list_element objects.
                 foreach ($ids as $id) {
-                    $courses[$id] = new core_course_list_element($records[$id]);
+                    // If a course is deleted after we got the cache entry it may not exist in the database anymore.
+                    if (!empty($records[$id])) {
+                        $courses[$id] = new core_course_list_element($records[$id]);
+                    }
                 }
             }
             return $courses;
@@ -1810,7 +1813,10 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
                 }
                 // Prepare the list of core_course_list_element objects.
                 foreach ($ids as $id) {
-                    $courses[$id] = new core_course_list_element($records[$id]);
+                    // If a course is deleted after we got the cache entry it may not exist in the database anymore.
+                    if (!empty($records[$id])) {
+                        $courses[$id] = new core_course_list_element($records[$id]);
+                    }
                 }
             }
             return $courses;
@@ -2019,7 +2025,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
         if (!$cb->delete_contents($this->get_context())) {
             throw new moodle_exception('errordeletingcontentfromcategory', 'contentbank', '', $this->get_formatted_name());
         }
-        if (!question_delete_course_category($this, 0, $showfeedback)) {
+        if (!question_delete_course_category($this, null)) {
             throw new moodle_exception('cannotdeletecategoryquestions', '', '', $this->get_formatted_name());
         }
 
@@ -2208,7 +2214,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
                 );
             }
         }
-        if (!question_delete_course_category($this, $newparentcat, $showfeedback)) {
+        if (!question_delete_course_category($this, $newparentcat)) {
             if ($showfeedback) {
                 echo $OUTPUT->notification(get_string('errordeletingquestionsfromcategory', 'question', $catname), 'notifysuccess');
             }
index 07394ca..ada3a69 100644 (file)
@@ -272,7 +272,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         $html .= html_writer::end_div();
         $html .= $icon;
         if ($hasactions) {
-            $textattributes = array('class' => 'float-left categoryname');
+            $textattributes = array('class' => 'float-left categoryname aalink');
         } else {
             $textattributes = array('class' => 'float-left categoryname without-actions');
         }
@@ -647,7 +647,7 @@ class core_course_management_renderer extends plugin_renderer_base {
             'for' => 'courselistitem' . $course->id));
         $html .= html_writer::end_div();
         $html .= html_writer::end_div();
-        $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
+        $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename aalink'));
         $html .= html_writer::start_div('float-right');
         if ($course->idnumber) {
             $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'text-muted idnumber'));
@@ -1222,7 +1222,7 @@ class core_course_management_renderer extends plugin_renderer_base {
             $html .= html_writer::end_div();
         }
         $html .= html_writer::end_div();
-        $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
+        $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename aalink'));
         $html .= html_writer::tag('span', $categoryname, array('class' => 'float-left ml-3 text-muted'));
         $html .= html_writer::start_div('float-right');
         $html .= $this->search_listitem_actions($course);
index 5e783d5..e67aa7a 100644 (file)
@@ -1054,10 +1054,7 @@ function course_delete_module($cmid, $async = false) {
         }
     }
 
-    // Delete activity context questions and question categories.
-    $showinfo = !defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0';
-
-    question_delete_activity($cm, $showinfo);
+    question_delete_activity($cm);
 
     // Call the delete_instance function, if it returns false throw an exception.
     if (!$deleteinstancefunction($cm->instance)) {
index af5d289..bb7b3b9 100644 (file)
@@ -709,7 +709,7 @@ class core_course_renderer extends plugin_renderer_base {
                 'class' => 'iconlarge activityicon', 'alt' => '', 'role' => 'presentation', 'aria-hidden' => 'true')) .
                 html_writer::tag('span', $instancename . $altname, array('class' => 'instancename'));
         if ($mod->uservisible) {
-            $output .= html_writer::link($url, $activitylink, array('class' => $linkclasses, 'onclick' => $onclick));
+            $output .= html_writer::link($url, $activitylink, array('class' => 'aalink' . $linkclasses, 'onclick' => $onclick));
         } else {
             // We may be displaying this just in order to show information
             // about visibility, without the actual link ($mod->is_visible_on_course_page()).
@@ -1126,6 +1126,58 @@ class core_course_renderer extends plugin_renderer_base {
         return $content;
     }
 
+    /**
+     * Returns HTML to display course name.
+     *
+     * @param coursecat_helper $chelper
+     * @param core_course_list_element $course
+     * @return string
+     */
+    protected function course_name(coursecat_helper $chelper, core_course_list_element $course): string {
+        $content = '';
+        if ($chelper->get_show_courses() >= self::COURSECAT_SHOW_COURSES_EXPANDED) {
+            $nametag = 'h3';
+        } else {
+            $nametag = 'div';
+        }
+        $coursename = $chelper->get_course_formatted_name($course);
+        $coursenamelink = html_writer::link(new moodle_url('/course/view.php', ['id' => $course->id]),
+            $coursename, ['class' => $course->visible ? 'aalink' : 'aalink dimmed']);
+        $content .= html_writer::tag($nametag, $coursenamelink, ['class' => 'coursename']);
+        // If we display course in collapsed form but the course has summary or course contacts, display the link to the info page.
+        $content .= html_writer::start_tag('div', ['class' => 'moreinfo']);
+        if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) {
+            if ($course->has_summary() || $course->has_course_contacts() || $course->has_course_overviewfiles()
+                || $course->has_custom_fields()) {
+                $url = new moodle_url('/course/info.php', ['id' => $course->id]);
+                $image = $this->output->pix_icon('i/info', $this->strings->summary);
+                $content .= html_writer::link($url, $image, ['title' => $this->strings->summary]);
+                // Make sure JS file to expand course content is included.
+                $this->coursecat_include_js();
+            }
+        }
+        $content .= html_writer::end_tag('div');
+        return $content;
+    }
+
+    /**
+     * Returns HTML to display course enrolment icons.
+     *
+     * @param core_course_list_element $course
+     * @return string
+     */
+    protected function course_enrolment_icons(core_course_list_element $course): string {
+        $content = '';
+        if ($icons = enrol_get_course_info_icons($course)) {
+            $content .= html_writer::start_tag('div', ['class' => 'enrolmenticons']);
+            foreach ($icons as $icon) {
+                $content .= $this->render($icon);
+            }
+            $content .= html_writer::end_tag('div');
+        }
+        return $content;
+    }
+
     /**
      * Displays one course in the list of courses.
      *
@@ -1150,11 +1202,8 @@ class core_course_renderer extends plugin_renderer_base {
         }
         $content = '';
         $classes = trim('coursebox clearfix '. $additionalclasses);
-        if ($chelper->get_show_courses() >= self::COURSECAT_SHOW_COURSES_EXPANDED) {
-            $nametag = 'h3';
-        } else {
+        if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) {
             $classes .= ' collapsed';
-            $nametag = 'div';
         }
 
         // .coursebox
@@ -1165,128 +1214,151 @@ class core_course_renderer extends plugin_renderer_base {
         ));
 
         $content .= html_writer::start_tag('div', array('class' => 'info'));
-
-        // course name
-        $coursename = $chelper->get_course_formatted_name($course);
-        $coursenamelink = html_writer::link(new moodle_url('/course/view.php', array('id' => $course->id)),
-                                            $coursename, array('class' => $course->visible ? '' : 'dimmed'));
-        $content .= html_writer::tag($nametag, $coursenamelink, array('class' => 'coursename'));
-        // If we display course in collapsed form but the course has summary or course contacts, display the link to the info page.
-        $content .= html_writer::start_tag('div', array('class' => 'moreinfo'));
-        if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) {
-            if ($course->has_summary() || $course->has_course_contacts() || $course->has_course_overviewfiles()
-                    || $course->has_custom_fields()) {
-                $url = new moodle_url('/course/info.php', array('id' => $course->id));
-                $image = $this->output->pix_icon('i/info', $this->strings->summary);
-                $content .= html_writer::link($url, $image, array('title' => $this->strings->summary));
-                // Make sure JS file to expand course content is included.
-                $this->coursecat_include_js();
-            }
-        }
-        $content .= html_writer::end_tag('div'); // .moreinfo
-
-        // print enrolmenticons
-        if ($icons = enrol_get_course_info_icons($course)) {
-            $content .= html_writer::start_tag('div', array('class' => 'enrolmenticons'));
-            foreach ($icons as $pix_icon) {
-                $content .= $this->render($pix_icon);
-            }
-            $content .= html_writer::end_tag('div'); // .enrolmenticons
-        }
-
-        $content .= html_writer::end_tag('div'); // .info
+        $content .= $this->course_name($chelper, $course);
+        $content .= $this->course_enrolment_icons($course);
+        $content .= html_writer::end_tag('div');
 
         $content .= html_writer::start_tag('div', array('class' => 'content'));
         $content .= $this->coursecat_coursebox_content($chelper, $course);
-        $content .= html_writer::end_tag('div'); // .content
+        $content .= html_writer::end_tag('div');
 
         $content .= html_writer::end_tag('div'); // .coursebox
         return $content;
     }
 
     /**
-     * Returns HTML to display course content (summary, course contacts and optionally category name)
-     *
-     * This method is called from coursecat_coursebox() and may be re-used in AJAX
+     * Returns HTML to display course summary.
      *
-     * @param coursecat_helper $chelper various display options
-     * @param stdClass|core_course_list_element $course
+     * @param coursecat_helper $chelper
+     * @param core_course_list_element $course
      * @return string
      */
-    protected function coursecat_coursebox_content(coursecat_helper $chelper, $course) {
-        global $CFG;
-        if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) {
-            return '';
-        }
-        if ($course instanceof stdClass) {
-            $course = new core_course_list_element($course);
-        }
+    protected function course_summary(coursecat_helper $chelper, core_course_list_element $course): string {
         $content = '';
-
-        // display course summary
         if ($course->has_summary()) {
-            $content .= html_writer::start_tag('div', array('class' => 'summary'));
+            $content .= html_writer::start_tag('div', ['class' => 'summary']);
             $content .= $chelper->get_course_formatted_summary($course,
-                    array('overflowdiv' => true, 'noclean' => true, 'para' => false));
-            $content .= html_writer::end_tag('div'); // .summary
+                array('overflowdiv' => true, 'noclean' => true, 'para' => false));
+            $content .= html_writer::end_tag('div');
         }
+        return $content;
+    }
+
+    /**
+     * Returns HTML to display course contacts.
+     *
+     * @param core_course_list_element $course
+     * @return string
+     */
+    protected function course_contacts(core_course_list_element $course) {
+        $content = '';
+        if ($course->has_course_contacts()) {
+            $content .= html_writer::start_tag('ul', ['class' => 'teachers']);
+            foreach ($course->get_course_contacts() as $coursecontact) {
+                $rolenames = array_map(function ($role) {
+                    return $role->displayname;
+                }, $coursecontact['roles']);
+      &nbs