Merge branch 'MDL-68667-master' of git://github.com/aanabit/moodle
authorSara Arjona <sara@moodle.com>
Wed, 27 May 2020 10:26:46 +0000 (12:26 +0200)
committerSara Arjona <sara@moodle.com>
Wed, 27 May 2020 10:26:46 +0000 (12:26 +0200)
388 files changed:
.eslintignore
.stylelintignore
admin/settings/license.php [new file with mode: 0644]
admin/settings/plugins.php
admin/settings/top.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/lang/en/tool_usertours.php
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/external_backpack.php
badges/classes/helper.php [new file with mode: 0644]
badges/classes/output/external_backpacks_page.php
badges/templates/external_backpacks_page.mustache
badges/tests/badgeslib_test.php
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
contentbank/index.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
grade/edit/letter/index.php
grade/report/grader/lib.php
grade/report/grader/styles.css
grade/report/lib.php
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/deprecated.txt
lang/en/license.php
lang/en/repository.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/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_modal_generallayout.mustache
lib/templates/filemanager_page_generallayout.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/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/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/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/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/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 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 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 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 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 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 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 f4f101b..3b15b1e 100644 (file)
@@ -67,7 +67,7 @@ if (has_capability('moodle/contentbank:upload', $context)) {
     $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 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']);
+                $name = implode(", ", $rolenames).': '.
+                    html_writer::link(new moodle_url('/user/view.php',
+                        ['id' => $coursecontact['user']->id, 'course' => SITEID]),
+                        $coursecontact['username']);
+                $content .= html_writer::tag('li', $name);
+            }
+            $content .= html_writer::end_tag('ul');
+        }
+        return $content;
+    }
+
+    /**
+     * Returns HTML to display course overview files.
+     *
+     * @param core_course_list_element $course
+     * @return string
+     */
+    protected function course_overview_files(core_course_list_element $course): string {
+        global $CFG;
 
-        // display course overview files
         $contentimages = $contentfiles = '';
         foreach ($course->get_course_overviewfiles() as $file) {
             $isimage = $file->is_valid_image();
-            $url = file_encode_url("$CFG->wwwroot/pluginfile.php",
-                    '/'. $file->get_contextid(). '/'. $file->get_component(). '/'.
-                    $file->get_filearea(). $file->get_filepath(). $file->get_filename(), !$isimage);
+            $url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php",
+                '/' . $file->get_contextid() . '/' . $file->get_component() . '/' .
+                $file->get_filearea() . $file->get_filepath() . $file->get_filename(), !$isimage);
             if ($isimage) {
                 $contentimages .= html_writer::tag('div',
-                        html_writer::empty_tag('img', array('src' => $url)),
-                        array('class' => 'courseimage'));
+                    html_writer::empty_tag('img', ['src' => $url]),
+                    ['class' => 'courseimage']);
             } else {
                 $image = $this->output->pix_icon(file_file_icon($file, 24), $file->get_filename(), 'moodle');
-                $filename = html_writer::tag('span', $image, array('class' => 'fp-icon')).
-                        html_writer::tag('span', $file->get_filename(), array('class' => 'fp-filename'));
+                $filename = html_writer::tag('span', $image, ['class' => 'fp-icon']).
+                    html_writer::tag('span', $file->get_filename(), ['class' => 'fp-filename']);
                 $contentfiles .= html_writer::tag('span',
-                        html_writer::link($url, $filename),
-                        array('class' => 'coursefile fp-filename-icon'));
+                    html_writer::link($url, $filename),
+                    ['class' => 'coursefile fp-filename-icon']);
             }
         }
-        $content .= $contentimages. $contentfiles;
-
-        // Display course contacts. See core_course_list_element::get_course_contacts().
-        if ($course->has_course_contacts()) {
-            $content .= html_writer::start_tag('ul', array('class' => 'teachers'));
-            foreach ($course->get_course_contacts() as $coursecontact) {
-                $rolenames = array_map(function ($role) {
-                    return $role->displayname;
-                }, $coursecontact['roles']);
-                $name = implode(", ", $rolenames).': '.
-                        html_writer::link(new moodle_url('/user/view.php',
-                                array('id' => $coursecontact['user']->id, 'course' => SITEID)),
-                            $coursecontact['username']);
-                $content .= html_writer::tag('li', $name);
-            }
-            $content .= html_writer::end_tag('ul'); // .teachers
-        }
+        return $contentimages . $contentfiles;
+    }
 
-        // display course category if necessary (for example in search results)
+    /**
+     * Returns HTML to display course category name.
+     *
+     * @param coursecat_helper $chelper
+     * @param core_course_list_element $course
+     * @return string
+     */
+    protected function course_category_name(coursecat_helper $chelper, core_course_list_element $course): string {
+        $content = '';
+        // Display course category if necessary (for example in search results).
         if ($chelper->get_show_courses() == self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT) {
             if ($cat = core_course_category::get($course->category, IGNORE_MISSING)) {
-                $content .= html_writer::start_tag('div', array('class' => 'coursecat'));
+                $content .= html_writer::start_tag('div', ['class' => 'coursecat']);
                 $content .= get_string('category').': '.
-                        html_writer::link(new moodle_url('/course/index.php', array('categoryid' => $cat->id)),
-                                $cat->get_formatted_name(), array('class' => $cat->visible ? '' : 'dimmed'));
-                $content .= html_writer::end_tag('div'); // .coursecat
+                    html_writer::link(new moodle_url('/course/index.php', ['categoryid' => $cat->id]),
+                        $cat->get_formatted_name(), ['class' => $cat->visible ? '' : 'dimmed']);
+                $content .= html_writer::end_tag('div');
             }
         }
+        return $content;
+    }
 
-        // Display custom fields.
+    /**
+     * Returns HTML to display course custom fields.
+     *
+     * @param core_course_list_element $course
+     * @return string
+     */
+    protected function course_custom_fields(core_course_list_element $course): string {
+        $content = '';
         if ($course->has_custom_fields()) {
             $handler = core_course\customfield\course_handler::create();
             $customfields = $handler->display_custom_fields_data($course->get_custom_fields());
             $content .= \html_writer::tag('div', $customfields, ['class' => 'customfields-container']);
         }
+        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
+     *
+     * @param coursecat_helper $chelper various display options
+     * @param stdClass|core_course_list_element $course
+     * @return string
+     */
+    protected function coursecat_coursebox_content(coursecat_helper $chelper, $course) {
+        if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) {
+            return '';
+        }
+        if ($course instanceof stdClass) {
+            $course = new core_course_list_element($course);
+        }
+        $content = $this->course_summary($chelper, $course);
+        $content .= $this->course_overview_files($course);
+        $content .= $this->course_contacts($course);
+        $content .= $this->course_category_name($chelper, $course);
+        $content .= $this->course_custom_fields($course);
         return $content;
     }
 
@@ -1576,7 +1648,7 @@ class core_course_renderer extends plugin_renderer_base {
         }
         $content .= html_writer::start_tag('div', array('class' => 'info'));
 
-        $content .= html_writer::tag(($depth > 1) ? 'h4' : 'h3', $categoryname, array('class' => 'categoryname'));
+        $content .= html_writer::tag(($depth > 1) ? 'h4' : 'h3', $categoryname, array('class' => 'categoryname aabtn'));
         $content .= html_writer::end_tag('div'); // .info
 
         // add category content to the output
@@ -1610,7 +1682,7 @@ class core_course_renderer extends plugin_renderer_base {
 
         if ($coursecat->get_children_count()) {
             $classes = array(
-                'collapseexpand',
+                'collapseexpand', 'aabtn'
             );
 
             // Check if the category content contains subcategories with children's content loaded.
@@ -2319,7 +2391,7 @@ class core_course_renderer extends plugin_renderer_base {
         }
         $output = html_writer::link('#' . $skipdivid,
             get_string('skipa', 'access', core_text::strtolower(strip_tags($header))),
-            array('class' => 'skip-block skip'));
+            array('class' => 'skip-block skip aabtn'));
 
         // Wrap frontpage part in div container.
         $output .= html_writer::start_tag('div', array('id' => $contentsdivid));
index 74300b9..b24636c 100644 (file)
@@ -57,7 +57,7 @@
                     </div>
                     {{/showshortname}}
                 </div>
-                <a href="{{viewurl}}" class="coursename mr-2">
+                <a href="{{viewurl}}" class="aalink coursename mr-2">
                     {{> core_course/favouriteicon }}
                     <span class="sr-only">
                             {{#str}}aria:coursename, core_course{{/str}}
index ccebf94..82e6de3 100644 (file)
@@ -29,7 +29,7 @@
         "icon": "<img class='icon' src='http://urltooptionicon'>"
     }
 }}
-<div role="menuitem" tabindex="-1" aria-label="{{title}}" class="option d-block text-center py-1 mb-1" data-region="chooser-option-container" data-internal="{{name}}" data-modname="{{componentname}}_{{link}}">
+<div role="menuitem" tabindex="-1" aria-label="{{title}}" class="option d-block text-center py-1 mb-1 aabtn" data-region="chooser-option-container" data-internal="{{name}}" data-modname="{{componentname}}_{{link}}">
     <div class="optioninfo w-100" data-region="chooser-option-info-container">
         <a class="d-block" href="{{link}}" title="{{#str}} addnew, moodle, {{title}} {{/str}}" tabindex="-1" data-action="add-chooser-option">
             <span class="optionicon d-block">
index 6847675..ad17e53 100644 (file)
@@ -1883,7 +1883,8 @@ class behat_course extends behat_base {
 
         $exception = new ExpectationException('"' . $categoryname . '" category can not be found', $this->getSession());
         $categoryliteral = behat_context_helper::escape($categoryname);
-        $xpath = "//div[@class='info']/descendant::*[" . implode(' or ', $headingtags) . "][@class='categoryname'][./descendant::a[.=$categoryliteral]]";
+        $xpath = "//div[@class='info']/descendant::*[" . implode(' or ', $headingtags) .
+            "][contains(@class,'categoryname')][./descendant::a[.=$categoryliteral]]";
         $node = $this->find('xpath', $xpath, $exception);
         $node->click();
 
index 5489939..8d8b26b 100644 (file)
@@ -1080,4 +1080,26 @@ class core_course_category_testcase extends advanced_testcase {
         }
         return $draftid;
     }
+
+    /**
+     * This test ensures that is the list of courses in a category can be retrieved while a course is being deleted.
+     */
+    public function test_get_courses_during_delete() {
+        global $DB;
+        $category = self::getDataGenerator()->create_category();
+        $course = self::getDataGenerator()->create_course(['category' => $category->id]);
+        $othercourse = self::getDataGenerator()->create_course(['category' => $category->id]);
+        $coursecategory = core_course_category::get($category->id);
+        // Get a list of courses before deletion to populate the cache.
+        $originalcourses = $coursecategory->get_courses();
+        $this->assertCount(2, $originalcourses);
+        $this->assertArrayHasKey($course->id, $originalcourses);
+        $this->assertArrayHasKey($othercourse->id, $originalcourses);
+        // Simulate the course deletion process being part way though.
+        $DB->delete_records('course', ['id' => $course->id]);
+        // Get the list of courses while a deletion is in progress.
+        $courses = $coursecategory->get_courses();
+        $this->assertCount(1, $courses);
+        $this->assertArrayHasKey($othercourse->id, $courses);
+    }
 }
index fef3933..8f00f15 100644 (file)
@@ -1653,11 +1653,8 @@ class core_course_courselib_testcase extends advanced_testcase {
             case 'quiz':
                 $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
                 $qcat = $qgen->create_question_category(array('contextid' => $modcontext->id));
-                $questions = array(
-                    $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
-                    $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
-                );
-                $this->expectOutputRegex('/'.get_string('unusedcategorydeleted', 'question').'/');
+                $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
+                $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
                 break;
             default:
                 break;
index 195a6eb..45dc3a1 100644 (file)
@@ -5,6 +5,15 @@ information provided here is intended especially for developers.
 
 * The function get_module_metadata is now deprecated. Please use \core_course\local\service\content_item_service instead.
 * Activity module names are now PARAM_ALPHANUM instead of PARAM_ALPHA so integers can be used in activity module names
+* The following functions have been added to core_course_renderer class to have more granularity. They can be overriden in
+  extending classes:
+  - course_name
+  - course_enrolment_icons
+  - course_summary
+  - course_contacts
+  - course_overview_files
+  - course_category_name
+  - course_custom_fields
 
 === 3.8 ===
 
index a7b60d4..49ed857 100644 (file)
@@ -92,6 +92,15 @@ table {
         echo \html_writer::end_tag('tr');
     }
 
+    /**
+     * Method to define whether the dataformat supports export of HTML
+     *
+     * @return bool
+     */
+    public function supports_html(): bool {
+        return true;
+    }
+
     /**
      * Write a single record
      *
@@ -99,6 +108,8 @@ table {
      * @param int $rownum
      */
     public function write_record($record, $rownum) {
+        $record = $this->format_record($record);
+
         echo \html_writer::start_tag('tr');
         foreach ($record as $cell) {
             echo \html_writer::tag('td', $cell);
index 599f3fe..effd83e 100644 (file)
@@ -80,7 +80,7 @@ class writer extends \core\dataformat\base {
             echo ",";
         }
 
-        echo json_encode($record, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+        echo json_encode($this->format_record($record), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
 
         $this->sheetdatadded = true;
     }
index 1fce439..2203af5 100644 (file)
@@ -93,16 +93,52 @@ class writer extends \core\dataformat\base {
         $this->print_heading();
     }
 
+    /**
+     * Method to define whether the dataformat supports export of HTML
+     *
+     * @return bool
+     */
+    public function supports_html(): bool {
+        return true;
+    }
+
+    /**
+     * When exporting images, we need to return their Base64 encoded content. Otherwise TCPDF will create a HTTP
+     * request for them, which will lead to the login page (i.e. not the image it expects) and throw an exception
+     *
+     * Note: ideally we would copy the file to a temp location and return it's path, but a bug in TCPDF currently
+     * prevents that
+     *
+     * @param \stored_file $file
+     * @return string|null
+     */
+    protected function export_html_image_source(\stored_file $file): ?string {
+        // Set upper dimensions for embedded images.
+        $resizedimage = $file->resize_image(400, 300);
+
+        return '@' . base64_encode($resizedimage);
+    }
+
+    /**
+     * Write a single record
+     *
+     * @param array $record
+     * @param int $rownum
+     */
     public function write_record($record, $rownum) {
         $rowheight = 0;
 
-        // If $record is an object convert it to an array.
-        if (is_object($record)) {
-            $record = (array)$record;
-        }
-
+        $record = $this->format_record($record);
         foreach ($record as $cell) {
-            $rowheight = max($rowheight, $this->pdf->getStringHeight($this->colwidth, $cell, false, true, '', 1));
+            // We need to calculate the row height (accounting for any content). Unfortunately TCPDF doesn't provide an easy
+            // method to do that, so we create a second PDF inside a transaction, add cell content and use the largest cell by
+            // height. Solution similar to that at https://stackoverflow.com/a/1943096.
+            $pdf2 = clone $this->pdf;
+            $pdf2->startTransaction();
+            $pdf2->AddPage('L');
+            $pdf2->writeHTMLCell($this->colwidth, 0, '', '', $cell, 1, 1, false, true, 'L');
+            $rowheight = max($rowheight, $pdf2->getY() - $pdf2->getMargins()['top']);
+            $pdf2->rollbackTransaction();
         }
 
         $margins = $this->pdf->getMargins();
@@ -123,7 +159,7 @@ class writer extends \core\dataformat\base {
             // Determine whether we're at the last element of the record.
             $nextposition = ($lastkey === $key) ? 1 : 0;
             // Write the element.
-            $this->pdf->Multicell($this->colwidth, $rowheight, $cell, 1, 'L', false, $nextposition);
+            $this->pdf->writeHTMLCell($this->colwidth, $rowheight, '', '', $cell, 1, $nextposition, false, true, 'L');
         }
     }
 
diff --git a/dataformat/pdf/tests/writer_test.php b/dataformat/pdf/tests/writer_test.php
new file mode 100644 (file)
index 0000000..e1218a0
--- /dev/null
@@ -0,0 +1,74 @@
+<?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 the dataformat_pdf writer
+ *
+ * @package    dataformat_pdf
+ * @copyright  2020 Paul Holden <paulh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace dataformat_pdf;
+
+use core\dataformat;
+use context_system;
+use html_writer;
+use moodle_url;
+
+/**
+ * Writer tests
+ *
+ * @package    dataformat_pdf
+ * @copyright  2020 Paul Holden <paulh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class writer_testcase extends \advanced_testcase {
+
+    /**
+     * Test writing data whose content contains an image with pluginfile.php source
+     */
+    public function test_write_data_with_pluginfile_image(): void {
+        global $CFG;
+
+        $this->resetAfterTest(true);
+
+        $imagefixture = "{$CFG->dirroot}/lib/filestorage/tests/fixtures/testimage.jpg";
+        $image = get_file_storage()->create_file_from_pathname([
+            'contextid' => context_system::instance()->id,
+            'component' => 'dataformat_pdf',
+            'filearea'  => 'test',
+            'itemid'    => 0,
+            'filepath'  => '/',
+            'filename'  => basename($imagefixture),
+
+        ], $imagefixture);
+
+        $imageurl = moodle_url::make_pluginfile_url($image->get_contextid(), $image->get_component(), $image->get_filearea(),
+            $image->get_itemid(), $image->get_filepath(), $image->get_filename());
+
+        // Insert out test image into the data so it is exported.
+        $columns = ['animal', 'image'];
+        $row = ['cat', html_writer::img($imageurl->out(), 'My image')];
+
+        // Export to file. Assert that the exported file exists.
+        $exportfile = dataformat::write_data('My export', 'pdf', $columns, [$row]);
+        $this->assertFileExists($exportfile);
+
+        // The exported file should be a reasonable size (~275kb).
+        $this->assertGreaterThan(270000, filesize($exportfile));
+    }
+}
index cf4d09e..89eaa5b 100644 (file)
@@ -9,6 +9,13 @@ information provided here is intended especially for developers.
 * Calls to the following dataformat plugin methods have been removed:
   - write_header()
   - write_footer()
+* The following methods have been added to the base class to allow instances to define support for exporting
+  HTML content, with additional support for defining how images should be embedded:
+  - supports_html()
+  - export_html_image_source()
+* Dataformat writers should also call the following method to ensure data is properly formatted before being
+  written, which takes into account prior methods defining support for HTML:
+  - format_record()
 
 === 3.4 ===
 * In order to allow multiple sheets in an exported file the functions write_header() and write_footer() have
index 49e51ba..cd3c979 100644 (file)
@@ -124,9 +124,7 @@ Feature: Teacher can search and enrol users one by one into the course
     And I should see "Student 001"
     And I click on "Enrol users" "button" in the "Enrol users" "dialogue"
     Then I should see "Active" in the "Student 001" "table_row"
-    # The following line is commented out as auto-hidden toasts fire events in the wrong place.
-    # TODO Uncomment this when we upgrade Bootstrap. This issue is fixed in v4.4.0 - see MDL-67386.
-    #And I should see "1 enrolled users"
+    And I should see "1 enrolled users"
 
   @javascript
   Scenario: Searching for a non-existing user
index 62e15b6..2df7fa9 100644 (file)
@@ -166,7 +166,7 @@ class core_files_renderer extends plugin_renderer_base {
     protected function fm_js_template_iconfilename() {
         $rv = '
 <div class="fp-file">
-    <a href="#">
+    <a href="#" class="d-block aabtn">
     <div style="position:relative;">
         <div class="fp-thumbnail"></div>
         <div class="fp-reficons1"></div>
@@ -176,7 +176,8 @@ class core_files_renderer extends plugin_renderer_base {
         <div class="fp-filename text-truncate"></div>
     </div>
     </a>
-    <a class="fp-contextmenu" href="#">'.$this->pix_icon('i/menu', '▶').'</a>
+    <a class="fp-contextmenu btn btn-icon btn-light border icon-no-margin icon-size-3" href="#">
+        <span>'.$this->pix_icon('i/menu', '▶').'</span></a>
 </div>';
         return $rv;
     }
@@ -226,7 +227,7 @@ class core_files_renderer extends plugin_renderer_base {
 <div class="filemanager fp-mkdir-dlg" role="dialog" aria-live="assertive" aria-labelledby="fp-mkdir-dlg-title">
     <div class="fp-mkdir-dlg-text">
         <label id="fp-mkdir-dlg-title">' . get_string('newfoldername', 'repository') . '</label><br/>
-        <input type="text" />
+        <input type="text" class="form-control"/>
     </div>
     <button class="fp-dlg-butcreate btn-primary btn">'.get_string('makeafolder').'</button>
     <button class="fp-dlg-butcancel btn-cancel btn">'.get_string('cancel').'</button>
index 9f4e075..b72e203 100644 (file)
@@ -137,6 +137,10 @@ if (!$edit) {
         redirect($returnurl);
 
     } else if ($data = $mform->get_data()) {
+
+        // Make sure we are updating the cache.
+        $cache = cache::make('core', 'grade_letters');
+
         if (!$admin and empty($data->override)) {
             $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
             foreach ($records as $record) {
@@ -148,6 +152,9 @@ if (!$edit) {
                 ));
                 $event->trigger();
             }
+
+            // Make sure we clear the cache for this context.
+            $cache->delete($context->id);
             redirect($returnurl);
         }
 
@@ -222,6 +229,15 @@ if (!$edit) {
             }
         }
 
+        // Cache the changed letters.
+        if (!empty($letters)) {
+
+            // For some reason, the cache saves it in the order in which they were entered
+            // but we really want to order them in descending order so we sort it here.
+            krsort($letters);
+            $cache->set($context->id, $letters);
+        }
+
         // Delete the unused records.
         foreach($pool as $leftover) {
             $DB->delete_records('grade_letters', array('id' => $leftover->id));
index 53741c4..5227baf 100644 (file)
@@ -710,12 +710,16 @@ class grade_report_grader extends grade_report {
         $rows = $this->get_left_icons_row($rows, $colspan);
 
         $suspendedstring = null;
+
+        $usercount = 0;
         foreach ($this->users as $userid => $user) {
             $userrow = new html_table_row();
             $userrow->id = 'fixed_user_'.$userid;
+            $userrow->attributes['class'] = ($usercount % 2) ? 'userrow even' : 'userrow odd';
 
             $usercell = new html_table_cell();
-            $usercell->attributes['class'] = 'header user';
+            $usercell->attributes['class'] = ($usercount % 2) ? 'header user even' : 'header user odd';
+            $usercount++;
 
             $usercell->header = true;
             $usercell->scope = 'row';
index 3e54686..92d658e 100644 (file)
     white-space: nowrap;
 }
 
-/**
- * Stripped table.
- */
-.path-grade-report-grader .gradeparent tr:nth-of-type(even) .cell {
-    background-color: #f9f9f9;
-}
-
 /**
  * All the floating divs.
  */
     text-align: left;
 }
 
-/**
- * All the floating cells.
- */
-.path-grade-report-grader .gradeparent .floater .cell {
-    background-color: #f9f9f9;
-}
-
 /**
  * The user cells.
  */
     float: left;
 }
 
-.path-grade-report .gradeparent .floater .controls.cell,
-.path-grade-report-grader .gradeparent .controls {
-    background-color: #f3ead8;
-}
-
 .path-grade-report-grader .gradeparent .category {
     text-align: left;
 }
index 7d8f7ed..70be5da 100644 (file)
@@ -378,8 +378,9 @@ abstract class grade_report {
                 $this->currentgroup = -2; // means can not access any groups at all
             }
             if ($this->currentgroup) {
-                $group = groups_get_group($this->currentgroup);
-                $this->currentgroupname     = $group->name;
+                if ($group = groups_get_group($this->currentgroup)) {
+                    $this->currentgroupname = $group->name;
+                }
                 $this->groupsql             = " JOIN {groups_members} gm ON gm.userid = u.id ";
                 $this->groupwheresql        = " AND gm.groupid = :gr_grpid ";
                 $this->groupwheresql_params = array('gr_grpid'=>$this->currentgroup);
index 81c7625..6cd3484 100644 (file)
@@ -502,4 +502,43 @@ class api {
 
         return ($h5p) ? $h5p : null;
     }
+
+    /**
+     * Return the H5P export information file when the file has been deployed.
+     * Otherwise, return null if H5P file:
+     * i) has not been deployed.
+     * ii) has changed the content.
+     *
+     * The information returned will be:
+     * - filename, filepath, mimetype, filesize, timemodified and fileurl.
+     *
+     * @param int $contextid ContextId of the H5P activity.
+     * @param factory $factory The \core_h5p\factory object.
+     * @param string $component component
+     * @param string $filearea file area
+     * @return array|null Return file info otherwise null.
+     */
+    public static function get_export_info_from_context_id(int $contextid,
+        factory $factory,
+        string $component,
+        string $filearea): ?array {
+
+        $core = $factory->get_core();
+        $fs = get_file_storage();
+        $files = $fs->get_area_files($contextid, $component, $filearea, 0, 'id', false);
+        $file = reset($files);
+
+        if ($h5p = self::get_content_from_pathnamehash($file->get_pathnamehash())) {
+            if ($h5p->contenthash == $file->get_contenthash()) {
+                $content = $core->loadContent($h5p->id);
+                $slug = $content['slug'] ? $content['slug'] . '-' : '';
+                $filename = "{$slug}{$content['id']}.h5p";
+                $deployedfile = helper::get_export_info($filename, null, $factory);
+
+                return $deployedfile;
+            }
+        }
+
+        return null;
+    }
 }
index 93780fa..8e85ffc 100644 (file)
@@ -429,4 +429,50 @@ class helper {
 
         return $strings;
     }
+
+    /**
+     * Get the information related to the H5P export file.
+     * The information returned will be:
+     * - filename, filepath, mimetype, filesize, timemodified and fileurl.
+     *
+     * @param  string $exportfilename The H5P export filename (with slug).
+     * @param  \moodle_url $url The URL of the exported file.
+     * @param  factory $factory The \core_h5p\factory object
+     * @return array|null The information export file otherwise null.
+     */
+    public static function get_export_info(string $exportfilename, \moodle_url $url = null, ?factory $factory = null): ?array {
+
+        if (!$factory) {
+            $factory = new factory();
+        }
+        $core = $factory->get_core();
+
+        // Get export file.
+        if (!$fileh5p = $core->fs->get_export_file($exportfilename)) {
+            return null;
+        }
+
+        // Build the export info array.
+        $file = [];
+        $file['filename'] = $fileh5p->get_filename();
+        $file['filepath'] = $fileh5p->get_filepath();
+        $file['mimetype'] = $fileh5p->get_mimetype();
+        $file['filesize'] = $fileh5p->get_filesize();
+        $file['timemodified'] = $fileh5p->get_timemodified();
+
+        if (!$url) {
+            $url  = \moodle_url::make_webservice_pluginfile_url(
+                $fileh5p->get_contextid(),
+                $fileh5p->get_component(),
+                $fileh5p->get_filearea(),
+                '',
+                '',
+                $fileh5p->get_filename()
+            );
+        }
+
+        $file['fileurl'] = $url->out(false);
+
+        return $file;
+    }
 }
index 23c5420..9cbc121 100644 (file)
@@ -456,7 +456,7 @@ class player {
     }
 
     /**
-     * Return the export file for Mobile App.
+     * Return the info export file for Mobile App.
      *
      * @return array
      */
@@ -467,23 +467,8 @@ class player {
         $path = $exporturl->out_as_local_url();
         $parts = explode('/', $path);
         $filename = array_pop($parts);
-        // Get the the export file.
-        $systemcontext = \context_system::instance();
-        $fs = get_file_storage();
-        $fileh5p = $fs->get_file($systemcontext->id,
-            \core_h5p\file_storage::COMPONENT,
-            \core_h5p\file_storage::EXPORT_FILEAREA,
-            0,
-            '/',
-            $filename);
-        // Get the options that the Mobile App needs.
-        $file = [];
-        $file['filename'] = $fileh5p->get_filename();
-        $file['filepath'] = $fileh5p->get_filepath();
-        $file['mimetype'] = $fileh5p->get_mimetype();
-        $file['filesize'] = $fileh5p->get_filesize();
-        $file['timemodified'] = $fileh5p->get_timemodified();
-        $file['fileurl'] = $exporturl->out(false);
+        // Get the required info from the export file to be able to get the export file by third apps.
+        $file = helper::get_export_info($filename, $exporturl);
 
         return $file;
     }
index f7f01b8..e1ec76e 100644 (file)
@@ -451,4 +451,56 @@ class api_testcase extends \advanced_testcase {
         api::delete_content_from_pluginfile_url($url->out(), $factory);
         $this->assertEquals(0, $DB->count_records('h5p'));
     }
+
+    /**
+     * Test the behaviour of get_export_info_from_context_id().
+     */
+    public function test_get_export_info_from_context_id(): void {
+        global $DB;
+
+        $this->setRunTestInSeparateProcess(true);
+        $this->resetAfterTest();
+        $factory = new factory();
+
+        // Create the H5P data.
+        $filename = 'find-the-words.h5p';
+        $syscontext = \context_system::instance();
+
+        // Test scenario 1: H5P exists and deployed.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
+        $fakeexportfile = $generator->create_export_file($filename,
+            $syscontext->id,
+            \core_h5p\file_storage::COMPONENT,
+            \core_h5p\file_storage::EXPORT_FILEAREA);
+
+        $exportfile = api::get_export_info_from_context_id($syscontext->id,
+            $factory,
+            \core_h5p\file_storage::COMPONENT,
+            \core_h5p\file_storage::EXPORT_FILEAREA);
+        $this->assertEquals($fakeexportfile['filename'], $exportfile['filename']);
+        $this->assertEquals($fakeexportfile['filepath'], $exportfile['filepath']);
+        $this->assertEquals($fakeexportfile['filesize'], $exportfile['filesize']);
+        $this->assertEquals($fakeexportfile['timemodified'], $exportfile['timemodified']);
+        $this->assertEquals($fakeexportfile['fileurl'], $exportfile['fileurl']);
+
+        // Test scenario 2: H5P exist, deployed but the content has changed.
+        // We need to change the contenthash to simulate the H5P file was changed.
+        $h5pfile = $DB->get_record('h5p', []);
+        $h5pfile->contenthash = sha1('testedit');
+        $DB->update_record('h5p', $h5pfile);
+        $exportfile = api::get_export_info_from_context_id($syscontext->id,
+            $factory,
+            \core_h5p\file_storage::COMPONENT,
+            \core_h5p\file_storage::EXPORT_FILEAREA);
+        $this->assertNull($exportfile);
+
+        // Tests scenario 3: H5P is not deployed.
+        // We need to delete the H5P record to simulate the H5P was not deployed.
+        $DB->delete_records('h5p', ['id' => $h5pfile->id]);
+        $exportfile = api::get_export_info_from_context_id($syscontext->id,
+            $factory,
+            \core_h5p\file_storage::COMPONENT,
+            \core_h5p\file_storage::EXPORT_FILEAREA);
+        $this->assertNull($exportfile);
+    }
 }
index 737c7cc..42f1aea 100644 (file)
@@ -55,56 +55,45 @@ class core_h5p_external_testcase extends externallib_advanced_testcase {
      * test_get_trusted_h5p_file description
      */
     public function test_get_trusted_h5p_file() {
-        global $DB;
         $this->resetAfterTest(true);
         $this->setAdminUser();
 
         // This is a valid .H5P file.
         $filename = 'find-the-words.h5p';
-        $path = __DIR__ . '/fixtures/'.$filename;
         $syscontext = \context_system::instance();
-        $filerecord = [
-            'contextid' => $syscontext->id,
-            'component' => \core_h5p\file_storage::COMPONENT,
-            'filearea'  => 'unittest',
-            'itemid'    => 0,
-            'filepath'  => '/',
-            'filename'  => $filename,
-        ];
-        // Load the h5p file into DB.
-        $fs = get_file_storage();
-        $file = $fs->create_file_from_pathname($filerecord, $path);
+
+        // Create a fake export H5P file with normal pluginfile call.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
+        $deployedfile = $generator->create_export_file($filename,
+            $syscontext->id,
+            \core_h5p\file_storage::COMPONENT,
+            \core_h5p\file_storage::EXPORT_FILEAREA,
+            $generator::PLUGINFILE);
+
         // Make the URL to pass to the WS.
         $url  = \moodle_url::make_pluginfile_url(
             $syscontext->id,
             \core_h5p\file_storage::COMPONENT,
-            'unittest',
+            \core_h5p\file_storage::EXPORT_FILEAREA,
             0,
             '/',
             $filename
         );
+
         // Call the WS.
-        $result = external::get_trusted_h5p_file($url->out(), 0, 0, 0, 0);
+        $result = external::get_trusted_h5p_file($url->out(false), 0, 0, 0, 0);
         $result = external_api::clean_returnvalue(external::get_trusted_h5p_file_returns(), $result);
         // Expected result: Just 1 record on files and none on warnings.
         $this->assertCount(1, $result['files']);
         $this->assertCount(0, $result['warnings']);
-        // Get the export file in the DB to compare with the ws's results.
-        $fileh5p = $this->get_export_file($filename, $file->get_pathnamehash());
-        $fileh5purl  = \moodle_url::make_pluginfile_url(
-            $syscontext->id,
-            \core_h5p\file_storage::COMPONENT,
-            \core_h5p\file_storage::EXPORT_FILEAREA,
-            '',
-            '',
-            $fileh5p->get_filename()
-        );
-        $this->assertEquals($fileh5p->get_filepath(), $result['files'][0]['filepath']);
-        $this->assertEquals($fileh5p->get_mimetype(), $result['files'][0]['mimetype']);
-        $this->assertEquals($fileh5p->get_filesize(), $result['files'][0]['filesize']);
-        $this->assertEquals($fileh5p->get_timemodified(), $result['files'][0]['timemodified']);
-        $this->assertEquals($fileh5p->get_filename(), $result['files'][0]['filename']);
-        $this->assertEquals($fileh5purl->out(), $result['files'][0]['fileurl']);
+
+        // Check info export file to compare with the ws's results.
+        $this->assertEquals($deployedfile['filepath'], $result['files'][0]['filepath']);
+        $this->assertEquals($deployedfile['mimetype'], $result['files'][0]['mimetype']);
+        $this->assertEquals($deployedfile['filesize'], $result['files'][0]['filesize']);
+        $this->assertEquals($deployedfile['timemodified'], $result['files'][0]['timemodified']);
+        $this->assertEquals($deployedfile['filename'], $result['files'][0]['filename']);
+        $this->assertEquals($deployedfile['fileurl'], $result['files'][0]['fileurl']);
     }
 
     /**
@@ -170,56 +159,41 @@ class core_h5p_external_testcase extends externallib_advanced_testcase {
      * using webservice/pluginfile.php as url param.
      */
     public function test_allow_webservice_pluginfile_in_url_param() {
-        global $DB;
         $this->resetAfterTest(true);
         $this->setAdminUser();
 
         // This is a valid .H5P file.
         $filename = 'find-the-words.h5p';
-        $path = __DIR__ . '/fixtures/'.$filename;
         $syscontext = \context_system::instance();
-        $filerecord = [
-            'contextid' => $syscontext->id,
-            'component' => \core_h5p\file_storage::COMPONENT,
-            'filearea'  => 'unittest',
-            'itemid'    => 0,
-            'filepath'  => '/',
-            'filename'  => $filename,
-        ];
-        // Load the h5p file into DB.
-        $fs = get_file_storage();
-        $file = $fs->create_file_from_pathname($filerecord, $path);
+
+        // Create a fake export H5P file with webservice call.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
+        $deployedfile = $generator->create_export_file($filename,
+            $syscontext->id,
+            \core_h5p\file_storage::COMPONENT,
+            \core_h5p\file_storage::EXPORT_FILEAREA);
+
         // Make the URL to pass to the WS.
         $url  = \moodle_url::make_webservice_pluginfile_url(
             $syscontext->id,
             \core_h5p\file_storage::COMPONENT,
-            'unittest',
+            \core_h5p\file_storage::EXPORT_FILEAREA,
             0,
             '/',
             $filename
         );
+
         // Call the WS.
         $result = external::get_trusted_h5p_file($url->out(), 0, 0, 0, 0);
         $result = external_api::clean_returnvalue(external::get_trusted_h5p_file_returns(), $result);
-        // Expected result: Just 1 record on files and none on warnings.
-        $this->assertCount(1, $result['files']);
-        $this->assertCount(0, $result['warnings']);
-        // Get the export file in the DB to compare with the ws's results.
-        $fileh5p = $this->get_export_file($filename, $file->get_pathnamehash());
-        $fileh5purl  = \moodle_url::make_webservice_pluginfile_url(
-            $syscontext->id,
-            \core_h5p\file_storage::COMPONENT,
-            \core_h5p\file_storage::EXPORT_FILEAREA,
-            '',
-            '',
-            $fileh5p->get_filename()
-        );
-        $this->assertEquals($fileh5p->get_filepath(), $result['files'][0]['filepath']);
-        $this->assertEquals($fileh5p->get_mimetype(), $result['files'][0]['mimetype']);
-        $this->assertEquals($fileh5p->get_filesize(), $result['files'][0]['filesize']);
-        $this->assertEquals($fileh5p->get_timemodified(), $result['files'][0]['timemodified']);
-        $this->assertEquals($fileh5p->get_filename(), $result['files'][0]['filename']);
-        $this->assertEquals($fileh5purl->out(), $result['files'][0]['fileurl']);
+
+        // Check info export file to compare with the ws's results.
+        $this->assertEquals($deployedfile['filepath'], $result['files'][0]['filepath']);
+        $this->assertEquals($deployedfile['mimetype'], $result['files'][0]['mimetype']);
+        $this->assertEquals($deployedfile['filesize'], $result['files'][0]['filesize']);
+        $this->assertEquals($deployedfile['timemodified'], $result['files'][0]['timemodified']);
+        $this->assertEquals($deployedfile['filename'], $result['files'][0]['filename']);
+        $this->assertEquals($deployedfile['fileurl'], $result['files'][0]['fileurl']);
     }
 
     /**
@@ -227,83 +201,46 @@ class core_h5p_external_testcase extends externallib_advanced_testcase {
      * using tokenpluginfile.php as url param.
      */
     public function test_allow_tokenluginfile_in_url_param() {
-        global $DB;
         $this->resetAfterTest(true);
         $this->setAdminUser();
 
         // This is a valid .H5P file.
         $filename = 'find-the-words.h5p';
-        $path = __DIR__ . '/fixtures/'.$filename;
         $syscontext = \context_system::instance();
-        $filerecord = [
-            'contextid' => $syscontext->id,
-            'component' => \core_h5p\file_storage::COMPONENT,
-            'filearea'  => 'unittest',
-            'itemid'    => 0,
-            'filepath'  => '/',
-            'filename'  => $filename,
-        ];
-        // Load the h5p file into DB.
-        $fs = get_file_storage();
-        $file = $fs->create_file_from_pathname($filerecord, $path);
+
+        // Create a fake export H5P file with tokenfile call.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
+        $deployedfile = $generator->create_export_file($filename,
+            $syscontext->id,
+            \core_h5p\file_storage::COMPONENT,
+            \core_h5p\file_storage::EXPORT_FILEAREA,
+            $generator::TOKENPLUGINFILE);
+
         // Make the URL to pass to the WS.
         $url  = \moodle_url::make_pluginfile_url(
             $syscontext->id,
             \core_h5p\file_storage::COMPONENT,
-            'unittest',
+            \core_h5p\file_storage::EXPORT_FILEAREA,
             0,
             '/',
             $filename,
             false,
             true
         );
+
         // Call the WS.
-        $result = external::get_trusted_h5p_file($url->out(), 0, 0, 0, 0);
+        $result = external::get_trusted_h5p_file($url->out(false), 0, 0, 0, 0);
         $result = external_api::clean_returnvalue(external::get_trusted_h5p_file_returns(), $result);
         // Expected result: Just 1 record on files and none on warnings.
         $this->assertCount(1, $result['files']);
         $this->assertCount(0, $result['warnings']);
-        // Get the export file in the DB to compare with the ws's results.
-        $fileh5p = $this->get_export_file($filename, $file->get_pathnamehash());
-        $fileh5purl  = \moodle_url::make_pluginfile_url(
-            $syscontext->id,
-            \core_h5p\file_storage::COMPONENT,
-            \core_h5p\file_storage::EXPORT_FILEAREA,
-            '',
-            '',
-            $fileh5p->get_filename(),
-            false,
-            true
-        );
-        $this->assertEquals($fileh5p->get_filepath(), $result['files'][0]['filepath']);
-        $this->assertEquals($fileh5p->get_mimetype(), $result['files'][0]['mimetype']);
-        $this->assertEquals($fileh5p->get_filesize(), $result['files'][0]['filesize']);
-        $this->assertEquals($fileh5p->get_timemodified(), $result['files'][0]['timemodified']);
-        $this->assertEquals($fileh5p->get_filename(), $result['files'][0]['filename']);
-        $this->assertEquals($fileh5purl->out(), $result['files'][0]['fileurl']);
-    }
 
-    /**
-     * Get the H5P export file.
-     *
-     * @param string $filename
-     * @param string $pathnamehash
-     * @return stored_file
-     */
-    protected function get_export_file($filename, $pathnamehash) {
-        global $DB;
-
-        // Simulate the filenameexport using slug as H5P does.
-        $id = $DB->get_field('h5p', 'id', ['pathnamehash' => $pathnamehash]);
-        $filenameexport = basename($filename, '.h5p').'-'.$id.'-'.$id.'.h5p';
-        $syscontext = \context_system::instance();
-        $fs = get_file_storage();
-        $fileh5p = $fs->get_file($syscontext->id,
-            \core_h5p\file_storage::COMPONENT,
-            \core_h5p\file_storage::EXPORT_FILEAREA,
-            0,
-            '/',
-            $filenameexport);
-        return $fileh5p;
+        // Check info export file to compare with the ws's results.
+        $this->assertEquals($deployedfile['filepath'], $result['files'][0]['filepath']);
+        $this->assertEquals($deployedfile['mimetype'], $result['files'][0]['mimetype']);
+        $this->assertEquals($deployedfile['filesize'], $result['files'][0]['filesize']);
+        $this->assertEquals($deployedfile['timemodified'], $result['files'][0]['timemodified']);
+        $this->assertEquals($deployedfile['filename'], $result['files'][0]['filename']);
+        $this->assertEquals($deployedfile['fileurl'], $result['files'][0]['fileurl']);
     }
 }
index 9cf4313..88c229d 100644 (file)
@@ -25,6 +25,8 @@
 
 use core_h5p\local\library\autoloader;
 use core_h5p\core;
+use core_h5p\player;
+use core_h5p\factory;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -38,6 +40,13 @@ defined('MOODLE_INTERNAL') || die();
  */
 class core_h5p_generator extends \component_generator_base {
 
+    /** Url pointing to webservice plugin file. */
+    public const WSPLUGINFILE = 0;
+    /** Url pointing to token plugin file. */
+    public const TOKENPLUGINFILE = 1;
+    /** Url pointing to plugin file. */
+    public const PLUGINFILE = 2;
+
     /**
      * Convenience function to create a file.
      *
@@ -428,4 +437,121 @@ class core_h5p_generator extends \component_generator_base {
         $fs = new file_storage();
         return $fs->create_file_from_string($filerecord, $content);
     }
+
+    /**
+     * Create a fake export H5P deployed file.
+     *
+     * @param string $filename Name of the H5P file to deploy.
+     * @param int $contextid Context id of the H5P activity.
+     * @param string $component component.
+     * @param string $filearea file area.
+     * @param int $typeurl Type of url to create the export url plugin file.
+     * @return array return deployed file information.
+     */
+    public function create_export_file(string $filename, int $contextid,
+        string $component,
+        string $filearea,
+        int $typeurl = self::WSPLUGINFILE): array {
+        global $CFG;
+
+        // We need the autoloader for H5P player.
+        autoloader::register();
+
+        $path = $CFG->dirroot.'/h5p/tests/fixtures/'.$filename;
+        $filerecord = [
+            'contextid' => $contextid,
+            'component' => $component,
+            'filearea'  => $filearea,
+            'itemid'    => 0,
+            'filepath'  => '/',
+            'filename'  => $filename,
+        ];
+        // Load the h5p file into DB.
+        $fs = get_file_storage();
+        if (!$fs->get_file($contextid, $component, $filearea, $filerecord['itemid'], $filerecord['filepath'], $filename)) {
+            $fs->create_file_from_pathname($filerecord, $path);
+        }
+
+        // Make the URL to pass to the player.
+        if ($typeurl == self::WSPLUGINFILE) {
+            $url = \moodle_url::make_webservice_pluginfile_url(
+                $filerecord['contextid'],
+                $filerecord['component'],
+                $filerecord['filearea'],
+                $filerecord['itemid'],
+                $filerecord['filepath'],
+                $filerecord['filename']
+            );
+        } else {
+            $includetoken = false;
+            if ($typeurl == self::TOKENPLUGINFILE) {
+                $includetoken = true;
+            }
+            $url = \moodle_url::make_pluginfile_url(
+                $filerecord['contextid'],
+                $filerecord['component'],
+                $filerecord['filearea'],
+                $filerecord['itemid'],
+                $filerecord['filepath'],
+                $filerecord['filename'],
+                false,
+                $includetoken
+            );
+        }
+
+        $config = new stdClass();
+        $h5pplayer = new player($url->out(false), $config);
+        // We need to add assets to page to create the export file.
+        $h5pplayer->add_assets_to_page();
+
+        // Call the method. We need the id of the new H5P content.
+        $rc = new \ReflectionClass(player::class);
+        $rcp = $rc->getProperty('h5pid');
+        $rcp->setAccessible(true);
+        $h5pid = $rcp->getValue($h5pplayer);
+
+        // Get the info export file.
+        $factory = new factory();
+        $core = $factory->get_core();
+        $content = $core->loadContent($h5pid);
+        $slug = $content['slug'] ? $content['slug'] . '-' : '';
+        $exportfilename = "{$slug}{$h5pid}.h5p";
+        $fileh5p = $core->fs->get_export_file($exportfilename);
+        $deployedfile = [];
+        $deployedfile['filename'] = $fileh5p->get_filename();
+        $deployedfile['filepath'] = $fileh5p->get_filepath();
+        $deployedfile['mimetype'] = $fileh5p->get_mimetype();
+        $deployedfile['filesize'] = $fileh5p->get_filesize();
+        $deployedfile['timemodified'] = $fileh5p->get_timemodified();
+
+        // Create the url depending the request was made through typeurl.
+        if ($typeurl == self::WSPLUGINFILE) {
+            $url  = \moodle_url::make_webservice_pluginfile_url(
+                $fileh5p->get_contextid(),
+                $fileh5p->get_component(),
+                $fileh5p->get_filearea(),
+                '',
+                '',
+                $fileh5p->get_filename()
+            );
+        } else {
+            $includetoken = false;
+            if ($typeurl == self::TOKENPLUGINFILE) {
+                $includetoken = true;
+            }
+            $url = \moodle_url::make_pluginfile_url(
+                $fileh5p->get_contextid(),
+                $fileh5p->get_component(),
+                $fileh5p->get_filearea(),
+                '',
+                '',
+                $fileh5p->get_filename(),
+                false,
+                $includetoken
+            );
+        }
+        $deployedfile['fileurl'] = $url->out(false);
+
+        return $deployedfile;
+    }
 }
index 734bd08..a58f61a 100644 (file)
@@ -328,4 +328,55 @@ class helper_testcase extends \advanced_testcase {
         $this->assertCount(7, $messages->error);
         $this->assertCount(2, $messages->info);
     }
+
+    /**
+     * Test the behaviour of get_export_info().
+     */
+    public function test_get_export_info(): void {
+         $this->resetAfterTest();
+
+        $filename = 'guess-the-answer.h5p';
+        $syscontext = \context_system::instance();
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
+        $deployedfile = $generator->create_export_file($filename,
+            $syscontext->id,
+            file_storage::COMPONENT,
+            file_storage::EXPORT_FILEAREA);
+
+        // Test scenario 1: Get export information from correct filename.
+        $helperfile = helper::get_export_info($deployedfile['filename']);
+        $this->assertEquals($deployedfile['filename'], $helperfile['filename']);
+        $this->assertEquals($deployedfile['filepath'], $helperfile['filepath']);
+        $this->assertEquals($deployedfile['filesize'], $helperfile['filesize']);
+        $this->assertEquals($deployedfile['timemodified'], $helperfile['timemodified']);
+        $this->assertEquals($deployedfile['fileurl'], $helperfile['fileurl']);
+
+        // Test scenario 2: Get export information from correct filename and url.
+        $url = \moodle_url::make_pluginfile_url(
+            $syscontext->id,
+            file_storage::COMPONENT,
+            'unittest',
+            0,
+            '/',
+            $deployedfile['filename'],
+            false,
+            true
+        );
+        $helperfile = helper::get_export_info($deployedfile['filename'], $url);
+        $this->assertEquals($url, $helperfile['fileurl']);
+
+        // Test scenario 3: Get export information from correct filename and factory.
+        $factory = new \core_h5p\factory();
+        $helperfile = helper::get_export_info($deployedfile['filename'], null, $factory);
+        $this->assertEquals($deployedfile['filename'], $helperfile['filename']);
+        $this->assertEquals($deployedfile['filepath'], $helperfile['filepath']);
+        $this->assertEquals($deployedfile['filesize'], $helperfile['filesize']);
+        $this->assertEquals($deployedfile['timemodified'], $helperfile['timemodified']);
+        $this->assertEquals($deployedfile['fileurl'], $helperfile['fileurl']);
+
+        // Test scenario 4: Get export information from wrong filename.
+        $helperfile = helper::get_export_info('nofileexist.h5p', $url);
+        $this->assertNull($helperfile);
+    }
 }
index 08ccfaa..19c40d7 100644 (file)
@@ -81,7 +81,6 @@ $string['autolang'] = 'Language autodetect';
 $string['autologinguests'] = 'Auto-login guests';
 $string['searchareas'] = 'Search areas';
 $string['availableto'] = 'Available to';
-$string['availablelicenses'] = 'Available licences';
 $string['backgroundcolour'] = 'Transparent colour';
 $string['backups'] = 'Backups';
 $string['backup_shortname'] = 'Use course name in backup filename';
@@ -763,7 +762,6 @@ $string['managecustomfields'] = 'Manage custom field types';
 $string['manageformats'] = 'Manage course formats';
 $string['manageformatsgotosettings'] = 'Default format can be changed in {$a}';
 $string['managelang'] = 'Manage';
-$string['managelicenses'] = 'Manage licences';
 $string['manageqbehaviours'] = 'Manage question behaviours';
 $string['manageqtypes'] = 'Manage question types';
 $string['maturity50'] = 'Alpha';
@@ -1046,6 +1044,8 @@ $string['registration_help'] = 'By registering:
 $string['registrationwarning'] = 'Your site is not yet registered.';
 $string['registrationwarningcontactadmin'] = 'Your site is not yet registered. Please notify your administrator.';
 $string['releasenoteslink'] = 'For information about this version of Moodle, please see the online <a target="_blank" href="{$a}">Release Notes</a>';
+$string['rememberuserlicensepref'] = 'Remember user licence preference';
+$string['rememberuserlicensepref_help'] = 'If enabled, the last licence selected by the user is preselected when uploading a file in the file picker. Otherwise, the default site licence is preselected.';
 $string['rememberusername'] = 'Remember username';
 $string['rememberusername_desc'] = 'Enable if you want to store permanent cookies with usernames during user login. Permanent cookies may be considered a privacy issue if used without consent.';
 $string['reportsmanage'] = 'Manage reports';
@@ -1461,3 +1461,7 @@ $string['registermoodleorgli2'] = 'Statistics about your site will be added to t
 $string['registerwithmoodleorg'] = 'Register your site';
 $string['configrequestcategoryselection'] = 'Allow the selection of a category when requesting a course.';
 $string['requestcategoryselection'] = 'Enable category selection';
+
+// Deprecated since Moodle 3.9.
+$string['availablelicenses'] = 'Available licences';
+$string['managelicenses'] = 'Manage licences';
index fa6690d..11bbe84 100644 (file)
@@ -270,6 +270,7 @@ $string['defaultissuerpassword_help'] = 'An account is required on the backpack
 $string['defaultissuername'] = 'Badge issuer name';
 $string['defaultissuername_desc'] = 'Name of the issuing agent or authority.';
 $string['delbadge'] = 'Would you like to delete badge \'{$a}\' and remove all existing issued badges?';
+$string['delexternalbackpack'] = 'Delete site backpack';
 $string['delexternalbackpackconfirm'] = 'Delete site backpack \'{$a}\'?';
 $string['delconfirm'] = 'Delete and remove existing issued badges';
 $string['deletehelp'] = '<p>Fully deleting a badge means that all its information and criteria records will be permanently removed. Users who have earned this badge will no longer be able to access it and display it on their profile pages.</p>
@@ -279,7 +280,6 @@ $string['delparamconfirm'] = 'Are you sure that you want to delete this paramete
 $string['description'] = 'Description';
 $string['disconnect'] = 'Disconnect';
 $string['donotaward'] = 'Currently, this badge is not active, so it cannot be awarded to users. If you would like to award this badge, please set its status to active.';
-$string['editsettings'] = 'Edit settings';
 $string['enablebadges'] = 'Enable badges';
 $string['endorsement'] = 'Endorsement';
 $string['error:backpackdatainvalid'] = 'The data return from the backpack was invalid.';
@@ -402,6 +402,7 @@ $string['month'] = 'Month(s)';
 $string['mybadges'] = 'My badges';
 $string['mybackpack'] = 'My backpack settings';
 $string['never'] = 'Never';
+$string['newbackpack'] = 'Add a new backpack';
 $string['newbadge'] = 'Add a new badge';
 $string['newimage'] = 'New image';
 $string['noalignment'] = 'This badge does not have any external skills or standards specified.';
@@ -513,6 +514,8 @@ $string['selecting'] = 'With selected badges...';
 $string['setup'] = 'Set up connection';
 $string['sitebackpack'] = 'Active external backpack';
 $string['sitebackpack_help'] = 'The external backpack that users can connect to from this site. Note that changing this setting after users have connected their backpacks will require each user to go to their backpack settings page and disconnect then reconnect.';
+$string['sitebackpackdeleted'] = 'The site backpack has been deleted.';
+$string['sitebackpacknotdeleted'] = 'This backpack couldn\'t be deleted because it\'s currently the site default.';
 $string['sitebackpackverify'] = 'Backpack connection';
 $string['sitebackpackwarning'] = 'Could not connect to backpack. <br/><br/>Check that the "Badge issuer email address" admin setting is the valid email for an account on the backpack website. <br/><br/>Check that the "Badge issuer password" on the <a href="{$a->url}">site backpack settings page</a>, is the correct password for the account on the backpack website. <br/><br/>The backpack returned: "{$a->warning}"';
 $string['sitebadges'] = 'Site badges';
@@ -572,3 +575,6 @@ $string['backpackbadges'] = 'You have {$a->totalbadges} badge(s) displayed from
 $string['error:nogroups'] = '<p>There are no public collections of badges available in your backpack. </p> <p>Only public collections are shown. <a href="https://backpack.openbadges.org">Visit your backpack</a> to create some public collections.</p>';
 $string['nobackpackbadges'] = 'There are no badges in the collections you have selected. <a href="mybackpack.php">Add more collections</a>.';
 $string['nobackpackcollections'] = 'No badge collections have been selected. <a href="mybackpack.php">Add collections</a>.';
+
+// Deprecated since Moodle 3.9.
+$string['editsettings'] = 'Edit settings';
index cb59517..6c22452 100644 (file)
@@ -60,6 +60,7 @@ $string['cachedef_groupdata'] = 'Course group information';
 $string['cachedef_h5p_content_type_translations'] = 'H5P content-type libraries translations';
 $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
 $string['cachedef_langmenu'] = 'List of available languages';
+$string['cachedef_license'] = 'List of licences';
 $string['cachedef_message_time_last_message_between_users'] = 'Time created for most recent message in a conversation';
 $string['cachedef_modelfirstanalyses'] = 'First analysis by model and analysable';
 $string['cachedef_locking'] = 'Locking';
@@ -78,6 +79,7 @@ $string['cachedef_recommendation_favourite_course_content_items'] = 'Recommendat
 $string['cachedef_repositories'] = 'Repositories instances data';
 $string['cachedef_roledefs'] = 'Role definitions';
 $string['cachedef_grade_categories'] = 'Grade category queries';
+$string['cachedef_grade_letters'] = 'Grade letters queries';
 $string['cachedef_string'] = 'Language string cache';
 $string['cachedef_tags'] = 'Tags collections and areas';
 $string['cachedef_temp_tables'] = 'Temporary tables cache';
index 280b3bd..1bb420f 100644 (file)
@@ -141,3 +141,6 @@ europe/belfast,core_timezones
 pacific/ponape,core_timezones
 pacific/truk,core_timezones
 pacific/yap,core_timezones
+editsettings,core_badges
+availablelicenses,core_admin
+managelicenses,core_admin
index 1c40f78..f33e687 100644 (file)
@@ -22,6 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+// Core licenses.
 $string['allrightsreserved'] = 'All rights reserved';
 $string['cc'] = 'Creative Commons';
 $string['cc-nc'] = 'Creative Commons - No Commercial';
@@ -30,4 +31,10 @@ $string['cc-nc-sa'] = 'Creative Commons - No Commercial ShareAlike';
 $string['cc-nd'] = 'Creative Commons - NoDerivs';
 $string['cc-sa'] = 'Creative Commons - ShareAlike';
 $string['public'] = 'Public domain';
-$string['unknown'] = 'Other';
+$string['unknown'] = 'Licence not specified';
+
+// Error messages.
+$string['cannotdeletecore'] = 'Cannot delete a standard licence';
+$string['cannotdeletelicenseinuse'] = 'Cannot delete a licence which is currently assigned to one or more files';
+$string['licensenotfoundshortname'] = 'Cannot find a licence with the short name \'{$a}\'';
+$string['missinglicensesortorder'] = 'Cannot set licence order, one or more installed licences is missing from new order';
index 7ed41e6..78a3234 100644 (file)
@@ -99,8 +99,6 @@ $string['displaytree'] = 'Display folder as file tree';
 $string['download'] = 'Download';
 $string['downloadallfiles'] = 'Download all files';
 $string['downloadfolder'] = 'Download all';
-$string['downloadselected'] = 'Download selected files';
-$string['deleteselected'] = 'Delete selected';
 $string['downloadsucc'] = 'The file has been downloaded successfully';
 $string['draftareanofiles'] = 'Cannot be downloaded because there is no files attached';
 $string['editrepositoryinstance'] = 'Edit repository instance';
index e010e09..5eee2cb 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['addcondition'] = 'Add condition';
+$string['adverbfor_and'] = 'and';
+$string['adverbfor_andnot'] = 'and';
+$string['adverbfor_or'] = 'or';
+$string['applyfilters'] = 'Apply filters';
+$string['clearfilterrow'] = 'Remove filter row';
+$string['clearfilters'] = 'Clear filters';
 $string['countparticipantsfound'] = '{$a} participants found';
+$string['filtersetmatchdescription'] = 'How multiple filters should be combined';
+$string['match'] = 'Match';
+$string['matchofthefollowing'] = 'of the following:';
+$string['placeholdertypeorselect'] = 'Type or select...';
+$string['placeholdertype'] = 'Type...';
 $string['privacy:courserequestpath'] = 'Requested courses';
 $string['privacy:descriptionpath'] = 'Profile description';
 $string['privacy:devicespath'] = 'User devices';
@@ -126,6 +138,8 @@ $string['privacy:passwordresetpath'] = 'Password resets';
 $string['privacy:profileimagespath'] = 'Profile images';
 $string['privacy:privatefilespath'] = 'Private files';
 $string['privacy:sessionpath'] = 'Session data';
+$string['filterbykeyword'] = 'Keyword';
+$string['selectfiltertype'] = 'Select';
 $string['target:upcomingactivitiesdue'] = 'Upcoming activities due';
 $string['target:upcomingactivitiesdue_help'] = 'This target generates reminders for upcoming activities due.';
 $string['target:upcomingactivitiesdueinfo'] = 'All upcoming activities due insights are listed here. These students have received these insights directly.';
index b75e4e1..1541db2 100644 (file)
@@ -7243,40 +7243,72 @@ class admin_setting_manageantiviruses extends admin_setting {
  * Special class for license administration.
  *
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 3.9 MDL-45184. Please use \tool_licensemanager\manager instead.
+ * @todo MDL-45184 This class will be deleted in Moodle 4.3.
  */
 class admin_setting_managelicenses extends admin_setting {
     /**
-     * Calls parent::__construct with specific arguments
+     * @deprecated since Moodle 3.9 MDL-45184. Please use \tool_licensemanager\manager instead.
+     * @todo MDL-45184 This class will be deleted in Moodle 4.3
      */
     public function __construct() {
-        $this->nosave = true;
-        parent::__construct('licensesui', get_string('licensesettings', 'admin'), '', '');
+        global $ADMIN;
+
+        debugging('admin_setting_managelicenses class is deprecated. Please use \tool_licensemanager\manager instead.',
+            DEBUG_DEVELOPER);
+
+        // Replace admin setting load with new external page load for tool_licensemanager, if not loaded already.
+        if (!is_null($ADMIN->locate('licensemanager'))) {
+            $temp = new admin_externalpage('licensemanager',
+                get_string('licensemanager', 'tool_licensemanager'),
+                \tool_licensemanager\helper::get_licensemanager_url());
+
+            $ADMIN->add('license', $temp);
+        }
     }
 
     /**
      * Always returns true, does nothing
      *
+     * @deprecated since Moodle 3.9 MDL-45184.
+     * @todo MDL-45184 This method will be deleted in Moodle 4.3
+     *
      * @return true
      */
     public function get_setting() {
+        debugging('admin_setting_managelicenses class is deprecated. Please use \tool_licensemanager\manager instead.',
+            DEBUG_DEVELOPER);
+
         return true;
     }
 
     /**
      * Always returns true, does nothing
      *
+     * @deprecated since Moodle 3.9 MDL-45184.
+     * @todo MDL-45184 This method will be deleted in Moodle 4.3
+     *
      * @return true
      */
     public function get_defaultsetting() {
+        debugging('admin_setting_managelicenses class is deprecated. Please use \tool_licensemanager\manager instead.',
+            DEBUG_DEVELOPER);
+
         return true;
     }
 
     /**
      * Always returns '', does not write anything
      *
+     * @deprecated since Moodle 3.9 MDL-45184.
+     * @todo MDL-45184 This method will be deleted in Moodle 4.3
+     *
      * @return string Always returns ''
      */
     public function write_setting($data) {
+        debugging('admin_setting_managelicenses class is deprecated. Please use \tool_licensemanager\manager instead.',
+            DEBUG_DEVELOPER);
+
         // do not write any setting
         return '';
     }
@@ -7284,53 +7316,18 @@ class admin_setting_managelicenses extends admin_setting {
     /**
      * Builds the XHTML to display the control
      *
+     * @deprecated since Moodle 3.9 MDL-45184. Please use \tool_licensemanager\manager instead.
+     * @todo MDL-45184 This method will be deleted in Moodle 4.3
+     *
      * @param string $data Unused
      * @param string $query
      * @return string
      */
     public function output_html($data, $query='') {
-        global $CFG, $OUTPUT;
-        require_once($CFG->libdir . '/licenselib.php');
-        $url = "licenses.php?sesskey=" . sesskey();
-
-        // display strings
-        $txt = get_strings(array('administration', 'settings', 'name', 'enable', 'disable', 'none'));
-        $licenses = license_manager::get_licenses();
-
-        $return = $OUTPUT->heading(get_string('availablelicenses', 'admin'), 3, 'main', true);
-
-        $return .= $OUTPUT->box_start('generalbox editorsui');
-
-        $table = new html_table();
-        $table->head  = array($txt->name, $txt->enable);
-        $table->colclasses = array('leftalign', 'centeralign');
-        $table->id = 'availablelicenses';
-        $table->attributes['class'] = 'admintable generaltable';
-        $table->data  = array();
-
-        foreach ($licenses as $value) {
-            $displayname = html_writer::link($value->source, get_string($value->shortname, 'license'), array('target'=>'_blank'));
-
-            if ($value->enabled == 1) {
-                $hideshow = html_writer::link($url.'&action=disable&license='.$value->shortname,
-                    $OUTPUT->pix_icon('t/hide', get_string('disable')));
-            } else {
-                $hideshow = html_writer::link($url.'&action=enable&license='.$value->shortname,
-                    $OUTPUT->pix_icon('t/show', get_string('enable')));
-            }
-
-            if ($value->shortname == $CFG->sitedefaultlicense) {
-                $displayname .= ' '.$OUTPUT->pix_icon('t/locked', get_string('default'));
-                $hideshow = '';
-            }
-
-            $enabled = true;
+        debugging('admin_setting_managelicenses class is deprecated. Please use \tool_licensemanager\manager instead.',
+            DEBUG_DEVELOPER);
 
-            $table->data[] =array($displayname, $hideshow);
-        }
-        $return .= html_writer::table($table);
-        $return .= $OUTPUT->box_end();
-        return highlight($query, $return);
+        redirect(\tool_licensemanager\helper::get_licensemanager_url());
     }
 }
 
index 154a606..8ef27f0 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 944df5c..77c999b 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js.map and b/lib/amd/build/form-autocomplete.min.js.map differ
index ae72462..42cdec9 100644 (file)
Binary files a/lib/amd/build/notification.min.js and b/lib/amd/build/notification.min.js differ
index ed26296..1d721de 100644 (file)
Binary files a/lib/amd/build/notification.min.js.map and b/lib/amd/build/notification.min.js.map differ
index 603a149..ba37d4e 100644 (file)
Binary files a/lib/amd/build/tag.min.js and b/lib/amd/build/tag.min.js differ
index 9749568..7db6808 100644 (file)
Binary files a/lib/amd/build/tag.min.js.map and b/lib/amd/build/tag.min.js.map differ
index 24665b1..bcc63b6 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 119e917..b75e37f 100644 (file)
Binary files a/lib/amd/build/templates.min.js.map and b/lib/amd/build/templates.min.js.map differ
index 035f94e..b91c5fb 100644 (file)
@@ -114,7 +114,7 @@ function($, log, str, templates, notification, LoadingIcon) {
         });
         var context = $.extend({items: items}, options, state);
         // Render the template.
-        return templates.render('core/form_autocomplete_selection_items', context)
+        return templates.render(options.templates.items, context)
         .then(function(html, js) {
             // Add it to the page.
             templates.replaceNodeContents(newSelection, html, js);
@@ -970,10 +970,11 @@ function($, log, str, templates, notification, LoadingIcon) {
          * @param {Boolean} showSuggestions - If suggestions should be shown
          * @param {String} noSelectionString - Text to display when there is no selection
          * @param {Boolean} closeSuggestionsOnSelect - Whether to close the suggestions immediately after making a selection.
+         * @param {Object} templateOverrides A set of templates to use instead of the standard templates
          * @return {Promise}
          */
         enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions, noSelectionString,
-                          closeSuggestionsOnSelect) {
+                          closeSuggestionsOnSelect, templateOverrides) {
             // Set some default values.
             var options = {
                 selector: selector,
@@ -982,7 +983,14 @@ function($, log, str, templates, notification, LoadingIcon) {
                 placeholder: placeholder,
                 caseSensitive: false,
                 showSuggestions: true,
-                noSelectionString: noSelectionString
+                noSelectionString: noSelectionString,
+                templates: $.extend({
+                        input: 'core/form_autocomplete_input',
+                        items: 'core/form_autocomplete_selection_items',
+                        layout: 'core/form_autocomplete_layout',
+                        selection: 'core/form_autocomplete_selection',
+                        suggestions: 'core/form_autocomplete_suggestions',
+                    }, templateOverrides),
             };
             var pendingKey = 'autocomplete-setup-' + selector;
             M.util.js_pending(pendingKey);
@@ -1058,27 +1066,35 @@ function($, log, str, templates, notification, LoadingIcon) {
             // Collect rendered inline JS to be executed once the HTML is shown.
             var collectedjs = '';
 
-            var renderInput = templates.render('core/form_autocomplete_input', context).then(function(html, js) {
+            var renderLayout = templates.render(options.templates.layout, {})
+            .then(function(html) {
+                return $(html);
+            });
+
+            var renderInput = templates.render(options.templates.input, context).then(function(html, js) {
                 collectedjs += js;
-                return html;
+                return $(html);
             });
 
-            var renderDatalist = templates.render('core/form_autocomplete_suggestions', context).then(function(html, js) {
+            var renderDatalist = templates.render(options.templates.suggestions