Merge branch 'MDL-67965-master' of git://github.com/andrewnicols/moodle
authorJun Pataleta <jun@moodle.com>
Fri, 14 Feb 2020 05:25:16 +0000 (13:25 +0800)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Fri, 14 Feb 2020 10:16:42 +0000 (11:16 +0100)
272 files changed:
Gruntfile.js
admin/settings/h5p.php
admin/tool/phpunit/settings.php
admin/tool/phpunit/upgrade.txt [new file with mode: 0644]
admin/tool/phpunit/webrunner.php [deleted file]
auth/shibboleth/auth.php
auth/shibboleth/index.php
auth/shibboleth/lang/en/auth_shibboleth.php
auth/shibboleth/settings.php
badges/classes/form/badge.php
badges/lib/bakerlib.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/db/upgrade.php
blocks/myoverview/lib.php
blocks/myoverview/settings.php
blocks/myoverview/templates/nav-grouping-selector.mustache
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/myoverview_test.php
blocks/myoverview/upgrade.txt
blocks/myoverview/version.php
blocks/site_main_menu/tests/behat/add_url.feature
calendar/view.php
competency/classes/api.php
competency/tests/hooks_test.php
config-dist.php
course/amd/build/activitychooser.min.js [new file with mode: 0644]
course/amd/build/activitychooser.min.js.map [new file with mode: 0644]
course/amd/build/local/activitychooser/dialogue.min.js [new file with mode: 0644]
course/amd/build/local/activitychooser/dialogue.min.js.map [new file with mode: 0644]
course/amd/build/local/activitychooser/repository.min.js [new file with mode: 0644]
course/amd/build/local/activitychooser/repository.min.js.map [new file with mode: 0644]
course/amd/build/local/activitychooser/selectors.min.js [new file with mode: 0644]
course/amd/build/local/activitychooser/selectors.min.js.map [new file with mode: 0644]
course/amd/src/activitychooser.js [new file with mode: 0644]
course/amd/src/local/activitychooser/dialogue.js [new file with mode: 0644]
course/amd/src/local/activitychooser/repository.js [new file with mode: 0644]
course/amd/src/local/activitychooser/selectors.js [new file with mode: 0644]
course/classes/category.php
course/classes/external/course_module_chooser_exporter.php [new file with mode: 0644]
course/classes/output/modchooser.php [deleted file]
course/classes/output/modchooser_item.php [deleted file]
course/externallib.php
course/renderer.php
course/templates/chooser.mustache [new file with mode: 0644]
course/templates/chooser_help.mustache [new file with mode: 0644]
course/templates/chooser_item.mustache [new file with mode: 0644]
course/templates/modchooser.mustache [deleted file]
course/tests/behat/activity_chooser.feature [new file with mode: 0644]
course/tests/behat/behat_course.php
course/tests/externallib_test.php
course/yui/build/moodle-course-modchooser/moodle-course-modchooser-debug.js [deleted file]
course/yui/build/moodle-course-modchooser/moodle-course-modchooser-min.js [deleted file]
course/yui/build/moodle-course-modchooser/moodle-course-modchooser.js [deleted file]
course/yui/src/modchooser/build.json [deleted file]
course/yui/src/modchooser/js/modchooser.js [deleted file]
course/yui/src/modchooser/meta/modchooser.json [deleted file]
h5p/classes/core.php
h5p/classes/file_storage.php
h5p/classes/helper.php
h5p/libraries.php
h5p/overview.php [new file with mode: 0644]
h5p/templates/h5plibraries.mustache
h5p/templates/h5ptoolsoverview.mustache [new file with mode: 0644]
h5p/tests/behat/h5p_overview.feature [new file with mode: 0644]
h5p/tests/h5p_core_test.php
h5p/tests/h5p_file_storage_test.php
install/lang/es_wp/langconfig.php
lang/en/admin.php
lang/en/course.php
lang/en/deprecated.txt
lang/en/h5p.php
lang/en/moodle.php
lang/en/webservice.php
lib/amd/build/modal.min.js
lib/amd/build/modal.min.js.map
lib/amd/src/modal.js
lib/badgeslib.php
lib/classes/date.php
lib/classes/lock/mysql_lock_factory.php [new file with mode: 0644]
lib/classes/session/manager.php
lib/db/services.php
lib/db/upgrade.php
lib/ddl/database_manager.php
lib/ddl/mssql_sql_generator.php
lib/ddl/mysql_sql_generator.php
lib/ddl/oracle_sql_generator.php
lib/ddl/postgres_sql_generator.php
lib/ddl/sql_generator.php
lib/ddl/tests/ddl_test.php
lib/dml/moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/sqlite3_pdo_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-debug.js
lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-min.js
lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button.js
lib/editor/atto/plugins/h5p/yui/src/button/js/button.js
lib/filestorage/file_storage.php
lib/filterlib.php
lib/form/duration.php
lib/form/templates/element-checkbox.mustache
lib/form/tests/duration_test.php
lib/moodlelib.php
lib/phpspreadsheet/readme_moodle.txt
lib/phpspreadsheet/vendor/autoload.php
lib/phpspreadsheet/vendor/composer/ClassLoader.php
lib/phpspreadsheet/vendor/composer/LICENSE
lib/phpspreadsheet/vendor/composer/autoload_real.php
lib/phpspreadsheet/vendor/composer/autoload_static.php
lib/phpspreadsheet/vendor/composer/installed.json
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/Builder.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/Functions.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/Matrix.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/Operators/DirectSum.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/functions/adjoint.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/functions/antidiagonal.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/functions/cofactors.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/functions/determinant.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/functions/diagonal.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/functions/identity.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/functions/inverse.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/functions/minors.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/functions/trace.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/functions/transpose.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/operations/add.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/operations/directsum.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/operations/divideby.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/operations/divideinto.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/operations/multiply.php
lib/phpspreadsheet/vendor/markbaker/matrix/classes/src/operations/subtract.php
lib/phpspreadsheet/vendor/markbaker/matrix/composer.7.2.json [new file with mode: 0644]
lib/phpspreadsheet/vendor/markbaker/matrix/composer.json
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/CHANGELOG.md
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/LICENSE
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/composer.json
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/composer.lock
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/phpunit.xml.dist
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Database.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTime.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Token/Stack.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/functionlist.txt
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Coordinate.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DefaultValueBinder.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Axis.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Chart.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeriesValues.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/GridLines.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/CellsFactory.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Document/Properties.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IOFactory.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReader.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/Properties.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Slk.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Properties.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Styles.php [new file with mode: 0644]
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReferenceHelper.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Settings.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/Matrix.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/StringHelper.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Color.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Fill.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Style.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Column.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnDimension.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Dimension.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowCellIterator.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/SheetView.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/BaseWriter.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Content.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Chart.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Comments.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php
lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
lib/phpunit/classes/util.php
lib/questionlib.php
lib/templates/local/toast/wrapper.mustache
lib/templates/modal.mustache
lib/tests/filterlib_test.php
lib/tests/fixtures/testable_core_h5p.php
lib/tests/h5p_get_content_types_task_test.php
lib/tests/lock_test.php
lib/tests/questionlib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js
lib/yui/src/formchangechecker/js/formchangechecker.js
mod/feedback/classes/responses_table.php
mod/forum/view.php
mod/quiz/attemptlib.php
mod/quiz/grade.php
mod/quiz/locallib.php
mod/quiz/renderer.php
mod/scorm/datamodels/aicc.js
mod/scorm/datamodels/scorm_12.js
mod/scorm/datamodels/scorm_13.js
mod/scorm/module.js
mod/scorm/request.js
mod/scorm/tests/behat/save_progress_on_unload.feature [new file with mode: 0644]
pix/b/h5p_library.svg [new file with mode: 0644]
question/engine/lib.php
question/tests/behat/edit_questions.feature
question/type/questiontypebase.php
theme/boost/amd/build/form-display-errors.min.js
theme/boost/amd/build/form-display-errors.min.js.map
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/build/scroll.min.js [new file with mode: 0644]
theme/boost/amd/build/scroll.min.js.map [new file with mode: 0644]
theme/boost/amd/src/form-display-errors.js
theme/boost/amd/src/loader.js
theme/boost/amd/src/scroll.js [new file with mode: 0644]
theme/boost/lang/en/theme_boost.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/footer.mustache
theme/classic/style/moodle.css
version.php
webservice/wsdoc.php

index f45c261..118c2f5 100644 (file)
@@ -167,7 +167,7 @@ module.exports = function(grunt) {
     // * fullRunDir             The full path to the runDir
     const gruntFilePath = fs.realpathSync(process.cwd());
     const cwd = getCwd(grunt);
-    const relativeCwd = cwd.replace(new RegExp(`${gruntFilePath}/?`), '');
+    const relativeCwd = path.relative(gruntFilePath, cwd);
     const componentDirectory = ComponentList.getOwningComponentDirectory(relativeCwd);
     const inComponent = !!componentDirectory;
     const runDir = inComponent ? componentDirectory : relativeCwd;
index 81ac282..881b34c 100644 (file)
@@ -25,5 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 // Settings page.
+$ADMIN->add('h5p', new admin_externalpage('h5poverview', get_string('h5poverview', 'core_h5p'),
+    new moodle_url('/h5p/overview.php'), ['moodle/site:config']));
 $ADMIN->add('h5p', new admin_externalpage('h5psettings', get_string('h5pmanage', 'core_h5p'),
     new moodle_url('/h5p/libraries.php'), ['moodle/site:config', 'moodle/h5p:updatelibraries']));
index efefd32..e42049b 100644 (file)
@@ -27,6 +27,4 @@ defined('MOODLE_INTERNAL') || die;
 
 if ($hassiteconfig) {
     $ADMIN->add('development', new admin_externalpage('toolphpunit', get_string('pluginname', 'tool_phpunit'), "$CFG->wwwroot/$CFG->admin/tool/phpunit/index.php"));
-    $ADMIN->add('development', new admin_externalpage('toolphpunitwebrunner', get_string('pluginname', 'tool_phpunit'), "$CFG->wwwroot/$CFG->admin/tool/phpunit/webrunner.php",
-        'moodle/site:config', true));
 }
diff --git a/admin/tool/phpunit/upgrade.txt b/admin/tool/phpunit/upgrade.txt
new file mode 100644 (file)
index 0000000..4580c98
--- /dev/null
@@ -0,0 +1,4 @@
+This files describes API changes in the tool_phpunit code.
+
+=== 3.9 ===
+* webrunner was removed
diff --git a/admin/tool/phpunit/webrunner.php b/admin/tool/phpunit/webrunner.php
deleted file mode 100644 (file)
index 47a2b42..0000000
+++ /dev/null
@@ -1,192 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * PHPUnit shell execution wrapper
- *
- * @package    tool_phpunit
- * @copyright  2012 Petr Skoda {@link http://skodak.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define('NO_OUTPUT_BUFFERING', true);
-
-require(__DIR__ . '/../../../config.php');
-require_once($CFG->libdir.'/adminlib.php');
-
-$testpath  = optional_param('testpath', '', PARAM_PATH);
-$testclass = optional_param('testclass', '', PARAM_ALPHANUMEXT);
-$execute   = optional_param('execute', 0, PARAM_BOOL);
-
-navigation_node::override_active_url(new moodle_url('/admin/tool/phpunit/index.php'));
-admin_externalpage_setup('toolphpunitwebrunner');
-
-if (!$CFG->debugdeveloper) {
-    print_error('notlocalisederrormessage', 'error', '', null, 'Not available on production sites, sorry.');
-}
-
-core_php_time_limit::raise(60*30);
-
-$oldcwd = getcwd();
-$code = 0;
-
-if (!isset($CFG->phpunit_dataroot) or !isset($CFG->phpunit_prefix)) {
-    tool_phpunit_problem('Missing $CFG->phpunit_dataroot or $CFG->phpunit_prefix, can not execute tests.');
-}
-if (!file_exists($CFG->phpunit_dataroot)) {
-    mkdir($CFG->phpunit_dataroot, 02777, true);
-}
-if (!is_writable($CFG->phpunit_dataroot)) {
-    tool_phpunit_problem('$CFG->phpunit_dataroot in not writable, can not execute tests.');
-}
-$output = null;
-exec('php --version', $output, $code);
-if ($code != 0) {
-    tool_phpunit_problem('Can not execute \'php\' binary.');
-}
-
-if ($execute) {
-    require_sesskey();
-
-    chdir($CFG->dirroot);
-    $output = null;
-    exec("php $CFG->admin/tool/phpunit/cli/util.php --diag", $output, $code);
-    if ($code == 0) {
-        // everything is ready
-
-    } else if ($code == PHPUNIT_EXITCODE_INSTALL) {
-        tool_phpunit_header();
-        echo $OUTPUT->box_start('generalbox');
-        echo '<pre>';
-        echo "Initialising test database:\n\n";
-        chdir($CFG->dirroot);
-        ignore_user_abort(true);
-        passthru("php $CFG->admin/tool/phpunit/cli/util.php --buildconfig", $code);
-        passthru("php $CFG->admin/tool/phpunit/cli/util.php --install", $code);
-        chdir($oldcwd);
-        echo '</pre>';
-        echo $OUTPUT->box_end();
-        if ($code != 0) {
-            tool_phpunit_problem('Can not initialize database');
-        }
-        set_debugging(DEBUG_NONE, false); // Hack: no redirect warning, we really want to redirect.
-        redirect(new moodle_url($PAGE->url, array('execute'=>1, 'tespath'=>$testpath, 'testclass'=>$testclass, 'sesskey'=>sesskey())), 'Reloading page');
-        echo $OUTPUT->footer();
-        die();
-
-    } else if ($code == PHPUNIT_EXITCODE_REINSTALL) {
-        tool_phpunit_header();
-        echo $OUTPUT->box_start('generalbox');
-        echo '<pre>';
-        echo "Reinitialising test database:\n\n";
-        chdir($CFG->dirroot);
-        ignore_user_abort(true);
-        passthru("php $CFG->admin/tool/phpunit/cli/util.php --drop", $code);
-        passthru("php $CFG->admin/tool/phpunit/cli/util.php --buildconfig", $code);
-        passthru("php $CFG->admin/tool/phpunit/cli/util.php --install", $code);
-        chdir($oldcwd);
-        echo '</pre>';
-        echo $OUTPUT->box_end();
-        if ($code != 0) {
-            tool_phpunit_problem('Can not initialize database');
-        }
-        set_debugging(DEBUG_NONE, false); // Hack: no redirect warning, we really want to redirect.
-        redirect(new moodle_url($PAGE->url, array('execute'=>1, 'tespath'=>$testpath, 'testclass'=>$testclass, 'sesskey'=>sesskey())), 'Reloading page');
-        die();
-
-    } else {
-        tool_phpunit_header();
-        echo $OUTPUT->box_start('generalbox');
-        echo '<pre>';
-        echo "Error: $code\n\n";
-        echo implode("\n", $output);
-        echo '</pre>';
-        echo $OUTPUT->box_end();
-        tool_phpunit_problem('Can not execute tests');
-        die();
-    }
-
-    tool_phpunit_header();
-    echo $OUTPUT->box_start('generalbox');
-    echo '<pre>';
-
-    // use the dataroot file
-    $configdir = "$CFG->phpunit_dataroot/phpunit/webrunner.xml";
-    if (!file_exists($configdir)) {
-        passthru("php $CFG->admin/tool/phpunit/cli/util.php --buildconfig", $code);
-        if ($code != 0) {
-            tool_phpunit_problem('Can not create configuration file');
-        }
-    }
-    $configdir = escapeshellarg($configdir);
-    // no cleanup of path - this is tricky because we can not use escapeshellarg and friends for escaping,
-    // this is from admin user so PARAM_PATH must be enough
-    chdir($CFG->dirroot);
-    passthru("php $CFG->admin/tool/phpunit/cli/util.php --run -c $configdir $testclass $testpath", $code);
-    chdir($oldcwd);
-
-    echo '</pre>';
-    echo $OUTPUT->box_end();
-
-} else {
-    tool_phpunit_header();
-}
-
-echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
-echo '<form method="get" action="webrunner.php">';
-echo '<fieldset class="invisiblefieldset">';
-echo '<label for="testpath">Test one file</label> ';
-echo '<input type="text" id="testpath" name="testpath" value="'.s($testpath).'" size="50" /> (all test cases from webrunner.xml if empty)';
-echo '</p>';
-echo '<label for="testclass">Class name</label> ';
-echo '<input type="text" id="testclass" name="testclass" value="'.s($testclass).'" size="50" /> (first class in file if empty)';
-echo '</p>';
-echo '<input type="submit" value="Run" />';
-echo '<input type="hidden" name="execute" value="1" />';
-echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
-echo '</fieldset>';
-echo '</form>';
-echo $OUTPUT->box_end();
-echo $OUTPUT->footer();
-die;
-
-
-
-//========================================
-
-/**
- * Print headers and experimental warning
- * @return void
- */
-function tool_phpunit_header() {
-    global $OUTPUT;
-    echo $OUTPUT->header();
-    echo $OUTPUT->heading(get_string('pluginname', 'tool_phpunit'));
-    echo $OUTPUT->box('EXPERIMENTAL: it is recommended to execute PHPUnit tests and init scripts only from command line.', array('generalbox'));
-}
-
-/**
- * Called when PHPUnit can not execute.
- * @param string $message
- * @return void
- */
-function tool_phpunit_problem($message) {
-    global $PAGE;
-    if (!$PAGE->headerprinted) {
-        tool_phpunit_header();
-    }
-    notice($message, new moodle_url('/admin/tool/phpunit/'));
-}
index ad0c55f..8bedda9 100644 (file)
@@ -263,7 +263,8 @@ class auth_plugin_shibboleth extends auth_plugin_base {
         global $OUTPUT;
 
         if (!isset($this->config->user_attribute) || empty($this->config->user_attribute)) {
-            echo $OUTPUT->notification(get_string("shib_not_set_up_error", "auth_shibboleth"), 'notifyproblem');
+            echo $OUTPUT->notification(get_string("shib_not_set_up_error", "auth_shibboleth",
+                (new moodle_url('/auth/shibboleth/README.txt'))->out()), 'notifyproblem');
             return;
         }
         if ($this->config->convert_data and $this->config->convert_data != '' and !is_readable($this->config->convert_data)) {
index b177f7c..0e0752c 100644 (file)
@@ -32,8 +32,9 @@
     $shibbolethauth = get_auth_plugin('shibboleth');
 
     // Check whether Shibboleth is configured properly
+    $readmeurl = (new moodle_url('/auth/shibboleth/README.txt'))->out();
     if (empty($pluginconfig->user_attribute)) {
-        print_error('shib_not_set_up_error', 'auth_shibboleth');
+        print_error('shib_not_set_up_error', 'auth_shibboleth', '', $readmeurl);
      }
 
 /// If we can find the Shibboleth attribute, save it in session and return to main login page
@@ -91,7 +92,7 @@
     elseif (!empty($_SERVER['HTTP_SHIB_APPLICATION_ID']) || !empty($_SERVER['Shib-Application-ID'])) {
         print_error('shib_no_attributes_error', 'auth_shibboleth' , '', '\''.$pluginconfig->user_attribute.'\', \''.$pluginconfig->field_map_firstname.'\', \''.$pluginconfig->field_map_lastname.'\' and \''.$pluginconfig->field_map_email.'\'');
     } else {
-        print_error('shib_not_set_up_error', 'auth_shibboleth');
+        print_error('shib_not_set_up_error', 'auth_shibboleth', '', $readmeurl);
     }
 
 
index bff0267..ca9b65b 100644 (file)
@@ -28,7 +28,7 @@ $string['auth_shib_auth_method_description'] = 'Provide a name for the Shibbolet
 $string['auth_shib_auth_logo'] = 'Authentication method logo';
 $string['auth_shib_auth_logo_description'] = 'Provide a logo for the Shibboleth authentication method that is familiar to your users. This could be the logo of your Shibboleth federation, e.g. <tt>SWITCHaai Login</tt> or <tt>InCommon Login</tt> or similar.';
 $string['auth_shib_contact_administrator'] = 'In case you are not associated with the given organizations and you need access to a course on this server, please contact the <a href="mailto:{$a}">Moodle Administrator</a>.';
-$string['auth_shibbolethdescription'] = 'Using this method users are created and authenticated using Shibboleth. For set-up details, see the <a href="../auth/shibboleth/README.txt">Shibboleth README</a>.';
+$string['auth_shibbolethdescription'] = 'Using this method users are created and authenticated using Shibboleth. For set-up details, see the <a href="{$a}">Shibboleth README</a>.';
 $string['auth_shibboleth_errormsg'] = 'Please select the organization you are member of!';
 $string['auth_shibboleth_login'] = 'Shibboleth login';
 $string['auth_shibboleth_login_long'] = 'Login to Moodle via Shibboleth';
@@ -36,7 +36,7 @@ $string['auth_shibboleth_manual_login'] = 'Manual login';
 $string['auth_shibboleth_select_member'] = 'I\'m a member of ...';
 $string['auth_shibboleth_select_organization'] = 'For authentication via Shibboleth, please select your organisation from the drop-down menu:';
 $string['auth_shib_convert_data'] = 'Data modification API';
-$string['auth_shib_convert_data_description'] = 'You can use this API to further modify the data provided by Shibboleth. Read the <a href="../auth/shibboleth/README.txt">README</a> for further instructions.';
+$string['auth_shib_convert_data_description'] = 'You can use this API to further modify the data provided by Shibboleth. Read the <a href="{$a}">README</a> for further instructions.';
 $string['auth_shib_convert_data_warning'] = 'The file does not exist or is not readable by the webserver process!';
 $string['auth_shib_changepasswordurl'] = 'Password-change URL';
 $string['auth_shib_idp_list'] = 'Identity providers';
@@ -57,6 +57,6 @@ $string['auth_shib_username_description'] = 'Name of the webserver Shibboleth en
 $string['shib_invalid_account_error'] = 'You seem to be Shibboleth authenticated but Moodle has no valid account for your username. Your account may not exist or it may have been suspended.';
 $string['shib_no_attributes_error'] = 'You seem to be Shibboleth authenticated but Moodle didn\'t receive any user attributes. Please check that your Identity Provider releases the necessary attributes ({$a}) to the Service Provider Moodle is running on or inform the webmaster of this server.';
 $string['shib_not_all_attributes_error'] = 'Moodle needs certain Shibboleth attributes which are not present in your case. The attributes are: {$a}<br />Please contact the webmaster of this server or your Identity Provider.';
-$string['shib_not_set_up_error'] = 'Shibboleth authentication doesn\'t seem to be set up correctly because no Shibboleth environment variables are present for this page. Please consult the <a href="README.txt">README</a> for further instructions on how to set up Shibboleth authentication or contact the webmaster of this Moodle installation.';
+$string['shib_not_set_up_error'] = 'Shibboleth authentication doesn\'t seem to be set up correctly because no Shibboleth environment variables are present for this page. Please consult the <a href="{$a}">README</a> for further instructions on how to set up Shibboleth authentication or contact the webmaster of this Moodle installation.';
 $string['pluginname'] = 'Shibboleth';
 $string['privacy:metadata'] = 'The Shibboleth authentication plugin does not store any personal data.';
index e4b4c3a..86dce35 100644 (file)
@@ -30,8 +30,9 @@ if ($ADMIN->fulltree) {
     require_once($CFG->dirroot.'/auth/shibboleth/classes/admin_setting_special_idp_configtextarea.php');
 
     // Introductory explanation.
+    $readmeurl = (new moodle_url('/auth/shibboleth/README.txt'))->out();
     $settings->add(new admin_setting_heading('auth_shibboleth/pluginname', '',
-            new lang_string('auth_shibbolethdescription', 'auth_shibboleth')));
+            new lang_string('auth_shibbolethdescription', 'auth_shibboleth', $readmeurl)));
 
     // Username.
     $settings->add(new admin_setting_configtext('auth_shibboleth/user_attribute', get_string('username'),
@@ -40,7 +41,7 @@ if ($ADMIN->fulltree) {
     // COnvert Data configuration file.
     $settings->add(new admin_setting_configfile('auth_shibboleth/convert_data',
             get_string('auth_shib_convert_data', 'auth_shibboleth'),
-            get_string('auth_shib_convert_data_description', 'auth_shibboleth'), ''));
+            get_string('auth_shib_convert_data_description', 'auth_shibboleth', $readmeurl), ''));
 
     // WAYF.
     $settings->add(new auth_shibboleth_admin_setting_special_wayf_select());
index f6d5e09..faa64a6 100644 (file)
@@ -49,7 +49,6 @@ class badge extends moodleform {
         $mform = $this->_form;
         $badge = (isset($this->_customdata['badge'])) ? $this->_customdata['badge'] : false;
         $action = $this->_customdata['action'];
-        $languages = get_string_manager()->get_list_of_languages();
 
         $mform->addElement('header', 'badgedetails', get_string('badgedetails', 'badges'));
         $mform->addElement('text', 'name', get_string('name'), array('size' => '70'));
@@ -61,6 +60,8 @@ class badge extends moodleform {
         $mform->addElement('text', 'version', get_string('version', 'badges'), array('size' => '70'));
         $mform->setType('version', PARAM_TEXT);
         $mform->addHelpButton('version', 'version', 'badges');
+
+        $languages = get_string_manager()->get_list_of_languages();
         $mform->addElement('select', 'language', get_string('language'), $languages);
         $mform->addHelpButton('language', 'language', 'badges');
 
@@ -157,7 +158,16 @@ class badge extends moodleform {
         $mform->setType('action', PARAM_TEXT);
 
         if ($action == 'new') {
-            $mform->setDefault('language', $CFG->lang);
+            // Try to set default badge language to that of current language, or it's parent.
+            $language = current_language();
+            if (isset($languages[$language])) {
+                $defaultlanguage = $language;
+            } else {
+                // Calling get_parent_language returns an empty string instead of 'en'.
+                $defaultlanguage = get_parent_language($language) ?: 'en';
+            }
+
+            $mform->setDefault('language', $defaultlanguage);
             $this->add_action_buttons(true, get_string('createbutton', 'badges'));
         } else {
             // Add hidden fields.
index 9507bfa..23fbcc8 100644 (file)
@@ -114,23 +114,59 @@ class PNG_MetaDataHandler
      * @param string $value Currently an assertion URL that is added to an image metadata.
      *
      * @return string $result File content with a new chunk as a string. Can be used in file_put_contents() to write to a file.
+     * @throws \moodle_exception when unsupported chunk type is defined.
      */
     public function add_chunks($type, $key, $value) {
         if (strlen($key) > 79) {
             debugging('Key is too big');
         }
 
-        // tEXt Textual data.
-        // Keyword:        1-79 bytes (character string)
-        // Null separator: 1 byte
-        // Text:           n bytes (character string)
-        $data = $key . "\0" . $value;
+        $dataparts = [];
+        if ($type === 'iTXt') {
+            // International textual data (iTXt).
+            // Keyword:             1-79 bytes (character string).
+            $dataparts[] = $key;
+            // Null separator:      1 byte.
+            $dataparts[] = "\x00";
+            // Compression flag:    1 byte
+            // A value of 0 means no compression.
+            $dataparts[] = "\x00";
+            // Compression method:  1 byte
+            // If compression is disabled, the method should also be 0.
+            $dataparts[] = "\x00";
+            // Language tag:        0 or more bytes (character string)
+            // When there is no language specified leave empty.
+
+            // Null separator:      1 byte.
+            $dataparts[] = "\x00";
+            // Translated keyword:  0 or more bytes
+            // When there is no translation specified, leave empty.
+
+            // Null separator:      1 byte.
+            $dataparts[] = "\x00";
+            // Text:                0 or more bytes.
+            $dataparts[] = $value;
+        } else if ($type === 'tEXt') {
+            // Textual data (tEXt).
+            // Keyword:             1-79 bytes (character string).
+            $dataparts[] = $key;
+            // Null separator:      1 byte.
+            $dataparts[] = "\0";
+            // Text:                n bytes (character string).
+            $dataparts[] = $value;
+        } else {
+            throw new \moodle_exception('Unsupported chunk type: ' . $type);
+        }
+
+        $data = implode($dataparts);
+
         $crc = pack("N", crc32($type . $data));
         $len = pack("N", strlen($data));
 
         // Chunk format: length + type + data + CRC.
         // CRC is a CRC-32 computed over the chunk type and chunk data.
         $newchunk = $len . $type . $data . $crc;
+        $this->_chunks[$type] = $data;
 
         $result = substr($this->_contents, 0, $this->_size - 12)
                 . $newchunk
index d9687de..96e18fb 100644 (file)
@@ -121,7 +121,7 @@ class main implements renderable, templatable {
      *
      * @var boolean
      */
-    private $displaygroupingstarred;
+    private $displaygroupingfavourites;
 
     /**
      * Store a course grouping option setting.
@@ -214,7 +214,7 @@ class main implements renderable, templatable {
         $this->displaygroupinginprogress = $config->displaygroupinginprogress;
         $this->displaygroupingfuture = $config->displaygroupingfuture;
         $this->displaygroupingpast = $config->displaygroupingpast;
-        $this->displaygroupingstarred = $config->displaygroupingstarred;
+        $this->displaygroupingfavourites = $config->displaygroupingfavourites;
         $this->displaygroupinghidden = $config->displaygroupinghidden;
         $this->displaygroupingcustomfield = ($config->displaygroupingcustomfield && $config->customfiltergrouping);
         $this->customfiltergrouping = $config->customfiltergrouping;
@@ -226,7 +226,7 @@ class main implements renderable, templatable {
                 $this->displaygroupinginprogress,
                 $this->displaygroupingfuture,
                 $this->displaygroupingpast,
-                $this->displaygroupingstarred,
+                $this->displaygroupingfavourites,
                 $this->displaygroupinghidden);
         $displaygroupingselectorscount = count(array_filter($displaygroupingselectors));
         if ($displaygroupingselectorscount > 1 || $this->displaygroupingcustomfield) {
@@ -259,7 +259,7 @@ class main implements renderable, templatable {
         if ($config->displaygroupingpast == true) {
             return BLOCK_MYOVERVIEW_GROUPING_PAST;
         }
-        if ($config->displaygroupingstarred == true) {
+        if ($config->displaygroupingfavourites == true) {
             return BLOCK_MYOVERVIEW_GROUPING_FAVOURITES;
         }
         if ($config->displaygroupinghidden == true) {
@@ -439,7 +439,7 @@ class main implements renderable, templatable {
             'displaygroupinginprogress' => $this->displaygroupinginprogress,
             'displaygroupingfuture' => $this->displaygroupingfuture,
             'displaygroupingpast' => $this->displaygroupingpast,
-            'displaygroupingstarred' => $this->displaygroupingstarred,
+            'displaygroupingfavourites' => $this->displaygroupingfavourites,
             'displaygroupinghidden' => $this->displaygroupinghidden,
             'displaygroupingselector' => $this->displaygroupingselector,
             'displaygroupingcustomfield' => $this->displaygroupingcustomfield && $customfieldvalues,
index fc4c890..d9bccdd 100644 (file)
@@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die();
  * @param int $oldversion
  */
 function xmldb_block_myoverview_upgrade($oldversion) {
-    global $DB;
+    global $DB, $CFG, $OUTPUT;
 
     if ($oldversion < 2019091800) {
         // Remove orphaned course favourites, which weren't being deleted when the course was deleted.
@@ -58,5 +58,25 @@ function xmldb_block_myoverview_upgrade($oldversion) {
     // Automatically generated Moodle v3.8.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2019111801) {
+        // Renaming the setting from displaygroupingstarred to displaygroupingfavourites to match Moodle convention.
+
+        // Check to see if record exists. get_config doesn't allow differentiation between not exists and false.
+        $dbval = $DB->get_field('config_plugins', 'value', ['plugin' => 'block_myoverview', 'name' => 'displaygroupingstarred']);
+        if ($dbval !== false) {
+            set_config('displaygroupingfavourites', $dbval, 'block_myoverview');
+            unset_config('displaygroupingstarred', 'block_myoverview');
+        }
+
+        if (isset($CFG->forced_plugin_settings['block_myoverview']['displaygroupingstarred'])) {
+            // Check to see if the starred setting is defined in the config file. Display a warning if so.
+            $warn = 'Setting block_myoverview->displaygroupingstarred has been renamed '.
+                    'to block_myoverview->displaygroupingfavourites. Old setting present in config.php.';
+            echo $OUTPUT->notification($warn, 'notifyproblem');
+        }
+
+        upgrade_block_savepoint(true, 2019111801, 'myoverview', false);
+    }
+
     return true;
 }
index 21f9b70..7af8cbd 100644 (file)
@@ -148,7 +148,7 @@ function block_myoverview_user_preferences() {
  * @param stdClass $course The deleted course
  */
 function block_myoverview_pre_course_delete(\stdClass $course) {
-    // Removing any starred courses which have been created for users, for this course.
+    // Removing any favourited courses which have been created for users, for this course.
     $service = \core_favourites\service_factory::get_service_for_component('core_course');
     $service->delete_favourites_by_type_and_item('courses', $course->id);
 }
index 0ec6ebe..c6bcdf4 100644 (file)
@@ -110,7 +110,7 @@ if ($ADMIN->fulltree) {
     $settings->hide_if('block_myoverview/customfiltergrouping', 'block_myoverview/displaygroupingcustomfield');
 
     $settings->add(new admin_setting_configcheckbox(
-            'block_myoverview/displaygroupingstarred',
+            'block_myoverview/displaygroupingfavourites',
             get_string('favourites', 'block_myoverview'),
             '',
             1));
index 5d6b598..ed38dc9 100644 (file)
@@ -33,7 +33,7 @@
         "displaygroupinginprogress": true,
         "displaygroupingfuture": true,
         "displaygroupingpast": true,
-        "displaygroupingstarred": true,
+        "displaygroupingfavourites": true,
         "displaygroupinghidden": true,
         "displaygroupingselector": true
     }
                 </li>
             {{/customfieldvalues}}
         {{/displaygroupingcustomfield}}
-        {{#displaygroupingstarred}}
+        {{#displaygroupingfavourites}}
         <li class="dropdown-divider" role="presentation">
             <span class="filler">&nbsp;</span>
         </li>
             <a class="dropdown-item {{#favourites}}active{{/favourites}}" href="#" data-filter="grouping" data-value="favourites"  data-pref="favourites" aria-label="{{#str}} aria:favourites, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
                 {{#str}} favourites, block_myoverview {{/str}}
             </a>
-        {{/displaygroupingstarred}}
+        {{/displaygroupingfavourites}}
         {{#displaygroupinghidden}}
         <li class="dropdown-divider" role="presentation">
             <span class="filler">&nbsp;</span>
index d7ee593..72d2f2c 100644 (file)
@@ -128,6 +128,20 @@ Feature: The my overview block allows users to easily access their courses
     And I should not see "Course 3" in the "Course overview" "block"
     And I should not see "Course 4" in the "Course overview" "block"
 
+  Scenario: View favourite courses - w/ persistence
+    Given I log in as "student1"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "Starred" "link" in the "Course overview" "block"
+    And I reload the page
+    Then I should see "Starred" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+
   Scenario: List display  persistence
     Given I log in as "student1"
     And I click on "Display drop-down menu" "button" in the "Course overview" "block"
index bc80abe..eba4b29 100644 (file)
@@ -109,7 +109,7 @@ class block_myoverview_testcase extends advanced_testcase {
         $this->assertEquals(1, $configs->plugin->displaygroupinghidden);
         $this->assertEquals(1, $configs->plugin->displaygroupinginprogress);
         $this->assertEquals(1, $configs->plugin->displaygroupingpast);
-        $this->assertEquals(1, $configs->plugin->displaygroupingstarred);
+        $this->assertEquals(1, $configs->plugin->displaygroupingfavourites);
         $this->assertEquals('card,list,summary', $configs->plugin->layouts);
         $this->assertEquals(get_config('block_myoverview', 'version'), $configs->plugin->version);
         // Test custom fields.
index e2d27b8..76588f9 100644 (file)
@@ -2,4 +2,8 @@ This file describes API changes in the myoverview block code.
 
 === 3.7 ===
 
-* The 'block/myoverview:addinstance' capability has been removed. It has never been used in code.
\ No newline at end of file
+* The 'block/myoverview:addinstance' capability has been removed. It has never been used in code.
+
+=== 3.9 ===
+
+* Rename setting block_myoverview->displaygroupingstarred to block_myoverview->displaygroupingfavourites.
index 8b31314..e75d2cf 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019111800;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019111801;         // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019111200;         // Requires this Moodle version.
 $plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
index dbd6386..1b2a7df 100644 (file)
@@ -16,4 +16,4 @@ Feature: Add URL to main menu block
       | External URL | http://www.google.com |
       | id_display | In pop-up |
     Then "google" "link" should exist in the "Main menu" "block"
-    And "Add an activity or resource" "link" should exist in the "Main menu" "block"
+    And "Add an activity or resource" "button" should exist in the "Main menu" "block"
index 33deb9a..0b9adfc 100644 (file)
@@ -52,11 +52,23 @@ require_once($CFG->dirroot.'/calendar/lib.php');
 $categoryid = optional_param('category', null, PARAM_INT);
 $courseid = optional_param('course', SITEID, PARAM_INT);
 $view = optional_param('view', 'upcoming', PARAM_ALPHA);
+$day = optional_param('cal_d', 0, PARAM_INT);
+$mon = optional_param('cal_m', 0, PARAM_INT);
+$year = optional_param('cal_y', 0, PARAM_INT);
 $time = optional_param('time', 0, PARAM_INT);
 $lookahead = optional_param('lookahead', null, PARAM_INT);
 
 $url = new moodle_url('/calendar/view.php');
 
+// If a day, month and year were passed then convert it to a timestamp. If these were passed
+// then we can assume the day, month and year are passed as Gregorian, as no where in core
+// should we be passing these values rather than the time. This is done for BC.
+if (!empty($day) && !empty($mon) && !empty($year)) {
+    if (checkdate($mon, $day, $year)) {
+        $time = make_timestamp($year, $mon, $day);
+    }
+}
+
 if (empty($time)) {
     $time = time();
 }
index 600f8f8..ba22662 100644 (file)
@@ -4802,6 +4802,40 @@ class api {
         $DB->delete_records(template_cohort::TABLE, array('cohortid' => $cohort->id));
     }
 
+    /**
+     * Action to perform when a user is deleted.
+     *
+     * @param int $userid The user id.
+     */
+    public static function hook_user_deleted($userid) {
+        global $DB;
+
+        $usercompetencies = $DB->get_records(user_competency::TABLE, ['userid' => $userid], '', 'id');
+        foreach ($usercompetencies as $usercomp) {
+            $DB->delete_records(evidence::TABLE, ['usercompetencyid' => $usercomp->id]);
+        }
+
+        $DB->delete_records(user_competency::TABLE, ['userid' => $userid]);
+        $DB->delete_records(user_competency_course::TABLE, ['userid' => $userid]);
+        $DB->delete_records(user_competency_plan::TABLE, ['userid' => $userid]);
+
+        // Delete any associated files.
+        $fs = get_file_storage();
+        $context = context_user::instance($userid);
+        $userevidences = $DB->get_records(user_evidence::TABLE, ['userid' => $userid], '', 'id');
+        foreach ($userevidences as $userevidence) {
+            $DB->delete_records(user_evidence_competency::TABLE, ['userevidenceid' => $userevidence->id]);
+            $DB->delete_records(user_evidence::TABLE, ['id' => $userevidence->id]);
+            $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id);
+        }
+
+        $userplans = $DB->get_records(plan::TABLE, ['userid' => $userid], '', 'id');
+        foreach ($userplans as $userplan) {
+            $DB->delete_records(plan_competency::TABLE, ['planid' => $userplan->id]);
+            $DB->delete_records(plan::TABLE, ['id' => $userplan->id]);
+        }
+    }
+
     /**
      * Manually grade a user competency.
      *
index e8be7c4..c1890ea 100644 (file)
@@ -207,4 +207,47 @@ class core_competency_hooks_testcase extends advanced_testcase {
         $this->assertEquals(1, \core_competency\template_cohort::count_records(array('templateid' => $t1->get('id'))));
         $this->assertEquals(0, \core_competency\template_cohort::count_records(array('templateid' => $t2->get('id'))));
     }
+
+    public function test_hook_user_deleted() {
+        $this->resetAfterTest();
+        $dg = $this->getDataGenerator();
+        $ccg = $dg->get_plugin_generator('core_competency');
+
+        $u1 = $dg->create_user();
+
+        $framework = $ccg->create_framework();
+        $comp1 = $ccg->create_competency(['competencyframeworkid' => $framework->get('id')]);
+        $comp2 = $ccg->create_competency(['competencyframeworkid' => $framework->get('id')]);
+
+        $c1 = $dg->create_course();
+        $cc1a = $ccg->create_course_competency(['competencyid' => $comp1->get('id'), 'courseid' => $c1->id]);
+        $cc1b = $ccg->create_course_competency(['competencyid' => $comp2->get('id'), 'courseid' => $c1->id]);
+        $assign1a = $dg->create_module('assign', ['course' => $c1]);
+        $assign1b = $dg->create_module('assign', ['course' => $c1]);
+        $cmc1a = $ccg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign1a->cmid]);
+        $cmc1b = $ccg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign1b->cmid]);
+        $ucc1a = $ccg->create_user_competency_course(['competencyid' => $comp1->get('id'), 'courseid' => $c1->id,
+            'userid' => $u1->id]);
+        $ucc1b = $ccg->create_user_competency_course(['competencyid' => $comp2->get('id'), 'courseid' => $c1->id,
+            'userid' => $u1->id]);
+
+        $c2 = $dg->create_course();
+        $cc2a = $ccg->create_course_competency(['competencyid' => $comp1->get('id'), 'courseid' => $c2->id]);
+        $cc2b = $ccg->create_course_competency(['competencyid' => $comp2->get('id'), 'courseid' => $c2->id]);
+        $assign2a = $dg->create_module('assign', ['course' => $c2]);
+        $assign2b = $dg->create_module('assign', ['course' => $c2]);
+        $cmc2a = $ccg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign2a->cmid]);
+        $cmc2b = $ccg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign2b->cmid]);
+        $ucc2a = $ccg->create_user_competency_course(['competencyid' => $comp1->get('id'), 'courseid' => $c2->id,
+            'userid' => $u1->id]);
+        $ucc2b = $ccg->create_user_competency_course(['competencyid' => $comp2->get('id'), 'courseid' => $c2->id,
+            'userid' => $u1->id]);
+
+        reset_course_userdata((object) ['id' => $c1->id, 'reset_competency_ratings' => true]);
+
+        delete_user($u1);
+
+        // Assert the records don't exist anymore.
+        $this->assertEquals(0, user_competency_course::count_records(['courseid' => $c1->id, 'userid' => $u1->id]));
+    }
 }
index 484a7d7..5da7105 100644 (file)
@@ -521,9 +521,9 @@ $CFG->admin = 'admin';
 //      $CFG->supportuserid = -20;
 //
 // Moodle 2.7 introduces a locking api for critical tasks (e.g. cron).
-// The default locking system to use is DB locking for Postgres, and file locking for
-// MySQL, Oracle and SQLServer. If $CFG->preventfilelocking is set, then the default
-// will always be DB locking. It can be manually set to one of the lock
+// The default locking system to use is DB locking for Postgres, MySQL, MariaDB and
+// file locking for Oracle and SQLServer. If $CFG->preventfilelocking is set, then the
+// default will always be DB locking. It can be manually set to one of the lock
 // factory classes listed below, or one of your own custom classes implementing the
 // \core\lock\lock_factory interface.
 //
@@ -537,6 +537,8 @@ $CFG->admin = 'admin';
 //
 // "\\core\\lock\\db_record_lock_factory" - DB locking based on table rows.
 //
+// "\\core\\lock\\mysql_lock_factory" - DB locking based on MySQL / MariaDB locks.
+//
 // "\\core\\lock\\postgres_lock_factory" - DB locking based on postgres advisory locks.
 //
 // Settings used by the lock factories
diff --git a/course/amd/build/activitychooser.min.js b/course/amd/build/activitychooser.min.js
new file mode 100644 (file)
index 0000000..39ab5cf
Binary files /dev/null and b/course/amd/build/activitychooser.min.js differ
diff --git a/course/amd/build/activitychooser.min.js.map b/course/amd/build/activitychooser.min.js.map
new file mode 100644 (file)
index 0000000..77cf4e6
Binary files /dev/null and b/course/amd/build/activitychooser.min.js.map differ
diff --git a/course/amd/build/local/activitychooser/dialogue.min.js b/course/amd/build/local/activitychooser/dialogue.min.js
new file mode 100644 (file)
index 0000000..e1d3ec5
Binary files /dev/null and b/course/amd/build/local/activitychooser/dialogue.min.js differ
diff --git a/course/amd/build/local/activitychooser/dialogue.min.js.map b/course/amd/build/local/activitychooser/dialogue.min.js.map
new file mode 100644 (file)
index 0000000..d2c6bcd
Binary files /dev/null and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
diff --git a/course/amd/build/local/activitychooser/repository.min.js b/course/amd/build/local/activitychooser/repository.min.js
new file mode 100644 (file)
index 0000000..da5b3c7
Binary files /dev/null and b/course/amd/build/local/activitychooser/repository.min.js differ
diff --git a/course/amd/build/local/activitychooser/repository.min.js.map b/course/amd/build/local/activitychooser/repository.min.js.map
new file mode 100644 (file)
index 0000000..67b1590
Binary files /dev/null and b/course/amd/build/local/activitychooser/repository.min.js.map differ
diff --git a/course/amd/build/local/activitychooser/selectors.min.js b/course/amd/build/local/activitychooser/selectors.min.js
new file mode 100644 (file)
index 0000000..d709bb4
Binary files /dev/null and b/course/amd/build/local/activitychooser/selectors.min.js differ
diff --git a/course/amd/build/local/activitychooser/selectors.min.js.map b/course/amd/build/local/activitychooser/selectors.min.js.map
new file mode 100644 (file)
index 0000000..98e3367
Binary files /dev/null and b/course/amd/build/local/activitychooser/selectors.min.js.map differ
diff --git a/course/amd/src/activitychooser.js b/course/amd/src/activitychooser.js
new file mode 100644 (file)
index 0000000..6f6613b
--- /dev/null
@@ -0,0 +1,148 @@
+// 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/>.
+
+/**
+ * A type of dialogue used as for choosing modules in a course.
+ *
+ * @module     core_course/activitychooser
+ * @package    core_course
+ * @copyright  2020 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import * as ChooserDialogue from 'core_course/local/activitychooser/dialogue';
+import * as Repository from 'core_course/local/activitychooser/repository';
+import selectors from 'core_course/local/activitychooser/selectors';
+import CustomEvents from 'core/custom_interaction_events';
+import * as Templates from 'core/templates';
+import * as ModalFactory from 'core/modal_factory';
+import {get_string as getString} from 'core/str';
+import Pending from 'core/pending';
+
+/**
+ * Set up the activity chooser.
+ *
+ * @method init
+ * @param {Number} courseId Course ID to use later on in fetchModules()
+ */
+export const init = courseId => {
+    const pendingPromise = new Pending();
+
+    registerListenerEvents(courseId);
+
+    pendingPromise.resolve();
+};
+
+/**
+ * Once a selection has been made make the modal & module information and pass it along
+ *
+ * @method registerListenerEvents
+ * @param {Number} courseId
+ */
+const registerListenerEvents = (courseId) => {
+    const events = [
+        'click',
+        CustomEvents.events.activate,
+        CustomEvents.events.keyboardActivate
+    ];
+
+    const fetchModuleData = (() => {
+        let innerPromise = null;
+
+        return () => {
+            if (!innerPromise) {
+                innerPromise = new Promise((resolve) => {
+                    resolve(Repository.activityModules(courseId));
+                });
+            }
+
+            return innerPromise;
+        };
+    })();
+
+    CustomEvents.define(document, events);
+
+    // Display module chooser event listeners.
+    events.forEach((event) => {
+        document.addEventListener(event, async(e) => {
+            if (e.target.closest(selectors.elements.sectionmodchooser)) {
+                const caller = e.target.closest(selectors.elements.sectionmodchooser);
+                const builtModuleData = sectionIdMapper(await fetchModuleData(), caller.dataset.sectionid);
+                const sectionModal = await modalBuilder(builtModuleData);
+
+                ChooserDialogue.displayChooser(caller, sectionModal, builtModuleData);
+            }
+        });
+    });
+};
+
+/**
+ * Given the web service data and an ID we want to make a deep copy
+ * of the WS data then add on the section ID to the addoption URL
+ *
+ * @method sectionIdMapper
+ * @param {Object} webServiceData Our original data from the Web service call
+ * @param {Array} id The ID of the section we need to append to the links
+ * @return {Array} [modules] with URL's built
+ */
+const sectionIdMapper = (webServiceData, id) => {
+    // We need to take a fresh deep copy of the original data as an object is a reference type.
+    const newData = JSON.parse(JSON.stringify(webServiceData));
+    newData.allmodules.forEach((module) => {
+        module.urls.addoption += '&section=' + id;
+    });
+    return newData.allmodules;
+};
+
+/**
+ * Build a modal for each section ID and store it into a map for quick access
+ *
+ * @method modalBuilder
+ * @param {Map} data our map of section ID's & modules to generate modals for
+ * @return {Object} TODO
+ */
+const modalBuilder = data => buildModal(templateDataBuilder(data));
+
+/**
+ * Given an array of modules we want to figure out where & how to place them into our template object
+ *
+ * @method templateDataBuilder
+ * @param {Array} data our modules to manipulate into a Templatable object
+ * @return {Object} Our built object ready to render out
+ */
+const templateDataBuilder = (data) => {
+    return {
+        'default': data,
+    };
+};
+
+/**
+ * Given an object we want to prebuild a modal ready to store into a map
+ *
+ * @method buildModal
+ * @param {Object} data The template data which contains arrays of modules
+ * @return {Object} The modal for the calling section with everything already set up
+ */
+const buildModal = data => {
+    return ModalFactory.create({
+        type: ModalFactory.types.DEFAULT,
+        title: getString('addresourceoractivity'),
+        body: Templates.render('core_course/chooser', data),
+        large: true,
+        templateContext: {
+            classes: 'modchooser'
+        }
+    });
+};
diff --git a/course/amd/src/local/activitychooser/dialogue.js b/course/amd/src/local/activitychooser/dialogue.js
new file mode 100644 (file)
index 0000000..4be4341
--- /dev/null
@@ -0,0 +1,287 @@
+// 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/>.
+
+/**
+ * A type of dialogue used as for choosing options.
+ *
+ * @module     core_course/local/chooser/dialogue
+ * @package    core
+ * @copyright  2019 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+import * as ModalEvents from 'core/modal_events';
+import selectors from 'core_course/local/activitychooser/selectors';
+import * as Templates from 'core/templates';
+import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';
+import {addIconToContainer} from 'core/loadingicon';
+
+/**
+ * Given an event from the main module 'page' navigate to it's help section via a carousel.
+ *
+ * @method showModuleHelp
+ * @param {jQuery} carousel Our initialized carousel to manipulate
+ * @param {Object} moduleData Data of the module to carousel to
+ */
+const showModuleHelp = (carousel, moduleData) => {
+    const help = carousel.find(selectors.regions.help)[0];
+    help.innerHTML = '';
+
+    // Add a spinner.
+    const spinnerPromise = addIconToContainer(help);
+
+    // Used later...
+    let transitionPromiseResolver = null;
+    const transitionPromise = new Promise(resolve => {
+        transitionPromiseResolver = resolve;
+    });
+
+    // Build up the html & js ready to place into the help section.
+    const contentPromise = Templates.renderForPromise('core_course/chooser_help', moduleData);
+
+    // Wait for the content to be ready, and for the transition to be complet.
+    Promise.all([contentPromise, spinnerPromise, transitionPromise])
+        .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))
+        .then(() => {
+            help.querySelector(selectors.regions.chooserSummary.description).focus();
+            return help;
+        })
+        .catch(Notification.exception);
+
+    // Move to the next slide, and resolve the transition promise when it's done.
+    carousel.one('slid.bs.carousel', () => {
+        transitionPromiseResolver();
+    });
+    // Trigger the transition between 'pages'.
+    carousel.carousel('next');
+};
+
+/**
+ * Register chooser related event listeners.
+ *
+ * @method registerListenerEvents
+ * @param {Promise} modal Our modal that we are working with
+ * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
+ */
+const registerListenerEvents = (modal, mappedModules) => {
+    const bodyClickListener = e => {
+        if (e.target.closest(selectors.actions.optionActions.showSummary)) {
+            const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
+
+            const module = e.target.closest(selectors.regions.chooserOption.container);
+            const moduleName = module.dataset.modname;
+            const moduleData = mappedModules.get(moduleName);
+            showModuleHelp(carousel, moduleData);
+        }
+
+        // From the help screen go back to the module overview.
+        if (e.target.matches(selectors.actions.closeOption)) {
+            const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
+
+            // Trigger the transition between 'pages'.
+            carousel.carousel('prev');
+            carousel.on('slid.bs.carousel', () => {
+                const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);
+                const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));
+                caller.focus();
+            });
+        }
+    };
+
+    modal.getBodyPromise()
+
+    // The return value of getBodyPromise is a jquery object containing the body NodeElement.
+    .then(body => body[0])
+
+    // Set up the carousel.
+    .then(body => {
+        $(body.querySelector(selectors.regions.carousel))
+            .carousel({
+                interval: false,
+                pause: true,
+                keyboard: false
+            });
+
+        return body;
+    })
+
+    // Add the listener for clicks on the body.
+    .then(body => {
+        body.addEventListener('click', bodyClickListener);
+        return body;
+    })
+
+    // Register event listeners related to the keyboard navigation controls.
+    .then(body => {
+        initKeyboardNavigation(body, mappedModules);
+        return body;
+    })
+    .catch();
+
+};
+
+/**
+ * Initialise the keyboard navigation controls for the chooser.
+ *
+ * @method initKeyboardNavigation
+ * @param {NodeElement} body Our modal that we are working with
+ * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
+ */
+const initKeyboardNavigation = (body, mappedModules) => {
+
+    const chooserOptions = body.querySelectorAll(selectors.regions.chooserOption.container);
+
+    Array.from(chooserOptions).forEach((element) => {
+        return element.addEventListener('keyup', (e) => {
+            const chooserOptions = document.querySelector(selectors.regions.chooserOptions);
+
+            // Check for enter/ space triggers for showing the help.
+            if (e.keyCode === enter || e.keyCode === space) {
+                if (e.target.matches(selectors.actions.optionActions.showSummary)) {
+                    e.preventDefault();
+                    const module = e.target.closest(selectors.regions.chooserOption.container);
+                    const moduleName = module.dataset.modname;
+                    const moduleData = mappedModules.get(moduleName);
+                    const carousel = $(body.querySelector(selectors.regions.carousel));
+                    carousel.carousel({
+                        interval: false,
+                        pause: true,
+                        keyboard: false
+                    });
+                    showModuleHelp(carousel, moduleData);
+                }
+            }
+
+            // Next.
+            if (e.keyCode === arrowRight) {
+                e.preventDefault();
+                const currentOption = e.target.closest(selectors.regions.chooserOption.container);
+                const nextOption = currentOption.nextElementSibling;
+                const firstOption = chooserOptions.firstElementChild;
+                const toFocusOption = clickErrorHandler(nextOption, firstOption);
+                focusChooserOption(toFocusOption, currentOption);
+            }
+
+            // Previous.
+            if (e.keyCode === arrowLeft) {
+                e.preventDefault();
+                const currentOption = e.target.closest(selectors.regions.chooserOption.container);
+                const previousOption = currentOption.previousElementSibling;
+                const lastOption = chooserOptions.lastElementChild;
+                const toFocusOption = clickErrorHandler(previousOption, lastOption);
+                focusChooserOption(toFocusOption, currentOption);
+            }
+
+            if (e.keyCode === home) {
+                e.preventDefault();
+                const currentOption = e.target.closest(selectors.regions.chooserOption.container);
+                const firstOption = chooserOptions.firstElementChild;
+                focusChooserOption(firstOption, currentOption);
+            }
+
+            if (e.keyCode === end) {
+                e.preventDefault();
+                const currentOption = e.target.closest(selectors.regions.chooserOption.container);
+                const lastOption = chooserOptions.lastElementChild;
+                focusChooserOption(lastOption, currentOption);
+            }
+        });
+    });
+};
+
+/**
+ * Focus on a chooser option element and remove the previous chooser element from the focus order
+ *
+ * @method focusChooserOption
+ * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus
+ * @param {HTMLElement} previousChooserOption The previous focused option element
+ */
+const focusChooserOption = (currentChooserOption, previousChooserOption = false) => {
+    if (previousChooserOption !== false) {
+        const previousChooserOptionLink = previousChooserOption.querySelector(selectors.actions.addChooser);
+        const previousChooserOptionHelp = previousChooserOption.querySelector(selectors.actions.optionActions.showSummary);
+        // Set tabindex to -1 to remove the previous chooser option element from the focus order.
+        previousChooserOption.tabIndex = -1;
+        previousChooserOptionLink.tabIndex = -1;
+        previousChooserOptionHelp.tabIndex = -1;
+    }
+
+    const currentChooserOptionLink = currentChooserOption.querySelector(selectors.actions.addChooser);
+    const currentChooserOptionHelp = currentChooserOption.querySelector(selectors.actions.optionActions.showSummary);
+    // Set tabindex to 0 to add current chooser option element to the focus order.
+    currentChooserOption.tabIndex = 0;
+    currentChooserOptionLink.tabIndex = 0;
+    currentChooserOptionHelp.tabIndex = 0;
+    // Focus the current chooser option element.
+    currentChooserOption.focus();
+};
+
+/**
+ * Small error handling function to make sure the navigated to object exists
+ *
+ * @method clickErrorHandler
+ * @param {HTMLElement} item What we want to check exists
+ * @param {HTMLElement} fallback If we dont match anything fallback the focus
+ * @return {String}
+ */
+const clickErrorHandler = (item, fallback) => {
+    if (item !== null) {
+        return item;
+    } else {
+        return fallback;
+    }
+};
+
+/**
+ * Display the module chooser.
+ *
+ * @method displayChooser
+ * @param {HTMLElement} origin The calling button
+ * @param {Object} modal Our created modal for the section
+ * @param {Array} sectionModules An array of all of the built module information
+ */
+export const displayChooser = (origin, modal, sectionModules) => {
+
+    // Make a map so we can quickly fetch a specific module's object for either rendering or searching.
+    const mappedModules = new Map();
+    sectionModules.forEach((module) => {
+        mappedModules.set(module.modulename, module);
+    });
+
+    // Register event listeners.
+    registerListenerEvents(modal, mappedModules);
+
+    // We want to focus on the action select when the dialog is closed.
+    modal.getRoot().on(ModalEvents.hidden, () => {
+        modal.destroy();
+    });
+
+    // We want to focus on the first chooser option element as soon as the modal is opened.
+    modal.getRoot().on(ModalEvents.shown, () => {
+        modal.getModal()[0].tabIndex = -1;
+
+        modal.getBodyPromise()
+        .then(body => {
+            const firstChooserOption = body[0].querySelector(selectors.regions.chooserOption.container);
+            focusChooserOption(firstChooserOption);
+
+            return;
+        })
+        .catch(Notification.exception);
+    });
+
+    modal.show();
+};
diff --git a/course/amd/src/local/activitychooser/repository.js b/course/amd/src/local/activitychooser/repository.js
new file mode 100644 (file)
index 0000000..1e6f1a5
--- /dev/null
@@ -0,0 +1,40 @@
+// 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/>.
+
+/**
+ *
+ * @module     core_course/repository
+ * @package    core_course
+ * @copyright  2019 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+import ajax from 'core/ajax';
+
+/**
+ * Fetch all the information on modules we'll need in the activity chooser.
+ *
+ * @method activityModules
+ * @param {Number} courseid What course to fetch the modules for
+ * @return {object} jQuery promise
+ */
+export const activityModules = (courseid) => {
+    const request = {
+        methodname: 'core_course_get_activity_picker_info',
+        args: {
+            courseid: courseid,
+        },
+    };
+    return ajax.call([request])[0];
+};
diff --git a/course/amd/src/local/activitychooser/selectors.js b/course/amd/src/local/activitychooser/selectors.js
new file mode 100644 (file)
index 0000000..adeb07f
--- /dev/null
@@ -0,0 +1,70 @@
+// 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 grading interface.
+ *
+ * @module     core_course/local/chooser/selectors
+ * @package    core_course
+ * @copyright  2019 Mathew May <mathew.solutions>
+ * @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 {
+    regions: {
+        chooser: getDataSelector('region', 'chooser-container'),
+        chooserOptions: getDataSelector('region', 'chooser-options-container'),
+        chooserOption: {
+            container: getDataSelector('region', 'chooser-option-container'),
+            actions: getDataSelector('region', 'chooser-option-actions-container'),
+            info: getDataSelector('region', 'chooser-option-info-container'),
+        },
+        chooserSummary: {
+            container: getDataSelector('region', 'chooser-option-summary-container'),
+            content: getDataSelector('region', 'chooser-option-summary-content-container'),
+            description: getDataSelector('region', 'summary-description'),
+            actions: getDataSelector('region', 'chooser-option-summary-actions-container'),
+        },
+        carousel: getDataSelector('region', 'carousel'),
+        help: getDataSelector('region', 'help'),
+        modules: getDataSelector('region', 'modules'),
+        getModuleSelector: modname => `[role="menuitem"][data-modname="${modname}"]`
+    },
+    actions: {
+        optionActions: {
+            showSummary: getDataSelector('action', 'show-option-summary'),
+        },
+        addChooser: getDataSelector('action', 'add-chooser-option'),
+        closeOption: getDataSelector('action', 'close-chooser-option-summary'),
+        hide: getDataSelector('action', 'hide')
+    },
+    elements: {
+        section: '.section',
+        sectionmodchooser: 'button.section-modchooser-link',
+        sitemenu: '.block_site_main_menu',
+        sitetopic: 'div.sitetopic',
+    },
+};
index de9b8b7..aea6ed0 100644 (file)
@@ -710,17 +710,38 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
      * 0 - array of ids of top-level categories (always present)
      * '0i' - array of ids of top-level categories that have visible=0 (always present but may be empty array)
      * $id (int) - array of ids of categories that are direct children of category with id $id. If
-     *   category with id $id does not exist returns false. If category has no children returns empty array
+     *   category with id $id does not exist, or category has no children, returns empty array
      * $id.'i' - array of ids of children categories that have visible=0
      *
      * @param int|string $id
      * @return mixed
      */
     protected static function get_tree($id) {
+        $all = self::get_cached_cat_tree();
+        if (is_null($all) || !isset($all[$id])) {
+            // Could not get or rebuild the tree, or requested a non-existant ID.
+            return [];
+        } else {
+            return $all[$id];
+        }
+    }
+
+    /**
+     * Return the course category tree.
+     *
+     * Returns the category tree array, from the cache if available or rebuilding the cache
+     * if required. Uses locking to prevent the cache being rebuilt by multiple requests at once.
+     *
+     * @return array|null The tree as an array, or null if rebuilding the tree failed due to a lock timeout.
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws moodle_exception
+     */
+    private static function get_cached_cat_tree() : ?array {
         $coursecattreecache = cache::make('core', 'coursecattree');
-        $rv = $coursecattreecache->get($id);
-        if ($rv !== false) {
-            return $rv;
+        $all = $coursecattreecache->get('all');
+        if ($all !== false) {
+            return $all;
         }
         // Might need to rebuild the tree. Put a lock in place to ensure other requests don't try and do this in parallel.
         $lockfactory = \core\lock\lock_config::get_lock_factory('core_coursecattree');
@@ -728,26 +749,22 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
                 course_modinfo::COURSE_CACHE_LOCK_WAIT, course_modinfo::COURSE_CACHE_LOCK_EXPIRY);
         if ($lock === false) {
             // Couldn't get a lock to rebuild the tree.
-            return [];
+            return null;
         }
-        $rv = $coursecattreecache->get($id);
-        if ($rv !== false) {
+        $all = $coursecattreecache->get('all');
+        if ($all !== false) {
             // Tree was built while we were waiting for the lock.
             $lock->release();
-            return $rv;
+            return $all;
         }
         // Re-build the tree.
         try {
             $all = self::rebuild_coursecattree_cache_contents();
-            $coursecattreecache->set_many($all);
+            $coursecattreecache->set('all', $all);
         } finally {
             $lock->release();
         }
-        if (array_key_exists($id, $all)) {
-            return $all[$id];
-        }
-        // Requested non-existing category.
-        return array();
+        return $all;
     }
 
     /**
@@ -1364,7 +1381,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
         while (count($walk) > 0) {
             $catid = array_pop($walk);
             $directchildren = self::get_tree($catid);
-            if ($directchildren !== false && count($directchildren) > 0) {
+            if (count($directchildren) > 0) {
                 $walk = array_merge($walk, $directchildren);
                 $children = array_merge($children, $directchildren);
             }
diff --git a/course/classes/external/course_module_chooser_exporter.php b/course/classes/external/course_module_chooser_exporter.php
new file mode 100644 (file)
index 0000000..01715e6
--- /dev/null
@@ -0,0 +1,151 @@
+<?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/>.
+
+/**
+ * Author exporter.
+ *
+ * @package    core_course
+ * @copyright  2019 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use renderer_base;
+
+/**
+ * Course module chooser exporter.
+ *
+ * @copyright  2019 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_module_chooser_exporter extends exporter {
+
+    /** @var array $modules Array containing the available modules */
+    private $modules;
+
+    /**
+     * Constructor.
+     *
+     * @param array $modules The available course modules
+     * @param array $related The related data for the export
+     */
+    public function __construct(array $modules, array $related = []) {
+        $this->modules = $modules;
+        return parent::__construct([], $related);
+    }
+
+    /**
+     * Return the list of additional properties.
+     *
+     * @return array
+     */
+    protected static function define_other_properties() {
+        return [
+            'options' => [
+                'multiple' => true,
+                'optional' => true,
+                'type' => [
+                    'label' => ['type' => PARAM_TEXT],
+                    'modulename' => ['type' => PARAM_TEXT],
+                    'description' => ['type' => PARAM_TEXT],
+                    'urls' => [
+                        'type' => [
+                            'addoption' => [
+                                'type' => PARAM_URL
+                            ]
+                        ]
+                    ],
+                    'icon' => [
+                        'type' => PARAM_RAW,
+                        'optional' => true,
+                        'default' => null,
+                        'null' => NULL_ALLOWED
+                    ]
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * Get the additional values to inject while exporting.
+     *
+     * @param renderer_base $output The renderer.
+     * @return array Keys are the property names, values are their values.
+     */
+    protected function get_other_values(renderer_base $output) {
+
+        $options = new \stdClass();
+        $options->trusted = false;
+        $options->noclean = false;
+        $options->smiley = false;
+        $options->filter = false;
+        $options->para = true;
+        $options->newlines = false;
+        $options->overflowdiv = false;
+
+        $context = $this->related['context'];
+
+        $modulesdata = [];
+        foreach ($this->modules as $module) {
+            $customiconurl = null;
+
+            // The property 'name' may contain more than just the module, in which case we need to extract the true module name.
+            $modulename = $module->name;
+            if ($colon = strpos($modulename, ':')) {
+                $modulename = substr($modulename, 0, $colon);
+            }
+
+            if (isset($module->help) || !empty($module->help)) {
+                list($description) = external_format_text((string) $module->help, FORMAT_MARKDOWN,
+                    $context->id, null, null, null, $options);
+            } else {
+                $description = get_string('nohelpforactivityorresource', 'moodle');
+            }
+
+            $icon = new \pix_icon('icon', '', $modulename);
+
+            // When exporting check if the title is an object, we assume it's a lang string object otherwise we send the raw string.
+            $modulesdata[] = [
+                'label' => $module->title instanceof \lang_string ? $module->title->out() : $module->title,
+                'modulename' => $modulename,
+                'description' => $description,
+                'urls' => [
+                    'addoption' => $module->link->out(false),
+                ],
+                'icon' => $icon->export_for_template($output)
+            ];
+        }
+
+        return [
+            'options' => $modulesdata
+        ];
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return [
+            'context' => 'context'
+        ];
+    }
+}
diff --git a/course/classes/output/modchooser.php b/course/classes/output/modchooser.php
deleted file mode 100644 (file)
index 95e487c..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * The modchooser renderable.
- *
- * @package    core_course
- * @copyright  2016 Frédéric Massart - FMCorz.net
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core_course\output;
-defined('MOODLE_INTERNAL') || die();
-
-use core\output\chooser;
-use core\output\chooser_section;
-use context_course;
-use lang_string;
-use moodle_url;
-use pix_icon;
-use renderer_base;
-use stdClass;
-
-/**
- * The modchooser renderable class.
- *
- * @package    core_course
- * @copyright  2016 Frédéric Massart - FMCorz.net
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class modchooser extends chooser {
-
-    /** @var stdClass The course. */
-    public $course;
-
-    /**
-     * Constructor.
-     *
-     * @param stdClass $course The course.
-     * @param stdClass[] $modules The modules.
-     */
-    public function __construct(stdClass $course, array $modules) {
-        $this->course = $course;
-
-        $sections = [];
-        $context = context_course::instance($course->id);
-
-         // Activities.
-        $activities = array_filter($modules, function($mod) {
-            return ($mod->archetype !== MOD_ARCHETYPE_RESOURCE && $mod->archetype !== MOD_ARCHETYPE_SYSTEM);
-        });
-        if (count($activities)) {
-            $sections[] = new chooser_section('activities', new lang_string('activities'),
-                array_map(function($module) use ($context) {
-                    return new modchooser_item($module, $context);
-                }, $activities)
-            );
-        }
-
-        $resources = array_filter($modules, function($mod) {
-            return ($mod->archetype === MOD_ARCHETYPE_RESOURCE);
-        });
-        if (count($resources)) {
-            $sections[] = new chooser_section('resources', new lang_string('resources'),
-                array_map(function($module) use ($context) {
-                    return new modchooser_item($module, $context);
-                }, $resources)
-            );
-        }
-
-        $actionurl = new moodle_url('/course/jumpto.php');
-        $title = new lang_string('addresourceoractivity');
-        parent::__construct($actionurl, $title, $sections, 'jumplink');
-
-        $this->set_instructions(new lang_string('selectmoduletoviewhelp'));
-        $this->add_param('course', $course->id);
-    }
-
-    /**
-     * Export for template.
-     *
-     * @param renderer_base  The renderer.
-     * @return stdClass
-     */
-    public function export_for_template(renderer_base $output) {
-        $data = parent::export_for_template($output);
-        $data->courseid = $this->course->id;
-        return $data;
-    }
-
-}
diff --git a/course/classes/output/modchooser_item.php b/course/classes/output/modchooser_item.php
deleted file mode 100644 (file)
index 553e534..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * The modchooser_item renderable.
- *
- * @package    core_course
- * @copyright  2016 Frédéric Massart - FMCorz.net
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core_course\output;
-defined('MOODLE_INTERNAL') || die();
-
-use context;
-use lang_string;
-use pix_icon;
-
-/**
- * The modchooser_item renderable class.
- *
- * @package    core_course
- * @copyright  2016 Frédéric Massart - FMCorz.net
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class modchooser_item extends \core\output\chooser_item {
-
-    /** @var string */
-    protected $customiconurl;
-
-    /**
-     * Constructor.
-     *
-     * @param stdClass $module The module.
-     * @param context $context The relevant context.
-     */
-    public function __construct($module, context $context) {
-        // The property 'name' may contain more than just the module, in which case we need to extract the true module name.
-        $modulename = $module->name;
-        if ($colon = strpos($modulename, ':')) {
-            $modulename = substr($modulename, 0, $colon);
-        }
-        if (preg_match('/src="([^"]*)"/i', $module->icon, $matches)) {
-            // Use the custom icon.
-            $this->customiconurl = str_replace('&amp;', '&', $matches[1]);
-        }
-
-        $icon = new pix_icon('icon', '', $modulename, ['class' => 'icon']);
-        $help = isset($module->help) ? $module->help : new lang_string('nohelpforactivityorresource', 'moodle');
-
-        parent::__construct($module->name, $module->title, $module->link->out(false), $icon, $help, $context);
-    }
-
-    /**
-     * Export for template.
-     *
-     * @param \renderer_base $output The renderer
-     * @return \stdClass $data
-     */
-    public function export_for_template(\renderer_base $output) {
-        $data = parent::export_for_template($output);
-        if ($this->customiconurl && !empty($data->icon['attributes'])) {
-            // Replace icon source with a module-provided icon.
-            foreach ($data->icon['attributes'] as &$attribute) {
-                if ($attribute['name'] === 'src') {
-                    $attribute['value'] = $this->customiconurl;
-                }
-            }
-        }
-        return $data;
-    }
-}
index b39acfe..c92ad97 100644 (file)
@@ -4140,4 +4140,86 @@ class core_course_external extends external_api {
         );
         return new external_single_structure($userfields);
     }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     */
+    public static function fetch_modules_activity_chooser_returns() {
+        return new external_single_structure([
+            'allmodules' => new external_multiple_structure(
+                new external_single_structure([
+                    'label' => new external_value(PARAM_TEXT, 'Human readable module name', VALUE_OPTIONAL),
+                    'modulename' => new external_value(PARAM_TEXT, 'Module name', VALUE_OPTIONAL),
+                    'description' => new external_value(PARAM_RAW, 'Help panel information', VALUE_OPTIONAL),
+                    'urls' => new external_single_structure([
+                        'addoption' => new external_value(PARAM_URL, 'The edit link for the module', VALUE_OPTIONAL),
+                    ]),
+                    'icon' => new external_single_structure([
+                        'attributes' => new external_multiple_structure(
+                            new external_single_structure([
+                                'name' => new external_value(PARAM_RAW, 'HTML attr', VALUE_OPTIONAL),
+                                'value' => new external_value(PARAM_RAW, 'Value of the HTML attr', VALUE_OPTIONAL),
+                            ])
+                        ),
+                        'extraclasses' => new external_value(PARAM_RAW, 'Anything extra the module defines', VALUE_OPTIONAL),
+                    ]),
+                ])
+            ),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     */
+    public static function fetch_modules_activity_chooser_parameters() {
+        return new external_function_parameters([
+            'courseid' => new external_value(PARAM_INT, 'ID of the course', VALUE_REQUIRED),
+        ]);
+    }
+
+    /**
+     * Given a course ID fetch all accessible modules for that course
+     *
+     * @param int $courseid The course we want to fetch the modules for
+     * @return array Contains array of modules and their metadata
+     * @throws moodle_exception
+     */
+    public static function fetch_modules_activity_chooser(int $courseid) {
+        global $DB, $OUTPUT;
+        [
+            'courseid' => $courseid,
+        ] = self::validate_parameters(self::fetch_modules_activity_chooser_parameters(), [
+            'courseid' => $courseid,
+        ]);
+        $warnings = array();
+
+        // Validate the course context.
+        $coursecontext = context_course::instance($courseid);
+        self::validate_context($coursecontext);
+        // Check to see if user can add menus and there are modules to add.
+        if (!has_capability('moodle/course:manageactivities', $coursecontext)
+            || !($modnames = get_module_types_names()) || empty($modnames)) {
+            return '';
+        }
+
+        $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+        // Retrieve all modules with associated metadata.
+        $modules = get_module_metadata($course, $modnames, null);
+        $related = [
+            'context' => $coursecontext
+        ];
+        // Export the module chooser data.
+        $modchooserdata = new \core_course\external\course_module_chooser_exporter($modules, $related);
+
+        $result = [];
+        $result['allmodules'] = $modchooserdata->export($OUTPUT)->options;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
 }
index 51b5254..83a329a 100644 (file)
@@ -141,11 +141,26 @@ class core_course_renderer extends plugin_renderer_base {
      * @return string The composed HTML for the module
      */
     public function course_modchooser($modules, $course) {
+        debugging('course_modchooser() is deprecated. Please use course_activitychooser() instead.', DEBUG_DEVELOPER);
+
+        return $this->course_activitychooser($course->id);
+    }
+
+    /**
+     * Build the HTML for the module chooser javascript popup.
+     *
+     * @param int $courseid The course id to fetch modules for.
+     * @return string
+     */
+    public function course_activitychooser($courseid) {
+
         if (!$this->page->requires->should_create_one_time_item_now('core_course_modchooser')) {
             return '';
         }
-        $modchooser = new \core_course\output\modchooser($course, $modules);
-        return $this->render($modchooser);
+
+        $this->page->requires->js_call_amd('core_course/activitychooser', 'init', [$courseid]);
+
+        return '';
     }
 
     /**
@@ -323,7 +338,12 @@ class core_course_renderer extends plugin_renderer_base {
             $modchooser.= html_writer::start_tag('div', array('class' => 'section-modchooser'));
             $icon = $this->output->pix_icon('t/add', '');
             $span = html_writer::tag('span', $straddeither, array('class' => 'section-modchooser-text'));
-            $modchooser .= html_writer::tag('span', $icon . $span, array('class' => 'section-modchooser-link'));
+            $modchooser .= html_writer::tag('button', $icon . $span, array(
+                    'class' => 'section-modchooser-link btn btn-link',
+                    'data-action' => 'open-chooser',
+                    'data-sectionid' => $section,
+                )
+            );
             $modchooser.= html_writer::end_tag('div');
             $modchooser.= html_writer::end_tag('div');
 
@@ -337,7 +357,7 @@ class core_course_renderer extends plugin_renderer_base {
                 $output = html_writer::tag('div', $output, array('class' => 'show addresourcedropdown'));
                 $modchooser = html_writer::tag('div', $modchooser, array('class' => 'hide addresourcemodchooser'));
             }
-            $output = $this->course_modchooser($modules, $course) . $modchooser . $output;
+            $output = $this->course_activitychooser($course->id) . $modchooser . $output;
         }
 
         return $output;
diff --git a/course/templates/chooser.mustache b/course/templates/chooser.mustache
new file mode 100644 (file)
index 0000000..c503c9b
--- /dev/null
@@ -0,0 +1,48 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_course/chooser
+
+    Chooser dialog template.
+
+    Example context (json):
+    {
+        "title": "Chooser title",
+        "options": {
+            "label": "Option name",
+            "description": "Option description",
+            "urls": {
+                "addoption": "http://addoptionurl.com"
+            },
+            "icon": "<img class='icon' src='http://urltooptionicon'>"
+        }
+    }
+}}
+<div data-region="carousel" class="carousel slide">
+    <div class="carousel-inner" aria-live="polite">
+        <div class="carousel-item active" data-region="modules">
+            <div class="modchoosercontainer" data-region="chooser-container" aria-label="{{#str}} activitymodules {{/str}}">
+                <div class="optionscontainer d-flex flex-wrap mw-100 p-3 position-relative" role="menubar" data-region="chooser-options-container">
+                    {{#default}}
+                        {{>core_course/chooser_item}}
+                    {{/default}}
+                </div>
+            </div>
+        </div>
+        <div class="carousel-item" data-region="help"></div>
+    </div>
+</div>
diff --git a/course/templates/chooser_help.mustache b/course/templates/chooser_help.mustache
new file mode 100644 (file)
index 0000000..ad4dda2
--- /dev/null
@@ -0,0 +1,54 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_course/chooser_help
+
+    Chooser help / more information template.
+
+    Example context (json):
+    {
+        "label": "Option name",
+        "description": "Option description",
+        "urls": {
+            "addoption": "http://addoptionurl.com"
+        },
+        "icon": "<img class='icon' src='http://urltooptionicon'>"
+    }
+}}
+<div class="optionsummary" tabindex="-1" data-region="chooser-option-summary-container" aria-labelledby="optionsummary_label" aria-describedby="optionsumary_desc">
+    <div class="content text-left mb-5 px-5 py-4" data-region="chooser-option-summary-content-container">
+        <div class="heading mb-4">
+            <h5 id="optionsummary_label">
+                {{#icon}}
+                    {{>core/pix_icon}}
+                {{/icon}}
+                {{label}}
+            </h5>
+        </div>
+        <div id="optionsumary_desc" class="description" data-region="summary-description" tabindex="0">
+            {{{description}}}
+        </div>
+    </div>
+    <div class="actions fixed-bottom w-100 d-flex justify-content-between position-absolute py-3 px-4" data-region="chooser-option-summary-actions-container">
+        <button data-action="close-chooser-option-summary" class="closeoptionsummary btn btn-secondary" tabindex="0" data-modname="{{modulename}}">
+            {{#str}} back {{/str}}
+        </button>
+        <a href="{{urls.addoption}}" title="{{#str}} addnew, moodle, {{label}} {{/str}}" data-action="add-chooser-option" class="addoption btn btn-primary" tabindex="0">
+            {{#str}} add {{/str}}
+        </a>
+    </div>
+</div>
diff --git a/course/templates/chooser_item.mustache b/course/templates/chooser_item.mustache
new file mode 100644 (file)
index 0000000..92b930f
--- /dev/null
@@ -0,0 +1,49 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_course/chooser_item
+
+    Chooser item template.
+
+    Example context (json):
+    {
+        "label": "Option name",
+        "description": "Option description",
+        "urls": {
+            "addoption": "http://addoptionurl.com"
+        },
+        "icon": "<img class='icon' src='http://urltooptionicon'>"
+    }
+}}
+<div role="menuitem" tabindex="-1" aria-label="{{label}}" class="option d-block text-center py-3 px-2" data-region="chooser-option-container" data-modname="{{modulename}}">
+    <div class="optioninfo w-100" data-region="chooser-option-info-container">
+        <a class="d-block" href="{{urls.addoption}}" title="{{#str}} addnew, moodle, {{label}} {{/str}}" tabindex="-1" data-action="add-chooser-option">
+            <span class="optionicon d-block">
+                {{#icon}}
+                    {{>core/pix_icon}}
+                {{/icon}}
+            </span>
+            <span class="optionname d-block">{{label}}</span>
+        </a>
+        <div class="optionactions btn-group" role="group" data-region="chooser-option-actions-container">
+            <button class="btn btn-icon icon-no-margin icon-size-3 m-0 optionaction" data-action="show-option-summary" tabindex="-1">
+                <span aria-hidden="true">{{#pix}} docs, core {{/pix}}</span>
+                <span class="sr-only">{{#str}} informationformodule, core_course, {{label}} {{/str}}</span>
+            </button>
+        </div>
+    </div>
+</div>
diff --git a/course/templates/modchooser.mustache b/course/templates/modchooser.mustache
deleted file mode 100644 (file)
index 88cf99c..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-{{!
-    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/>.
-}}
-{{!
-    Course module chooser.
-}}
-{{> core/chooser }}
-{{#js}}
-require([
-    'core/yui',
-    'core/str'
-], function(Y, Str) {
-    Str.get_strings([
-        { key: 'addresourceoractivity', component: 'moodle' },
-        { key: 'close', component: 'editor' },
-    ]).then(function(add, close) {
-        Y.use('moodle-course-modchooser', function() {
-            M.course.init_chooser({
-                courseid: {{courseid}},
-                closeButtonTitle: close
-            });
-        });
-    });
-});
-{{/js}}
diff --git a/course/tests/behat/activity_chooser.feature b/course/tests/behat/activity_chooser.feature
new file mode 100644 (file)
index 0000000..9d3f2b9
--- /dev/null
@@ -0,0 +1,55 @@
+@core @core_course @javascript
+Feature: Display and choose from the available activities in course
+  In order to add activities to a course
+  As a teacher
+  I should be enabled to choose from a list of available activities and also being able to read their summaries.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher | Teacher | 1 | teacher@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course | C | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher | C | editingteacher |
+    And I log in as "teacher"
+    And I am on "Course" course homepage with editing mode on
+
+  Scenario: The available activities are displayed to the teacher in the activity chooser
+    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Then I should see "Add an activity or resource" in the ".modal-title" "css_element"
+    And I should see "Assignment" in the ".modal-body" "css_element"
+
+  Scenario: The teacher can choose to add an activity from the activity items in the activity chooser
+    Given I click on "Add an activity or resource" "button" in the "Topic 3" "section"
+    When I click on "Add a new Assignment" "link" in the "Add an activity or resource" "dialogue"
+    Then I should see "Adding a new Assignment"
+    And I set the following fields to these values:
+      | Assignment name | Test Assignment Topic 3 |
+    And I press "Save and return to course"
+    Then I should see "Test Assignment Topic 3" in the "Topic 3" "section"
+
+  Scenario: The teacher can choose to add an activity from the activity summary in the activity chooser
+    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
+    When I click on "Add a new Assignment" "link" in the "help" "core_course > Activity chooser screen"
+    Then I should see "Adding a new Assignment"
+
+  Scenario: Show summary
+    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
+    Then I should see "Assignment" in the "help" "core_course > Activity chooser screen"
+    And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback."
+
+  Scenario: Hide summary
+    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    When I click on "Information about the Assignment activity" "button" in the "modules" "core_course > Activity chooser screen"
+    And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "help" "core_course > Activity chooser screen"
+    And I should see "Back" in the "help" "core_course > Activity chooser screen"
+    When I click on "Back" "button" in the "help" "core_course > Activity chooser screen"
+    Then "modules" "core_course > Activity chooser screen" should exist
+    And "help" "core_course > Activity chooser screen" should not exist
+    And "Back" "button" should not exist in the "modules" "core_course > Activity chooser screen"
+    And I should not see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "Add an activity or resource" "dialogue"
index 16fa6e3..8453529 100644 (file)
@@ -42,6 +42,41 @@ use Behat\Gherkin\Node\TableNode as TableNode,
  */
 class behat_course extends behat_base {
 
+    /**
+     * Return the list of partial named selectors.
+     *
+     * @return array
+     */
+    public static function get_partial_named_selectors(): array {
+        return [
+            new behat_component_named_selector(
+                'Activity chooser screen', [
+                    "%core_course/activityChooser%//*[@data-region=%locator%][contains(concat(' ', @class, ' '), ' carousel-item ')]"
+                ]
+            ),
+        ];
+    }
+
+    /**
+     * Return a list of the Mink named replacements for the component.
+     *
+     * Named replacements allow you to define parts of an xpath that can be reused multiple times, or in multiple
+     * xpaths.
+     *
+     * This method should return a list of {@link behat_component_named_replacement} and the docs on that class explain
+     * how it works.
+     *
+     * @return behat_component_named_replacement[]
+     */
+    public static function get_named_replacements(): array {
+        return [
+            new behat_component_named_replacement(
+                'activityChooser',
+                ".//*[contains(concat(' ', @class, ' '), ' modchooser ')][contains(concat(' ', @class, ' '), ' modal-dialog ')]"
+            ),
+        ];
+    }
+
     /**
      * Turns editing mode on.
      * @Given /^I turn editing mode on$/
@@ -203,17 +238,18 @@ class behat_course extends behat_base {
 
             // Clicks add activity or resource section link.
             $sectionxpath = $sectionxpath . "/descendant::div" .
-                    "[contains(concat(' ', normalize-space(@class) , ' '), ' section-modchooser ')]/span/a";
-            $sectionnode = $this->find('xpath', $sectionxpath);
-            $sectionnode->click();
+                    "[contains(concat(' ', normalize-space(@class) , ' '), ' section-modchooser ')]/button";
+
+            $this->execute('behat_general::i_click_on', [$sectionxpath, 'xpath']);
 
             // Clicks the selected activity if it exists.
-            $activityxpath = "//div[@id='chooseform']/descendant::label" .
-                    "/descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' typename ')]" .
+            $activityxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' modchooser ')]" .
+                    "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optioninfo ')]" .
+                    "/descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' optionname ')]" .
                     "[normalize-space(.)=$activityliteral]" .
-                    "/parent::label/child::input";
-            $activitynode = $this->find('xpath', $activityxpath);
-            $activitynode->doubleClick();
+                    "/parent::a";
+
+            $this->execute('behat_general::i_click_on', [$activityxpath, 'xpath']);
 
         } else {
             // Without Javascript.
@@ -1938,4 +1974,17 @@ class behat_course extends behat_base {
             throw new ExpectationException($msg, $this->getSession());
         }
     }
+
+    /**
+     * Open the activity chooser in a course.
+     *
+     * @Given /^I open the activity chooser$/
+     */
+    public function i_open_the_activity_chooser() {
+        $this->execute('behat_general::i_click_on',
+            array('//button[@data-action="open-chooser"]', 'xpath_element'));
+
+        $node = $this->get_selected_node('xpath_element', '//div[@data-region="modules"]');
+        $this->ensure_node_is_visible($node);
+    }
 }
index 67611e5..b2f1671 100644 (file)
@@ -3049,4 +3049,39 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(2, count($users['users']));
         $this->assertEquals($expectedusers, $users);
     }
+
+    /**
+     * Test fetch_modules_activity_chooser
+     */
+    public function test_fetch_modules_activity_chooser() {
+        global $OUTPUT;
+
+        $this->resetAfterTest(true);
+
+        // Log in as Admin.
+        $this->setAdminUser();
+
+        $course1  = self::getDataGenerator()->create_course();
+
+        // Fetch course modules.
+        $result = core_course_external::fetch_modules_activity_chooser($course1->id);
+        $result = external_api::clean_returnvalue(core_course_external::fetch_modules_activity_chooser_returns(), $result);
+        // Check for 0 warnings.
+        $this->assertEquals(0, count($result['warnings']));
+        // Check we have the right number of standard modules.
+        $this->assertEquals(21, count($result['allmodules']));
+
+        $coursecontext = context_course::instance($course1->id);
+        $modnames = get_module_types_names();
+        $modules = get_module_metadata($course1, $modnames, null);
+        $related = [
+            'context' => $coursecontext
+        ];
+        // Export the module chooser data.
+        $modchooserdata = new \core_course\external\course_module_chooser_exporter($modules, $related);
+        $formatteddata = $modchooserdata->export($OUTPUT)->options;
+
+        // Check if the webservice returns exactly what the exporter defines.
+        $this->assertEquals($formatteddata, $result['allmodules']);
+    }
 }
diff --git a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-debug.js b/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-debug.js
deleted file mode 100644 (file)
index f530680..0000000
Binary files a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-debug.js and /dev/null differ
diff --git a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-min.js b/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-min.js
deleted file mode 100644 (file)
index 703d338..0000000
Binary files a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-min.js and /dev/null differ
diff --git a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser.js b/course/yui/build/moodle-course-modchooser/moodle-course-modchooser.js
deleted file mode 100644 (file)
index f530680..0000000
Binary files a/course/yui/build/moodle-course-modchooser/moodle-course-modchooser.js and /dev/null differ
diff --git a/course/yui/src/modchooser/build.json b/course/yui/src/modchooser/build.json
deleted file mode 100644 (file)
index 569ab7b..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "name": "moodle-course-modchooser",
-  "builds": {
-    "moodle-course-modchooser": {
-      "jsfiles": [
-        "modchooser.js"
-      ]
-    }
-  }
-}
diff --git a/course/yui/src/modchooser/js/modchooser.js b/course/yui/src/modchooser/js/modchooser.js
deleted file mode 100644 (file)
index 6c75c60..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- * The activity chooser dialogue for courses.
- *
- * @module moodle-course-modchooser
- */
-
-var CSS = {
-    PAGECONTENT: 'body',
-    SECTION: null,
-    SECTIONMODCHOOSER: 'span.section-modchooser-link',
-    SITEMENU: '.block_site_main_menu',
-    SITETOPIC: 'div.sitetopic'
-};
-
-var MODCHOOSERNAME = 'course-modchooser';
-
-/**
- * The activity chooser dialogue for courses.
- *
- * @constructor
- * @class M.course.modchooser
- * @extends M.core.chooserdialogue
- */
-var MODCHOOSER = function() {
-    MODCHOOSER.superclass.constructor.apply(this, arguments);
-};
-
-Y.extend(MODCHOOSER, M.core.chooserdialogue, {
-    /**
-     * The current section ID.
-     *
-     * @property sectionid
-     * @private
-     * @type Number
-     * @default null
-     */
-    sectionid: null,
-
-    /**
-     * Set up the activity chooser.
-     *
-     * @method initializer
-     */
-    initializer: function() {
-        var sectionclass = M.course.format.get_sectionwrapperclass();
-        if (sectionclass) {
-            CSS.SECTION = '.' + sectionclass;
-        }
-        var dialogue = Y.one('.chooserdialoguebody');
-        var header = Y.one('.choosertitle');
-        var params = {};
-        this.setup_chooser_dialogue(dialogue, header, params);
-
-        // Initialize existing sections and register for dynamically created sections
-        this.setup_for_section();
-        M.course.coursebase.register_module(this);
-    },
-
-    /**
-     * Update any section areas within the scope of the specified
-     * selector with AJAX equivalents
-     *
-     * @method setup_for_section
-     * @param baseselector The selector to limit scope to
-     */
-    setup_for_section: function(baseselector) {
-        if (!baseselector) {
-            baseselector = CSS.PAGECONTENT;
-        }
-
-        // Setup for site topics
-        Y.one(baseselector).all(CSS.SITETOPIC).each(function(section) {
-            this._setup_for_section(section);
-        }, this);
-
-        // Setup for standard course topics
-        if (CSS.SECTION) {
-            Y.one(baseselector).all(CSS.SECTION).each(function(section) {
-                this._setup_for_section(section);
-            }, this);
-        }
-
-        // Setup for the block site menu
-        Y.one(baseselector).all(CSS.SITEMENU).each(function(section) {
-            this._setup_for_section(section);
-        }, this);
-    },
-
-    /**
-     * Update any section areas within the scope of the specified
-     * selector with AJAX equivalents
-     *
-     * @method _setup_for_section
-     * @private
-     * @param baseselector The selector to limit scope to
-     */
-    _setup_for_section: function(section) {
-        var chooserspan = section.one(CSS.SECTIONMODCHOOSER);
-        if (!chooserspan) {
-            return;
-        }
-        var chooserlink = Y.Node.create("<a href='#' />");
-        chooserspan.get('children').each(function(node) {
-            chooserlink.appendChild(node);
-        });
-        chooserspan.insertBefore(chooserlink);
-        chooserlink.on('click', this.display_mod_chooser, this);
-    },
-    /**
-     * Display the module chooser
-     *
-     * @method display_mod_chooser
-     * @param {EventFacade} e Triggering Event
-     */
-    display_mod_chooser: function(e) {
-        // Set the section for this version of the dialogue
-        if (e.target.ancestor(CSS.SITETOPIC)) {
-            // The site topic has a sectionid of 1
-            this.sectionid = 1;
-        } else if (e.target.ancestor(CSS.SECTION)) {
-            var section = e.target.ancestor(CSS.SECTION);
-            this.sectionid = section.get('id').replace('section-', '');
-        } else if (e.target.ancestor(CSS.SITEMENU)) {
-            // The block site menu has a sectionid of 0
-            this.sectionid = 0;
-        }
-        this.display_chooser(e);
-    },
-
-    /**
-     * Helper function to set the value of a hidden radio button when a
-     * selection is made.
-     *
-     * @method option_selected
-     * @param {String} thisoption The selected option value
-     * @private
-     */
-    option_selected: function(thisoption) {
-        // Add the sectionid to the URL.
-        this.hiddenRadioValue.setAttrs({
-            name: 'jump',
-            value: thisoption.get('value') + '&section=' + this.sectionid
-        });
-    }
-},
-{
-    NAME: MODCHOOSERNAME,
-    ATTRS: {
-        /**
-         * The maximum height (in pixels) of the activity chooser.
-         *
-         * @attribute maxheight
-         * @type Number
-         * @default 800
-         */
-        maxheight: {
-            value: 800
-        }
-    }
-});
-M.course = M.course || {};
-M.course.init_chooser = function(config) {
-    return new MODCHOOSER(config);
-};
diff --git a/course/yui/src/modchooser/meta/modchooser.json b/course/yui/src/modchooser/meta/modchooser.json
deleted file mode 100644 (file)
index 2ef461b..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "moodle-course-modchooser": {
-    "requires": [
-        "moodle-core-chooserdialogue",
-        "moodle-course-coursebase"
-    ]
-  }
-}
index 660396d..1458372 100644 (file)
@@ -164,7 +164,7 @@ class core extends \H5PCore {
      */
     public function fetch_latest_content_types(): ?\stdClass {
 
-        $contenttypes = self::get_latest_content_types();
+        $contenttypes = $this->get_latest_content_types();
         if (!empty($contenttypes->error)) {
             return $contenttypes;
         }
@@ -184,7 +184,7 @@ class core extends \H5PCore {
                 'machineName' => $type->id,
                 'majorVersion' => $type->version->major,
                 'minorVersion' => $type->version->minor,
-                'patchVersion' => $type->version->patch
+                'patchVersion' => $type->version->patch,
             ];
 
             $shoulddownload = true;
@@ -197,7 +197,7 @@ class core extends \H5PCore {
             if ($shoulddownload) {
                 $installed['id'] = $this->fetch_content_type($library);
                 if ($installed['id']) {
-                    $installed['name'] = $librarykey = \H5PCore::libraryToString($library);
+                    $installed['name'] = \H5PCore::libraryToString($library);
                     $typesinstalled[] = $installed;
                 }
             }
@@ -256,14 +256,21 @@ class core extends \H5PCore {
     /**
      * Get H5P endpoints.
      *
-     * If $library is null, moodle_url is the endpoint of the latest version of the H5P content types. If library is the
-     * machine name of a content type, moodle_url is the endpoint to download the content type.
+     * If $endpoint = 'content' and $library is null, moodle_url is the endpoint of the latest version of the H5P content
+     * types; however, if $library is the machine name of a content type, moodle_url is the endpoint to download the content type.
+     * The SITES endpoint ($endpoint = 'site') may be use to get a site UUID or send site data.
      *
      * @param string|null $library The machineName of the library whose endpoint is requested.
+     * @param string $endpoint The endpoint required. Valid values: "site", "content".
      * @return moodle_url The endpoint moodle_url object.
      */
-    public function get_api_endpoint(?string $library): moodle_url {
-        $h5purl = \H5PHubEndpoints::createURL(\H5PHubEndpoints::CONTENT_TYPES ) . $library;
+    public function get_api_endpoint(?string $library = null, string $endpoint = 'content'): moodle_url {
+        if ($endpoint == 'site') {
+            $h5purl = \H5PHubEndpoints::createURL(\H5PHubEndpoints::SITES );
+        } else if ($endpoint == 'content') {
+            $h5purl = \H5PHubEndpoints::createURL(\H5PHubEndpoints::CONTENT_TYPES ) . $library;
+        }
+
         return new moodle_url($h5purl);
     }
 
@@ -275,9 +282,11 @@ class core extends \H5PCore {
      *     - array contentTypes: an object for each H5P content type with its information
      */
     public function get_latest_content_types(): \stdClass {
+        $siteuuid = $this->get_site_uuid() ?? md5($CFG->wwwroot);
+        $postdata = ['uuid' => $siteuuid];
+
         // Get the latest content-types json.
-        $postdata = ['uuid' => 'foo'];
-        $endpoint = $this->get_api_endpoint(null);
+        $endpoint = $this->get_api_endpoint();
         $request = download_file_content($endpoint, null, $postdata, true);
 
         if (!empty($request->error) || $request->status != '200' || empty($request->results)) {
@@ -293,6 +302,43 @@ class core extends \H5PCore {
         return $contenttypes;
     }
 
+    /**
+     * Get the site UUID. If site UUID is not defined, try to register the site.
+     *
+     * return $string The site UUID, null if it is not set.
+     */
+    public function get_site_uuid(): ?string {
+        // Check if the site_uuid is already set.
+        $siteuuid = get_config('core_h5p', 'site_uuid');
+
+        if (!$siteuuid) {
+            $siteuuid = $this->register_site();
+        }
+
+        return $siteuuid;
+    }
+
+    /**
+     * Get H5P generated site UUID.
+     *
+     * return ?string Returns H5P generated site UUID, null if can't get it.
+     */
+    private function register_site(): ?string {
+        $endpoint = $this->get_api_endpoint(null, 'site');
+        $siteuuid = download_file_content($endpoint, null, '');
+
+        // Successful UUID retrieval from H5P.
+        if ($siteuuid) {
+            $json = json_decode($siteuuid);
+            if (isset($json->uuid)) {
+                set_config('site_uuid', $json->uuid, 'core_h5p');
+                return $json->uuid;
+            }
+        }
+
+        return null;
+    }
+
     /**
      * Checks that the required H5P core API version or higher is installed.
      *
@@ -308,5 +354,4 @@ class core extends \H5PCore {
         }
         return true;
     }
-
 }
index 4af2234..ee2f215 100644 (file)
@@ -45,6 +45,8 @@ class file_storage implements \H5PFileStorage {
     public const CACHED_ASSETS_FILEAREA = 'cachedassets';
     /** The export file area */
     public const EXPORT_FILEAREA = 'export';
+    /** The icon filename */
+    public const ICON_FILENAME = 'icon.svg';
 
     /**
      * @var \context $context Currently we use the system context everywhere.
@@ -347,6 +349,40 @@ class file_storage implements \H5PFileStorage {
         // This is to be implemented when the h5p editor is introduced / created.
     }
 
+    /**
+     * Get the file URL or given library and then return it.
+     *
+     * @param int $itemid
+     * @param string $machinename
+     * @param int $majorversion
+     * @param int $minorversion
+     * @return string url or false if the file doesn't exist
+     */
+    public function get_icon_url(int $itemid, string $machinename, int $majorversion, int $minorversion) {
+        $filepath = '/' . "{$machinename}-{$majorversion}.{$minorversion}" . '/';
+        if ($file = $this->fs->get_file(
+            $this->context->id,
+            self::COMPONENT,
+            self::LIBRARY_FILEAREA,
+            $itemid,
+            $filepath,
+            self::ICON_FILENAME)
+        ) {
+            $iconurl  = \moodle_url::make_pluginfile_url(
+                $this->context->id,
+                self::COMPONENT,
+                self::LIBRARY_FILEAREA,
+                $itemid,
+                $filepath,
+                $file->get_filename());
+
+            // Return image URL.
+            return $iconurl->out();
+        }
+
+        return false;
+    }
+
     /**
      * Checks to see if content has the given file.
      * Used when saving content.
index ef1ef52..d413745 100644 (file)
@@ -180,4 +180,81 @@ class helper {
         return $fs->create_file_from_pathname($filerecord, $filepath);
     }
 
+    /**
+     * Get information about different H5P tools and their status.
+     *
+     * @return array Data to render by the template
+     */
+    public static function get_h5p_tools_info(): array {
+        $tools = array();
+
+        // Getting information from available H5P tools one by one because their enabled/disabled options are totally different.
+        // Check the atto button status.
+        $link = \editor_atto\plugininfo\atto::get_manage_url();
+        $status = strpos(get_config('editor_atto', 'toolbar'), 'h5p') > -1;
+        $tools[] = self::convert_info_into_array('atto_h5p', $link, $status);
+
+        // Check the Display H5P filter status.
+        $link = \core\plugininfo\filter::get_manage_url();
+        $status = filter_get_active_state('displayh5p', \context_system::instance()->id);
+        $tools[] = self::convert_info_into_array('filter_displayh5p', $link, $status);
+
+        // Check H5P scheduled task.
+        $link = '';
+        $status = 0;
+        $statusaction = '';
+        if ($task = \core\task\manager::get_scheduled_task('\core\task\h5p_get_content_types_task')) {
+            $status = !$task->get_disabled();
+            $link = new \moodle_url(
+                '/admin/tool/task/scheduledtasks.php',
+                array('action' => 'edit', 'task' => get_class($task))
+            );
+            if ($status && \tool_task\run_from_cli::is_runnable() && get_config('tool_task', 'enablerunnow')) {
+                $statusaction = \html_writer::link(
+                    new \moodle_url('/admin/tool/task/schedule_task.php',
+                        array('task' => get_class($task))),
+                    get_string('runnow', 'tool_task'));
+            }
+        }
+        $tools[] = self::convert_info_into_array('task_h5p', $link, $status, $statusaction);
+
+        return $tools;
+    }
+
+    /**
+     * Convert information into needed mustache template data array
+     * @param string $tool The name of the tool
+     * @param \moodle_url $link The URL to management page
+     * @param int $status The current status of the tool
+     * @param string $statusaction A link to 'Run now' option for the task
+     * @return array
+     */
+    static private function convert_info_into_array(string $tool,
+        \moodle_url $link,
+        int $status,
+        string $statusaction = ''): array {
+
+        $statusclasses = array(
+            TEXTFILTER_DISABLED => 'badge badge-danger',
+            TEXTFILTER_OFF => 'badge badge-warning',
+            0 => 'badge badge-danger',
+            TEXTFILTER_ON => 'badge badge-success',
+        );
+
+        $statuschoices = array(
+            TEXTFILTER_DISABLED => get_string('disabled', 'admin'),
+            TEXTFILTER_OFF => get_string('offbutavailable', 'core_filters'),
+            0 => get_string('disabled', 'admin'),
+            1 => get_string('enabled', 'admin'),
+        );
+
+        return [
+            'tool' => get_string($tool, 'h5p'),
+            'tool_description' => get_string($tool . '_description', 'h5p'),
+            'link' => $link,
+            'status' => $statuschoices[$status],
+            'status_class' => $statusclasses[$status],
+            'status_action' => $statusaction,
+        ];
+    }
 }
index 66b4e58..bffce14 100644 (file)
@@ -67,10 +67,17 @@ $form->display();
 
 // Load installed Libraries.
 $framework = $h5pfactory->get_framework();
+$filestorage = $h5pfactory->get_core()->fs;
 $libraries = $framework->loadLibraries();
 $installed = [];
 foreach ($libraries as $libraryname => $versions) {
     foreach ($versions as $version) {
+        $version->icon = $filestorage->get_icon_url(
+            $version->id,
+            $version->machine_name,
+            $version->major_version,
+            $version->minor_version
+        );
         $installed[] = $version;
     }
 }
diff --git a/h5p/overview.php b/h5p/overview.php
new file mode 100644 (file)
index 0000000..996cb92
--- /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/>.
+
+/**
+ * Manage H5P tools status overview page.
+ *
+ * @package    core_h5p
+ * @copyright  2019 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../config.php');
+
+require_login(null, false);
+
+$context = context_system::instance();
+require_capability('moodle/site:config', $context);
+
+$pagetitle = get_string('h5poverview', 'core_h5p');
+$url = new \moodle_url("/h5p/overview.php");
+
+$PAGE->set_context($context);
+$PAGE->set_url($url);
+$PAGE->set_pagelayout('admin');
+$PAGE->set_title("$SITE->shortname: " . $pagetitle);
+$PAGE->set_heading($SITE->fullname);
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading($pagetitle);
+
+$tools = \core_h5p\helper::get_h5p_tools_info();
+echo $OUTPUT->render_from_template('core_h5p/h5ptoolsoverview', array('tools' => $tools));
+
+echo $OUTPUT->footer();
index 3ac8ee5..a6125fb 100644 (file)
@@ -25,7 +25,8 @@
                 "major_version": 1,
                 "minor_version:": 0,
                 "patch_version:": 0,
-                "runnable": 1
+                "runnable": 1,
+                "icon": "icon.svg"
             },
             {
                 "title": "Collage",
@@ -39,7 +40,8 @@
                 "major_version": 4,
                 "minor_version:": 5,
                 "patch_version:": 0,
-                "runnable": 0
+                "runnable": 1,
+                "icon": "icon.svg"
             }
         ]
     }
                         {{#runnable}}
                         <tr class="">
                             <td>
+                                {{#icon}}
+                                    <img alt=""
+                                         class="icon iconsize-big"
+                                         src="{{{ icon }}}">
+                                {{/icon}}
+                                {{^icon}}
+                                    {{#pix}} b/h5p_library, core {{/pix}}
+                                {{/icon}}
                                 {{{ title }}}
                             </td>
                             <td>{{{ major_version }}}.{{{ minor_version }}}.{{{ patch_version }}}</td>
diff --git a/h5p/templates/h5ptoolsoverview.mustache b/h5p/templates/h5ptoolsoverview.mustache
new file mode 100644 (file)
index 0000000..8dd2db5
--- /dev/null
@@ -0,0 +1,62 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_h5p/h5ptoolsoverview
+
+    Example context (json):
+    {
+        "tools": [
+            {
+                "tool": "h5ptasks",
+                "tool_description": "h5ptasks_description",
+                "link": "https://example.com/admin/tool/task/scheduledtasks.php",
+                "status": "On",
+                "status_class": "bade badge-success",
+                "status_action": "<a href=\"admin/tool/task/schedule_task.php?task=core_task_h5p_get_content_types_task\">Run now</a>"
+            },
+            {
+                "tool": "h5ptasks",
+                "tool_description": "h5ptasks_description",
+                "link": "https://example.com/admin/filters.php",
+                "status": "Off",
+                "status_class": "bade badge-danger"
+            }
+        ]
+    }
+
+}}
+<table class="admintable generaltable" id="h5ptools">
+    <thead>
+    <tr>
+        <th>{{#str}} feature, h5p {{/str}}</th>
+        <th class="text-center">{{#str}} status, h5p {{/str}}</th>
+        <th>{{#str}} description, h5p {{/str}}</th>
+    </tr>
+    </thead>
+    <tbody>
+    {{#tools}}
+        <tr class="">
+            <td><a href="{{{ link }}}" title="{{#str}} settings {{/str}}">{{{ tool }}}</a></td>
+            <td class="text-center">
+                <div class="{{{ status_class }}}">{{{ status }}}</div>
+                {{#status_action}}<div>{{{ status_action }}}</div>{{/status_action}}
+            </td>
+            <td>{{{ tool_description }}}</td>
+        </tr>
+    {{/tools}}
+    </tbody>
+</table>
diff --git a/h5p/tests/behat/h5p_overview.feature b/h5p/tests/behat/h5p_overview.feature
new file mode 100644 (file)
index 0000000..5d42da7
--- /dev/null
@@ -0,0 +1,34 @@
+@editor @core_h5p
+Feature: Check H5P tools information is correct
+
+  @javascript
+  Scenario: Display H5P filter information.
+    Given I log in as "admin"
+    When I navigate to "H5P > H5P overview" in site administration
+    Then I should see "Enable" in the "Display H5P filter" "table_row"
+    And I click on "Display H5P filter" "link"
+    And I set the field "newstate" in the "Display H5P" "table_row" to "Off, but available"
+    And I navigate to "H5P > H5P overview" in site administration
+    And I should see "Off, but available" in the "Display H5P filter" "table_row"
+
+  @javascript
+  Scenario: 'Download available H5P content types from h5p.org' scheduled task.
+    Given I log in as "admin"
+    When I navigate to "H5P > H5P overview" in site administration
+    Then I should see "Enable" in the "H5P scheduled task" "table_row"
+    And I click on "H5P scheduled task" "link"
+    And I set the field "disabled" to "1"
+    And I click on "Save changes" "button"
+    And I navigate to "H5P > H5P overview" in site administration
+    And I should see "Disable" in the "H5P scheduled task" "table_row"
+
+  @javascript
+  Scenario: H5P atto button.
+    Given I log in as "admin"
+    When I navigate to "H5P > H5P overview" in site administration
+    Then I should see "Enable" in the "Insert H5P button" "table_row"
+    And I click on "Insert H5P button" "link"
+    And I set the field "Toolbar config" to "style1 = title, bold, italic"
+    And I click on "Save changes" "button"
+    When I navigate to "H5P > H5P overview" in site administration
+    Then I should see "Disable" in the "Insert H5P button" "table_row"
index 27d8882..648b64e 100644 (file)
@@ -36,7 +36,7 @@ defined('MOODLE_INTERNAL') || die();
  *
  * @runTestsInSeparateProcesses
  */
-class h5p_core_test extends \advanced_testcase {
+class h5p_core_testcase extends \advanced_testcase {
 
     protected function setup() {
         global $CFG;
@@ -147,4 +147,27 @@ class h5p_core_test extends \advanced_testcase {
         $this->assertEquals($numcontenttypes, count($contentfiles));
         $this->assertCount(0, $result->typesinstalled);
     }
+
+    /**
+     * Test that if site_uuid is not set, the site is registered and site_uuid is set.
+     *
+     */
+    public function test_get_site_uuid(): void {
+        $this->resetAfterTest(true);
+
+        if (!PHPUNIT_LONGTEST) {
+            $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
+        }
+
+        // Check that site_uuid does not have a value.
+        $this->assertFalse(get_config('core_h5p', 'site_uuid'));
+
+        $siteuuid = $this->core->get_site_uuid();
+
+        $this->assertSame($siteuuid, get_config('core_h5p', 'site_uuid'));
+
+        // Check that after a new request the site_uuid remains the same.
+        $siteuuid2 = $this->core->get_site_uuid();
+        $this->assertEquals( $siteuuid, $siteuuid2);
+    }
 }
index 17354a1..d6199e4 100644 (file)
@@ -27,6 +27,7 @@ namespace core_h5p\local\tests;
 
 use core_h5p\file_storage;
 use core_h5p\autoloader;
+use core_h5p\helper;
 use file_archive;
 use zip_archive;
 
@@ -553,4 +554,67 @@ class h5p_file_storage_testcase extends \advanced_testcase {
                 file_storage::LIBRARY_FILEAREA);
         $this->assertCount(7, $files);
     }
+
+    /**
+     * Test get_icon_url() function behaviour.
+     *
+     * @dataProvider get_icon_url_provider
+     * @param  string  $filename  The name of the H5P file to load.
+     * @param  bool    $expected  Whether the icon should exist or not.
+     */
+    public function test_get_icon_url(string $filename, bool $expected): void {
+        global $DB;
+
+        $this->resetAfterTest();
+        $factory = new \core_h5p\factory();
+
+        $admin = get_admin();
+
+        // Prepare a valid .H5P file.
+        $path = __DIR__ . '/fixtures/'.$filename;
+
+        // Libraries can be updated when the file has been created by admin, even when the current user is not the admin.
+        $this->setUser($admin);
+        $file = helper::create_fake_stored_file_from_path($path, (int)$admin->id);
+        $factory->get_framework()->set_file($file);
+        $config = (object)[
+            'frame' => 1,
+            'export' => 1,
+            'embed' => 0,
+            'copyright' => 0,
+        ];
+
+        $h5pid = helper::save_h5p($factory, $file, $config);
+        $h5p = $DB->get_record('h5p', ['id' => $h5pid]);
+        $h5plib = $DB->get_record('h5p_libraries', ['id' => $h5p->mainlibraryid]);
+        $iconurl = $this->h5p_file_storage->get_icon_url(
+            $h5plib->id,
+            $h5plib->machinename,
+            $h5plib->majorversion,
+            $h5plib->minorversion
+        );
+        if ($expected) {
+            $this->assertContains(file_storage::ICON_FILENAME, $iconurl);
+        } else {
+            $this->assertFalse($iconurl);
+        }
+    }
+
+    /**
+     * Data provider for test_get_icon_url().
+     *
+     * @return array
+     */
+    public function get_icon_url_provider(): array {
+        return [
+            'Icon included' => [
+                'filltheblanks.h5p',
+                true,
+            ],
+            'Icon not included' => [
+                'greeting-card-887.h5p',
+                false,
+            ],
+        ];
+    }
 }
\ No newline at end of file
index 9f05ce4..a0d7bfe 100644 (file)
@@ -31,4 +31,5 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['parentlanguage'] = 'es';
+$string['thisdirection'] = 'ltr';
 $string['thislanguage'] = 'Español para la Empresa';
index a29697c..473305b 100644 (file)
@@ -480,6 +480,7 @@ $string['devicedetectregex_desc'] = '<p>By default, Moodle can detect devices of
 $string['devicedetectregexexpression'] = 'Regular expression';
 $string['devicedetectregexvalue'] = 'Return value';
 $string['devicetype'] = 'Device type';
+$string['disabled'] = 'Disabled';
 $string['disableuserimages'] = 'Disable user profile images';
 $string['displayerrorswarning'] = 'Enabling the PHP setting <em>display_errors</em> is not recommended on production sites because some error messages may reveal sensitive information about your server.';
 $string['displayloginfailures'] = 'Display login failures';
index cc82f2c..2573793 100644 (file)
@@ -42,6 +42,7 @@ $string['customfieldsettings'] = 'Common course custom fields settings';
 $string['errorendbeforestart'] = 'The end date ({$a}) is before the course start date.';
 $string['favourite'] = 'Starred course';
 $string['gradetopassnotset'] = 'This course does not have a grade to pass set. It may be set in the grade item of the course (Gradebook setup).';
+$string['informationformodule'] = 'Information about the {$a} activity';
 $string['nocourseactivity'] = 'Not enough course activity between the start and the end of the course';
 $string['nocourseendtime'] = 'The course does not have an end time';
 $string['nocoursesections'] = 'No course sections';
index abef9f9..7753147 100644 (file)
@@ -120,3 +120,4 @@ global,core_calendar
 globalevent,core_calendar
 globalevents,core_calendar
 eventtypeglobal,core_calendar
+documentation,core_webservice
index 2fc0a62..febe040 100644 (file)
@@ -30,6 +30,8 @@ $string['addedandupdatedss'] = 'Added {$a->%new} new H5P library and updated {$a
 $string['addednewlibraries'] = 'Added {$a->%new} new H5P libraries.';
 $string['addednewlibrary'] = 'Added {$a->%new} new H5P library.';
 $string['additionallicenseinfo'] = 'Any additional information about the license';
+$string['atto_h5p'] = 'Insert H5P button';
+$string['atto_h5p_description'] = 'The Insert H5P button in the Atto editor enables users to insert H5P content by either entering a URL or embed code, or by uploading an H5P file.';
 $string['author'] = 'Author';
 $string['authorcomments'] = 'Author comments';
 $string['authorcommentsdescription'] = 'Comments for the editor of the content. (This text will not be published as a part of the copyright info.)';
@@ -65,6 +67,7 @@ $string['couldNotParseJSONFromZip'] = 'Unable to parse JSON from the package: {$
 $string['couldNotReadFileFromZip'] = 'Unable to read file from the package: {$a->%fileName}';
 $string['creativecommons'] = 'Creative Commons';
 $string['date'] = 'Date';
+$string['description'] = 'Description';
 $string['disablefullscreen'] = 'Disable fullscreen';
 $string['download'] = 'Download';
 $string['downloadtitle'] = 'Download this content as a H5P file.';
@@ -73,8 +76,11 @@ $string['embed'] = 'Embed';
 $string['embedtitle'] = 'View the embed code for this content.';
 $string['eventh5pviewed'] = 'H5P content viewed';
 $string['eventh5pdeleted'] = 'H5P deleted';
+$string['feature'] = 'Feature';
 $string['fetchtypesfailure'] = 'No information could be obtained on the H5P content types available. H5P repository connection failure';
 $string['fileExceedsMaxSize'] = 'One of the files inside the package exceeds the maximum file size allowed. ({$a->%file} {$a->%used} > {$a->%max})';
+$string['filter_displayh5p'] = 'Display H5P filter';
+$string['filter_displayh5p_description'] = 'The Display H5P filter converts URLs into embedded H5P content.';
 $string['fullscreen'] = 'Fullscreen';
 $string['gpl'] = 'General Public License v3';
 $string['h5p'] = 'H5P';
@@ -83,6 +89,7 @@ $string['h5pfilenotfound'] = 'H5P file not found';
 $string['h5pinvalidurl'] = 'Invalid H5P content URL.';
 $string['h5pprivatefile'] = 'This H5P content can\'t be displayed because you don\'t have access to the .h5p file.';
 $string['h5pmanage'] = 'Manage H5P content types';
+$string['h5poverview'] = 'H5P overview';
 $string['h5ppackage'] = 'H5P content type';
 $string['h5ppackage_help'] = 'An H5P content type is a file with an H5P or ZIP extension containing all libraries required to display the content.';
 $string['hideadvanced'] = 'Hide advanced';
@@ -157,10 +164,13 @@ $string['reuseDescription'] = 'Reuse this content.';
 $string['showadvanced'] = 'Show advanced';
 $string['showless'] = 'Show less';
 $string['showmore'] = 'Show more';
+$string['status'] = 'Status';
 $string['size'] = 'Size';
 $string['source'] = 'Source';
 $string['startingover'] = 'You\'ll be starting over.';
 $string['sublevel'] = 'Sublevel';
+$string['task_h5p'] = 'H5P scheduled task';
+$string['task_h5p_description'] = 'The H5P scheduled task downloads available H5P content types from h5p.org.';
 $string['thumbnail'] = 'Thumbnail';
 $string['title'] = 'Title';
 $string['undisclosed'] = 'Undisclosed';
index 6c17f58..5010450 100644 (file)
@@ -43,6 +43,7 @@ $string['activityselect'] = 'Select this activity to be moved elsewhere';
 $string['activitysince'] = 'Activity since {$a}';
 $string['activitytypetitle'] = '{$a->activity} - {$a->type}';
 $string['activityweighted'] = 'Activity per user';
+$string['actionsfor'] = 'Actions for {$a}';
 $string['add'] = 'Add';
 $string['addactivity'] = 'Add an activity...';
 $string['addactivitytosection'] = 'Add an activity to section \'{$a}\'';
@@ -58,6 +59,7 @@ $string['addedtogroupnot'] = 'Not added to group "{$a}"';
 $string['addedtogroupnotenrolled'] = 'Not added to group "{$a}", because not enrolled in course';
 $string['addfilehere'] = 'Add file(s) here';
 $string['addinganew'] = 'Adding a new {$a}';
+$string['addnew'] = 'Add a new {$a}';
 $string['addinganewto'] = 'Adding a new {$a->what} to {$a->to}';
 $string['addingdatatoexisting'] = 'Adding data to existing';
 $string['additionalnames'] = 'Additional names';
index c5ee85c..2cfd47c 100644 (file)
@@ -60,7 +60,6 @@ $string['deletetokenconfirm'] = 'Do you really want to delete this web service t
 $string['disabledwarning'] = 'All web service protocols are disabled.  The "Enable web services" setting can be found in Advanced features.';
 $string['doc'] = 'Documentation';
 $string['docaccessrefused'] = 'You are not allowed to see the documentation for this token';
-$string['documentation'] = 'web service documentation';
 $string['downloadfiles'] = 'Can download files';
 $string['downloadfiles_help'] = 'If enabled, any user can download files with their security keys. Of course they are restricted to the files they are allowed to download in the site.';
 $string['editaservice'] = 'Edit service';
@@ -241,3 +240,6 @@ $string['wsdocumentationintro'] = 'To create a client we advise you to read the
 $string['wsdocumentationlogin'] = 'or enter your web service username and password:';
 $string['wspassword'] = 'Web service password';
 $string['wsusername'] = 'Web service username';
+
+// Deprecated since Moodle 3.9.
+$string['documentation'] = 'web service documentation';
index 604c45b..e54631a 100644 (file)
Binary files a/lib/amd/build/modal.min.js and b/lib/amd/build/modal.min.js differ
index b83637a..0a501e5 100644 (file)
Binary files a/lib/amd/build/modal.min.js.map and b/lib/amd/build/modal.min.js.map differ
index 65ab6f4..16469ae 100644 (file)
@@ -74,9 +74,13 @@ define([
         this.root = $(root);
         this.modal = this.root.find(SELECTORS.MODAL);
         this.header = this.modal.find(SELECTORS.HEADER);
+        this.headerPromise = $.Deferred();
         this.title = this.header.find(SELECTORS.TITLE);
+        this.titlePromise = $.Deferred();
         this.body = this.modal.find(SELECTORS.BODY);
+        this.bodyPromise = $.Deferred();
         this.footer = this.modal.find(SELECTORS.FOOTER);
+        this.footerPromise = $.Deferred();
         this.hiddenSiblings = [];
         this.isAttached = false;
         this.bodyJS = null;
@@ -229,6 +233,36 @@ define([
         return this.footer;
     };
 
+    /**
+     * Get a promise resolving to the title region.
+     *
+     * @method getTitlePromise
+     * @return {Promise}
+     */
+    Modal.prototype.getTitlePromise = function() {
+        return this.titlePromise;
+    };
+
+    /**
+     * Get a promise resolving to the body region.
+     *
+     * @method getBodyPromise
+     * @return {object} jQuery object
+     */
+    Modal.prototype.getBodyPromise = function() {
+        return this.bodyPromise;
+    };
+
+    /**
+     * Get a promise resolving to the footer region.
+     *
+     * @method getFooterPromise
+     * @return {object} jQuery object
+     */
+    Modal.prototype.getFooterPromise = function() {
+        return this.footerPromise;
+    };
+
     /**
      * Get the unique modal count.
      *
@@ -250,8 +284,13 @@ define([
      */
     Modal.prototype.setTitle = function(value) {
         var title = this.getTitle();
+        this.titlePromise = $.Deferred();
 
-        this.asyncSet(value, title.html.bind(title));
+        this.asyncSet(value, title.html.bind(title))
+        .then(function() {
+            this.titlePromise.resolve(title);
+        }.bind(this))
+        .catch(Notification.exception);
     };
 
     /**
@@ -264,6 +303,8 @@ define([
      * @param {(string|object)} value The body string or jQuery promise which resolves to the body.
      */
     Modal.prototype.setBody = function(value) {
+        this.bodyPromise = $.Deferred();
+
         var body = this.getBody();
 
         if (typeof value === 'string') {
@@ -271,6 +312,7 @@ define([
             body.html(value);
             Event.notifyFilterContentUpdated(body);
             this.getRoot().trigger(ModalEvents.bodyRendered, this);
+            this.bodyPromise.resolve(body);
         } else {
             var jsPendingId = 'amd-modal-js-pending-id-' + this.getModalCount();
             M.util.js_pending(jsPendingId);
@@ -360,6 +402,10 @@ define([
                 this.getRoot().trigger(ModalEvents.bodyRendered, this);
                 return result;
             }.bind(this))
+            .then(function() {
+                this.bodyPromise.resolve(body);
+                return;
+            }.bind(this))
             .fail(Notification.exception)
             .always(function() {
                 // When we're done displaying all of the content we need
@@ -389,32 +435,43 @@ define([
     Modal.prototype.setFooter = function(value) {
         // Make sure the footer is visible.
         this.showFooter();
+        this.footerPromise = $.Deferred();
 
         var footer = this.getFooter();
 
         if (typeof value === 'string') {
             // Just set the value if it's a string.
             footer.html(value);
+            this.footerPromise.resolve(footer);
         } else {
             // Otherwise we assume it's a promise to be resolved with
             // html and javascript.
-            Templates.render(TEMPLATES.LOADING, {}).done(function(html) {
+            Templates.render(TEMPLATES.LOADING, {})
+            .then(function(html) {
                 footer.html(html);
 
-                value.done(function(html, js) {
-                    footer.html(html);
-
-                    if (js) {
-                        if (this.isAttached) {
-                            // If we're in the DOM then run the JS immediately.
-                            Templates.runTemplateJS(js);
-                        } else {
-                            // Otherwise cache it to be run when we're attached.
-                            this.footerJS = js;
-                        }
+                return value;
+            })
+            .then(function(html, js) {
+                footer.html(html);
+
+                if (js) {
+                    if (this.isAttached) {
+                        // If we're in the DOM then run the JS immediately.
+                        Templates.runTemplateJS(js);
+                    } else {
+                        // Otherwise cache it to be run when we're attached.
+                        this.footerJS = js;
                     }
-                }.bind(this));
-            }.bind(this));
+                }
+
+                return footer;
+            }.bind(this))
+            .then(function(footer) {
+                this.footerPromise.resolve(footer);
+                return;
+            }.bind(this))
+            .catch(Notification.exception);
         }
     };
 
index 572a520..940d567 100644 (file)
@@ -503,10 +503,13 @@ function badges_bake($hash, $badgeid, $userid = 0, $pathhash = false) {
             $contents = $file->get_content();
 
             $filehandler = new PNG_MetaDataHandler($contents);
-            $assertion = new moodle_url('/badges/assertion.php', array('b' => $hash));
-            if ($filehandler->check_chunks("tEXt", "openbadges")) {
-                // Add assertion URL tExt chunk.
-                $newcontents = $filehandler->add_chunks("tEXt", "openbadges", $assertion->out(false));
+            // For now, the site backpack OB version will be used as default.
+            $obversion = badges_open_badges_backpack_api();
+            $assertion = new core_badges_assertion($hash, $obversion);
+            $assertionjson = json_encode($assertion->get_badge_assertion());
+            if ($filehandler->check_chunks("iTXt", "openbadges")) {
+                // Add assertion URL iTXt chunk.
+                $newcontents = $filehandler->add_chunks("iTXt", "openbadges", $assertionjson);
                 $fileinfo = array(
                         'contextid' => $user_context->id,
                         'component' => 'badges',
index a2b4a6b..e537137 100644 (file)
@@ -374,18 +374,24 @@ class core_date {
             'Central Standard Time (Mexico)' => 'America/Mexico_City',
             'Canada Central Standard Time' => 'America/Regina',
             'SA Pacific Standard Time' => 'America/Bogota',
+            'S.A. Pacific Standard Time' => 'America/Bogota',
             'Eastern Standard Time' => 'America/New_York',
             'US Eastern Standard Time' => 'America/Indianapolis',
+            'U.S. Eastern Standard Time' => 'America/Indianapolis',
             'Venezuela Standard Time' => 'America/Caracas',
             'Paraguay Standard Time' => 'America/Asuncion',
             'Atlantic Standard Time' => 'America/Halifax',
             'Central Brazilian Standard Time' => 'America/Cuiaba',
             'SA Western Standard Time' => 'America/La_Paz',
+            'S.A. Western Standard Time' => 'America/La_Paz',
             'Pacific SA Standard Time' => 'America/Santiago',
+            'Pacific S.A. Standard Time' => 'America/Santiago',
             'Newfoundland Standard Time' => 'America/St_Johns',
+            'Newfoundland and Labrador Standard Time' => 'America/St_Johns',
             'E. South America Standard Time' => 'America/Sao_Paulo',
             'Argentina Standard Time' => 'America/Buenos_Aires',
             'SA Eastern Standard Time' => 'America/Cayenne',
+            'S.A. Eastern Standard Time' => 'America/Cayenne',
             'Greenland Standard Time' => 'America/Godthab',
             'Montevideo Standard Time' => 'America/Montevideo',
             'Bahia Standard Time' => 'America/Bahia',
@@ -435,6 +441,7 @@ class core_date {
             'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
             'Myanmar Standard Time' => 'Asia/Rangoon',
             'SE Asia Standard Time' => 'Asia/Bangkok',
+            'S.E. Asia Standard Time' => 'Asia/Bangkok',
             'North Asia Standard Time' => 'Asia/Krasnoyarsk',
             'China Standard Time' => 'Asia/Shanghai',
             'North Asia East Standard Time' => 'Asia/Irkutsk',
@@ -447,8 +454,10 @@ class core_date {
             'Yakutsk Standard Time' => 'Asia/Yakutsk',
             'Cen. Australia Standard Time' => 'Australia/Adelaide',
             'AUS Central Standard Time' => 'Australia/Darwin',
+            'A.U.S. Central Standard Time' => 'Australia/Darwin',
             'E. Australia Standard Time' => 'Australia/Brisbane',
             'AUS Eastern Standard Time' => 'Australia/Sydney',
+            'A.U.S. Eastern Standard Time' => 'Australia/Sydney',
             'West Pacific Standard Time' => 'Pacific/Port_Moresby',
             'Tasmania Standard Time' => 'Australia/Hobart',
             'Magadan Standard Time' => 'Asia/Magadan',
@@ -458,9 +467,18 @@ class core_date {
             'Russia Time Zone 11' => 'Asia/Kamchatka',
             'New Zealand Standard Time' => 'Pacific/Auckland',
             'Fiji Standard Time' => 'Pacific/Fiji',
+            'Fiji Islands Standard Time' => 'Pacific/Fiji',
             'Tonga Standard Time' => 'Pacific/Tongatapu',
             'Samoa Standard Time' => 'Pacific/Apia',
             'Line Islands Standard Time' => 'Pacific/Kiritimati',
+            'Mexico Standard Time 2' => 'America/Chihuahua',
+            'Mexico Standard Time' => 'America/Mexico_City',
+            'U.S. Mountain Standard Time' => 'America/Phoenix',
+            'Mid-Atlantic Standard Time' => 'Atlantic/South_Georgia',
+            'E. Europe Standard Time' => 'Europe/Minsk',
+            'Transitional Islamic State of Afghanistan Standard Time' => 'Asia/Kabul',
+            'Armenian Standard Time' => 'Asia/Yerevan',
+            'Kamchatka Standard Time' => 'Asia/Kamchatka',
 
             // A lot more bad legacy time zones.
             'CET' => 'Europe/Berlin',
diff --git a/lib/classes/lock/mysql_lock_factory.php b/lib/classes/lock/mysql_lock_factory.php
new file mode 100644 (file)
index 0000000..6f05924
--- /dev/null
@@ -0,0 +1,189 @@
+<?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/>.
+
+/**
+ * MySQL / MariaDB locking factory.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * MySQL / MariaDB locking factory.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Brendan Heywood <brendan@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mysql_lock_factory implements lock_factory {
+
+    /** @var string $dbprefix - used as a namespace for these types of locks */
+    protected $dbprefix = '';
+
+    /** @var \moodle_database $db Hold a reference to the global $DB */
+    protected $db;
+
+    /** @var array $openlocks - List of held locks - used by auto-release */
+    protected $openlocks = [];
+
+    /**
+     * Return a unique prefix based on the database name and prefix.
+     * @param string $type - Used to prefix lock keys.
+     * @return string.
+     */
+    protected function get_unique_db_prefix($type) {
+        global $CFG;
+        $prefix = $CFG->dbname . ':';
+        if (isset($CFG->prefix)) {
+            $prefix .= $CFG->prefix;
+        }
+        $prefix .= '_' . $type . '_';
+        return $prefix;
+    }
+
+    /**
+     * Lock constructor.
+     * @param string $type - Used to prefix lock keys.
+     */
+    public function __construct($type) {
+        global $DB;
+
+        $this->dbprefix = $this->get_unique_db_prefix($type);
+        // Save a reference to the global $DB so it will not be released while we still have open locks.
+        $this->db = $DB;
+
+        \core_shutdown_manager::register_function([$this, 'auto_release']);
+    }
+
+    /**
+     * Is available.
+     * @return boolean - True if this lock type is available in this environment.
+     */
+    public function is_available() {
+        return $this->db->get_dbfamily() === 'mysql';
+    }
+
+    /**
+     * Return information about the blocking behaviour of the lock type on this platform.
+     * @return boolean - Defer to the DB driver.
+     */
+    public function supports_timeout() {
+        return true;
+    }
+
+    /**
+     * Will this lock type will be automatically released when a process ends.
+     *
+     * @return boolean - Via shutdown handler.
+     */
+    public function supports_auto_release() {
+        return true;
+    }
+
+    /**
+     * Multiple locks for the same resource can NOT be held by a single process.
+     *
+     * Hard coded to false and workaround inconsistent support in different
+     * versions of MySQL / MariaDB.
+     *
+     * @return boolean - false
+     */
+    public function supports_recursion() {
+        return false;
+    }
+
+    /**
+     * Create and get a lock
+     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+     * @param int $timeout - The number of seconds to wait for a lock before giving up.
+     * @param int $maxlifetime - Unused by this lock type.
+     * @return boolean - true if a lock was obtained.
+     */
+    public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+
+        // We sha1 to avoid long key names hitting the mysql str limit.
+        $resourcekey = sha1($this->dbprefix . $resource);
+
+        // Even though some versions of MySQL and MariaDB can support stacked locks
+        // just never stack them and always fail fast.
+        if (isset($this->openlocks[$resourcekey])) {
+            return false;
+        }
+
+        $params = [
+            'resourcekey' => $resourcekey,
+            'timeout' => $timeout
+        ];
+
+        $result = $this->db->get_record_sql('SELECT GET_LOCK(:resourcekey, :timeout) AS locked', $params);
+        $locked = $result->locked == 1;
+
+        if ($locked) {
+            $this->openlocks[$resourcekey] = 1;
+            return new lock($resourcekey, $this);
+        }
+        return false;
+    }
+
+    /**
+     * Release a lock that was previously obtained with @lock.
+     * @param lock $lock - a lock obtained from this factory.
+     * @return boolean - true if the lock is no longer held (including if it was never held).
+     */
+    public function release_lock(lock $lock) {
+
+        $params = [
+            'resourcekey' => $lock->get_key()
+        ];
+        $result = $this->db->get_record_sql('SELECT RELEASE_LOCK(:resourcekey) AS unlocked', $params);
+        $result = $result->unlocked == 1;
+        if ($result) {
+            unset($this->openlocks[$lock->get_key()]);
+        }
+        return $result;
+    }
+
+    /**
+     * Extend a lock that was previously obtained with @lock.
+     * @param lock $lock - a lock obtained from this factory.
+     * @param int $maxlifetime - the new lifetime for the lock (in seconds).
+     * @return boolean - true if the lock was extended.
+     */
+    public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        // Not supported by this factory.
+        return false;
+    }
+
+    /**
+     * Auto release any open locks on shutdown.
+     * This is required, because we may be using persistent DB connections.
+     */
+    public function auto_release() {
+        // Called from the shutdown handler. Must release all open locks.
+        foreach ($this->openlocks as $key => $unused) {
+            $lock = new lock($key, $this);
+            $lock->release();
+        }
+    }
+
+}
index 8a6f101..7510dcc 100644 (file)
@@ -292,7 +292,27 @@ class manager {
 
         // Set configuration.
         session_name($sessionname);
-        session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $cookiesecure, $CFG->cookiehttponly);
+
+        if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
+            $sessionoptions = [
+                'lifetime' => 0,
+                'path' => $CFG->sessioncookiepath,
+                'domain' => $CFG->sessioncookiedomain,
+                'secure' => $cookiesecure,
+                'httponly' => $CFG->cookiehttponly,
+            ];
+
+            if (self::should_use_samesite_none()) {
+                // If $samesite is empty, we don't want there to be any SameSite attribute.
+                $sessionoptions['samesite'] = 'None';
+            }
+
+            session_set_cookie_params($sessionoptions);
+        } else {
+            // Once PHP 7.3 becomes our minimum, drop this in favour of the alternative call to session_set_cookie_params above,
+            // as that does not require a hack to work with same site settings on cookies.
+            session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $cookiesecure, $CFG->cookiehttponly);
+        }
         ini_set('session.use_trans_sid', '0');
         ini_set('session.use_only_cookies', '1');
         ini_set('session.hash_function', '0');        // For now MD5 - we do not have room for sha-1 in sessions table.
@@ -455,6 +475,8 @@ class manager {
         if ($timedout) {
             $_SESSION['SESSION']->has_timed_out = true;
         }
+
+        self::append_samesite_cookie_attribute();
     }
 
     /**
@@ -522,6 +544,61 @@ class manager {
 
         // Setup $USER object.
         self::set_user($user);
+        self::append_samesite_cookie_attribute();
+    }
+
+    /**
+     * Returns a valid setting for the SameSite cookie attribute.
+     *
+     * @return string The desired setting for the SameSite attribute on the cookie. Empty string indicates the SameSite attribute
+     * should not be set at all.
+     */
+    private static function should_use_samesite_none(): bool {
+        // We only want None or no attribute at this point. When we have cookie handling compatible with Lax,
+        // we can look at checking a setting.
+
+        // Browser support for none is not consistent yet. There are known issues with Safari, and IE11.
+        // Things are stablising, however as they're not stable yet we will deal specifically with the version of chrome
+        // that introduces a default of lax, setting it to none for the current version of chrome (2 releases before the change).
+        // We also check you are using secure cookies and HTTPS because if you are not running over HTTPS
+        // then setting SameSite=None will cause your session cookie to be rejected.
+        if (\core_useragent::is_chrome() && \core_useragent::check_chrome_version('78') && is_moodle_cookie_secure()) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Conditionally append the SameSite attribute to the session cookie if necessary.
+     *
+     * Contains a hack for versions of PHP lower than 7.3 as there is no API built into PHP cookie API
+     * for adding the SameSite setting.
+     *
+     * This won't change the Set-Cookie headers if:
+     *  - PHP 7.3 or higher is being used. That already adds the SameSite attribute without any hacks.
+     *  - If the samesite setting is empty.
+     *  - If the samesite setting is None but the browser is not compatible with that setting.
+     */
+    private static function append_samesite_cookie_attribute() {
+        if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
+            // This hack is only necessary if we weren't able to set the samesite flag via the session_set_cookie_params API.
+            return;
+        }
+
+        if (!self::should_use_samesite_none()) {
+            return;
+        }
+
+        $cookies = headers_list();
+        header_remove('Set-Cookie');
+        $setcookiesession = 'Set-Cookie: ' . session_name() . '=';
+
+        foreach ($cookies as $cookie) {
+            if (strpos($cookie, $setcookiesession) === 0) {
+                $cookie .= '; SameSite=None';
+            }
+            header($cookie, false);
+        }
     }
 
     /**
@@ -558,6 +635,8 @@ class manager {
         self::add_session_record($_SESSION['USER']->id); // Do not use $USER here because it may not be set up yet.
         session_write_close();
         self::$sessionactive = false;
+
+        self::append_samesite_cookie_attribute();
     }
 
     /**
index de2a200..50422b2 100644 (file)
@@ -629,6 +629,14 @@ $functions = array(
         'type' => 'read',
         'ajax' => true,
     ),
+    'core_course_get_activity_picker_info' => array(
+        'classname' => 'core_course_external',
+        'methodname' => 'fetch_modules_activity_chooser',
+        'classpath' => 'course/externallib.php',
+        'description' => 'Fetch all the module information for the activity picker',
+        'type' => 'read',
+        'ajax' => true,
+    ),
     'core_enrol_get_course_enrolment_methods' => array(
         'classname' => 'core_enrol_external',
         'methodname' => 'get_course_enrolment_methods',
index 1a7ebc4..de135c8 100644 (file)
@@ -2172,5 +2172,48 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2020011700.02);
     }
 
+    if ($oldversion < 2020013000.01) {
+        global $DB;
+        // Delete any associated files.
+        $fs = get_file_storage();
+        $sql = "SELECT cuc.id, cuc.userid
+                  FROM {competency_usercomp} cuc
+             LEFT JOIN {user} u ON cuc.userid = u.id
+                 WHERE u.deleted = 1";
+        $usercompetencies = $DB->get_records_sql($sql);
+        foreach ($usercompetencies as $usercomp) {
+            $DB->delete_records('competency_evidence', ['usercompetencyid' => $usercomp->id]);
+            $DB->delete_records('competency_usercompcourse', ['userid' => $usercomp->userid]);
+            $DB->delete_records('competency_usercompplan', ['userid' => $usercomp->userid]);
+            $DB->delete_records('competency_usercomp', ['userid' => $usercomp->userid]);
+        }
+
+        $sql = "SELECT cue.id, cue.userid
+                  FROM {competency_userevidence} cue
+             LEFT JOIN {user} u ON cue.userid = u.id
+                 WHERE u.deleted = 1";
+        $userevidences = $DB->get_records_sql($sql);
+        foreach ($userevidences as $userevidence) {
+            $DB->delete_records('competency_userevidencecomp', ['userevidenceid' => $userevidence->id]);
+            $DB->delete_records('competency_userevidence', ['id' => $userevidence->id]);
+
+            $context = context_user::instance($userevidence->userid);
+            $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id);
+        }
+
+        $sql = "SELECT cp.id
+                  FROM {competency_plan} cp
+             LEFT JOIN {user} u ON cp.userid = u.id
+                 WHERE u.deleted = 1";
+        $userplans = $DB->get_records_sql($sql);
+        foreach ($userplans as $userplan) {
+            $DB->delete_records('competency_plancomp', ['planid' => $userplan->id]);
+            $DB->delete_records('competency_plan', ['id' => $userplan->id]);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2020013000.01);
+    }
+
     return true;
 }
index 7ddf633..c74b839 100644 (file)
@@ -330,6 +330,8 @@ class database_manager {
             throw new ddl_exception('ddlunknownerror', null, 'table drop sql not generated');
         }
         $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
+
+        $this->generator->cleanup_after_drop($xmldb_table);
     }
 
     /**
index ff672ed..8c0bd93 100644 (file)
@@ -183,21 +183,6 @@ class mssql_sql_generator extends sql_generator {
         return $sqlarr;
     }
 
-    /**
-     * Given one correct xmldb_table, returns the SQL statements
-     * to drop it (inside one array).
-     *
-     * @param xmldb_table $xmldb_table The table to drop.
-     * @return array SQL statement(s) for dropping the specified table.
-     */
-    public function getDropTableSQL($xmldb_table) {
-        $sqlarr = parent::getDropTableSQL($xmldb_table);
-        if ($this->temptables->is_temptable($xmldb_table->getName())) {
-            $this->temptables->delete_temptable($xmldb_table->getName());
-        }
-        return $sqlarr;
-    }
-
     /**
      * Given one XMLDB Type, length and decimals, returns the DB proper SQL type.
      *
index d768ca5..def4c20 100644 (file)
@@ -384,7 +384,6 @@ class mysql_sql_generator extends sql_generator {
         $sqlarr = parent::getDropTableSQL($xmldb_table);
         if ($this->temptables->is_temptable($xmldb_table->getName())) {
             $sqlarr = preg_replace('/^DROP TABLE/', "DROP TEMPORARY TABLE", $sqlarr);
-            $this->temptables->delete_temptable($xmldb_table->getName());
         }
         return $sqlarr;
     }
index 4fb03bd..e501316 100644 (file)
@@ -211,7 +211,6 @@ class oracle_sql_generator extends sql_generator {
         $sqlarr = parent::getDropTableSQL($xmldb_table);
         if ($this->temptables->is_temptable($xmldb_table->getName())) {
             array_unshift($sqlarr, "TRUNCATE TABLE ". $this->getTableName($xmldb_table)); // oracle requires truncate before being able to drop a temp table
-            $this->temptables->delete_temptable($xmldb_table->getName());
         }
         return $sqlarr;
     }
index eb2685e..eeb3354 100644 (file)
@@ -103,21 +103,6 @@ class postgres_sql_generator extends sql_generator {
         return $sqlarr;
     }
 
-    /**
-     * Given one correct xmldb_table, returns the SQL statements
-     * to drop it (inside one array).
-     *
-     * @param xmldb_table $xmldb_table The table to drop.
-     * @return array SQL statement(s) for dropping the specified table.
-     */
-    public function getDropTableSQL($xmldb_table) {
-        $sqlarr = parent::getDropTableSQL($xmldb_table);
-        if ($this->temptables->is_temptable($xmldb_table->getName())) {
-            $this->temptables->delete_temptable($xmldb_table->getName());
-        }
-        return $sqlarr;
-    }
-
     /**
      * Given one correct xmldb_index, returns the SQL statements
      * needed to create it (in array).
index 9ad419f..99fd21f 100644 (file)
@@ -653,7 +653,7 @@ abstract class sql_generator {
     }
 
     /**
-     * Given one correct xmldb_table and the new name, returns the SQL statements
+     * Given one correct xmldb_table, returns the SQL statements
      * to drop it (inside one array). Works also for temporary tables.
      *
      * @param xmldb_table $xmldb_table The table to drop.
@@ -674,6 +674,17 @@ abstract class sql_generator {
         return $results;
     }
 
+    /**
+     * Performs any clean up that needs to be done after a table is dropped.
+     *
+     * @param xmldb_table $table
+     */
+    public function cleanup_after_drop(xmldb_table $table): void {
+        if ($this->temptables->is_temptable($table->getName())) {
+            $this->temptables->delete_temptable($table->getName());
+        }
+    }
+
     /**
      * Given one xmldb_table and one xmldb_field, return the SQL statements needed to add the field to the table.
      *
index a913ce7..9ffeb10 100644 (file)
@@ -1828,6 +1828,76 @@ class core_ddl_testcase extends database_driver_testcase {
         $this->assertFalse($dbman->table_exists('test_table1'));
     }
 
+    /**
+     * get_columns should return an empty array for ex-temptables.
+     */
+    public function test_leftover_temp_tables_columns() {
+        $DB = $this->tdb; // Do not use global $DB!
+        $dbman = $this->tdb->get_manager();
+
+        // Create temp table0.
+        $table0 = $this->tables['test_table0'];
+        $dbman->create_temp_table($table0);
+
+        $dbman->drop_table($table0);
+
+        // Get columns and perform some basic tests.
+        $columns = $DB->get_columns('test_table0');
+        $this->assertEquals([], $columns);
+    }
+
+    /**
+     * Deleting a temp table should not purge the whole cache
+     */
+    public function test_leftover_temp_tables_cache() {
+        $DB = $this->tdb; // Do not use global $DB!
+        $dbman = $this->tdb->get_manager();
+
+        // Create 2 temp tables.
+        $table0 = $this->tables['test_table0'];
+        $dbman->create_temp_table($table0);
+        $table1 = $this->tables['test_table1'];
+        $dbman->create_temp_table($table1);
+
+        // Create a normal table.
+        $table2 = new xmldb_table ('test_table2');
+        $table2->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table2->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+        $table2->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table2->setComment("This is a test'n drop table. You can drop it safely");
+        $this->tables[$table2->getName()] = $table2;
+        $dbman->create_table($table2);
+
+        // Get columns for the tables, so that relevant caches are populated with their data.
+        $DB->get_columns('test_table0');
+        $DB->get_columns('test_table1');
+        $DB->get_columns('test_table2');
+
+        $dbman->drop_table($table0);
+
+        $rc = new ReflectionClass('moodle_database');
+        $rcm = $rc->getMethod('get_temp_tables_cache');
+        $rcm->setAccessible(true);
+        $metacachetemp = $rcm->invokeArgs($DB, []);
+
+        // Data of test_table0 should be removed from the cache.
+        $this->assertEquals(false, $metacachetemp->has('test_table0'));
+
+        // Data of test_table1 should be intact.
+        $this->assertEquals(true, $metacachetemp->has('test_table1'));
+
+        $rc = new ReflectionClass('moodle_database');
+        $rcm = $rc->getMethod('get_metacache');
+        $rcm->setAccessible(true);
+        $metacache = $rcm->invokeArgs($DB, []);
+
+        // Data of test_table2 should be intact.
+        $this->assertEquals(true, $metacache->has('test_table2'));
+
+        // Delete the leftover temp table.
+        $dbman->drop_table($table1);
+    }
+
     public function test_reset_sequence() {
         $DB = $this->tdb;
         $dbman = $DB->get_manager();
index 021706b..1d86919 100644 (file)
@@ -1094,11 +1094,48 @@ abstract class moodle_database {
 
     /**
      * Returns detailed information about columns in table. This information is cached internally.
+     *
      * @param string $table The table's name.
      * @param bool $usecache Flag to use internal cacheing. The default is true.
      * @return database_column_info[] of database_column_info objects indexed with column names
      */
-    public abstract function get_columns($table, $usecache=true);
+    public function get_columns($table, $usecache = true): array {
+        if (!$table) { // Table not specified, return empty array directly.
+            return [];
+        }
+
+        if ($usecache) {
+            if ($this->temptables->is_temptable($table)) {
+                if ($data = $this->get_temp_tables_cache()->get($table)) {
+                    return $data;
+                }
+            } else {
+                if ($data = $this->get_metacache()->get($table)) {
+                    return $data;
+                }
+            }
+        }
+
+        $structure = $this->fetch_columns($table);
+
+        if ($usecache) {
+            if ($this->temptables->is_temptable($table)) {
+                $this->get_temp_tables_cache()->set($table, $structure);
+            } else {
+                $this->get_metacache()->set($table, $structure);
+            }
+        }
+
+        return $structure;
+    }
+
+    /**
+     * Returns detailed information about columns in table. This information is cached internally.
+     *
+     * @param string $table The table's name.
+     * @return database_column_info[] of database_column_info objects indexed with column names
+     */
+    protected abstract function fetch_columns(string $table): array;
 
     /**
      * Normalise values based on varying RDBMS's dependencies (booleans, LOBs...)
index 8469c0d..0b58d07 100644 (file)
@@ -700,24 +700,12 @@ class mysqli_native_moodle_database extends moodle_database {
     }
 
     /**
-     * Returns detailed information about columns in table. This information is cached internally.
+     * Fetches detailed information about columns in table.
+     *
      * @param string $table name
-     * @param bool $usecache
      * @return database_column_info[] array of database_column_info objects indexed with column names
      */
-    public function get_columns($table, $usecache=true) {
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                if ($data = $this->get_temp_tables_cache()->get($table)) {
-                    return $data;
-                }
-            } else {
-                if ($data = $this->get_metacache()->get($table)) {
-                    return $data;
-                }
-            }
-        }
-
+    protected function fetch_columns(string $table): array {
         $structure = array();
 
         $sql = "SELECT column_name, data_type, character_maximum_length, numeric_precision,
@@ -821,14 +809,6 @@ class mysqli_native_moodle_database extends moodle_database {
             $result->close();
         }
 
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                $this->get_temp_tables_cache()->set($table, $structure);
-            } else {
-                $this->get_metacache()->set($table, $structure);
-            }
-        }
-
         return $structure;
     }
 
index 6f91610..5bffeb6 100644 (file)
@@ -468,29 +468,12 @@ class oci_native_moodle_database extends moodle_database {
     }
 
     /**
-     * Returns detailed information about columns in table. This information is cached internally.
+     * Fetches detailed information about columns in table.
+     *
      * @param string $table name
-     * @param bool $usecache
      * @return array array of database_column_info objects indexed with column names
      */
-    public function get_columns($table, $usecache=true) {
-
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                if ($data = $this->get_temp_tables_cache()->get($table)) {
-                    return $data;
-                }
-            } else {
-                if ($data = $this->get_metacache()->get($table)) {
-                    return $data;
-                }
-            }
-        }
-
-        if (!$table) { // table not specified, return empty array directly
-            return array();
-        }
-
+    protected function fetch_columns(string $table): array {
         $structure = array();
 
         // We give precedence to CHAR_LENGTH for VARCHAR2 columns over WIDTH because the former is always
@@ -673,14 +656,6 @@ class oci_native_moodle_database extends moodle_database {
             $structure[$info->name] = new database_column_info($info);
         }
 
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                $this->get_temp_tables_cache()->set($table, $structure);
-            } else {
-                $this->get_metacache()->set($table, $structure);
-            }
-        }
-
         return $structure;
     }
 
index c54384e..9bd1c88 100644 (file)
@@ -388,24 +388,12 @@ class pgsql_native_moodle_database extends moodle_database {
     }
 
     /**
-     * Returns detailed information about columns in table. This information is cached internally.
+     * Returns detailed information about columns in table.
+     *
      * @param string $table name
-     * @param bool $usecache
      * @return database_column_info[] array of database_column_info objects indexed with column names
      */
-    public function get_columns($table, $usecache=true) {
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                if ($data = $this->get_temp_tables_cache()->get($table)) {
-                    return $data;
-                }
-            } else {
-                if ($data = $this->get_metacache()->get($table)) {
-                    return $data;
-                }
-            }
-        }
-
+    protected function fetch_columns(string $table): array {
         $structure = array();
 
         $tablename = $this->prefix.$table;
@@ -605,14 +593,6 @@ class pgsql_native_moodle_database extends moodle_database {
 
         pg_free_result($result);
 
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                $this->get_temp_tables_cache()->set($table, $structure);
-            } else {
-                $this->get_metacache()->set($table, $structure);
-            }
-        }
-
         return $structure;
     }
 
index 58753bd..2a540b8 100644 (file)
@@ -193,25 +193,12 @@ class sqlite3_pdo_moodle_database extends pdo_moodle_database {
     }
 
     /**
-     * Returns detailed information about columns in table. This information is cached internally.
+     * Returns detailed information about columns in table.
+     *
      * @param string $table name
-     * @param bool $usecache
      * @return array array of database_column_info objects indexed with column names
      */
-    public function get_columns($table, $usecache=true) {
-
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                if ($data = $this->get_temp_tables_cache()->get($table)) {
-                    return $data;
-                }
-            } else {
-                if ($data = $this->get_metacache()->get($table)) {
-                    return $data;
-                }
-            }
-        }
-
+    protected function fetch_columns(string $table): array {
         $structure = array();
 
         // get table's CREATE TABLE command (we'll need it for autoincrement fields)
@@ -303,14 +290,6 @@ class sqlite3_pdo_moodle_database extends pdo_moodle_database {
             $structure[$columninfo['name']] = new database_column_info($columninfo);
         }
 
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                $this->get_temp_tables_cache()->set($table, $structure);
-            } else {
-                $this->get_metacache()->set($table, $structure);
-            }
-        }
-
         return $structure;
     }
 
index a91317d..8b1f156 100644 (file)
@@ -108,7 +108,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
         return 'mssql';
     }
 
-   /**
+    /**
      * Returns more specific database driver type
      * Note: can be used before connect()
      * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
@@ -117,7 +117,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
         return 'sqlsrv';
     }
 
-   /**
+    /**
      * Returns general database library name
      * Note: can be used before connect()
      * @return string db type pdo, native
@@ -534,24 +534,12 @@ class sqlsrv_native_moodle_database extends moodle_database {
     }
 
     /**
-     * Returns detailed information about columns in table. This information is cached internally.
+     * Returns detailed information about columns in table.
+     *
      * @param string $table name
-     * @param bool $usecache
      * @return array array of database_column_info objects indexed with column names
      */
-    public function get_columns($table, $usecache = true) {
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                if ($data = $this->get_temp_tables_cache()->get($table)) {
-                    return $data;
-                }
-            } else {
-                if ($data = $this->get_metacache()->get($table)) {
-                    return $data;
-                }
-            }
-        }
-
+    protected function fetch_columns(string $table): array {
         $structure = array();
 
         if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema
@@ -642,14 +630,6 @@ class sqlsrv_native_moodle_database extends moodle_database {
         }
         $this->free_result($result);
 
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                $this->get_temp_tables_cache()->set($table, $structure);
-            } else {
-                $this->get_metacache()->set($table, $structure);
-            }
-        }
-
         return $structure;
     }
 
index a87c362..609c200 100644 (file)
@@ -5712,7 +5712,9 @@ class moodle_database_for_testing extends moodle_database {
     public function get_last_error() {}
     public function get_tables($usecache=true) {}
     public function get_indexes($table) {}
-    public function get_columns($table, $usecache=true) {}
+    protected function fetch_columns(string $table): array {
+        return [];
+    }
     protected function normalise_value($column, $value) {}
     public function set_debug($state) {}
     public function get_debug() {}
index 836a2bf..29ed69b 100644 (file)
Binary files a/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-debug.js and b/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-debug.js differ
index f18c330..41c5f75 100644 (file)
Binary files a/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-min.js and b/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-min.js differ
index 836a2bf..29ed69b 100644 (file)
Binary files a/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button.js and b/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button.js differ
index 66971a8..4b753ba 100644 (file)
@@ -186,32 +186,11 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
             tagMatchRequiresAll: false
         });
 
-        this.editor.on(['keyup', 'cut'], this._clearH5P, this);
         this.editor.all('.h5p-placeholder').setAttribute('contenteditable', 'false');
         this.editor.delegate('dblclick', this._handleDblClick, '.h5p-placeholder', this);
         this.editor.delegate('click', this._handleClick, '.h5p-placeholder', this);
     },
 
-    /**
-     * Deletes elements with class .h5p-placeholder on backspace and delete.
-     *
-     * @method _clearH5P
-     * @param {EventFacade} e
-     * @private
-     */
-    _clearH5P: function(e) {
-        if (e.keyCode === 8 || e.keyCode === 46) {
-            var parentNodes = this.get('host').getSelectedNodes().get('parentNode');
-            if (parentNodes.hasOwnProperty('_nodes')) {
-                var placeholder = parentNodes.filter('.h5p-placeholder');
-                if (!placeholder.isEmpty()) {
-                    placeholder.remove();
-                }
-            }
-        }
-        e.preventDefault();
-    },
-
     /**
      * Handle a double click on a H5P Placeholder.
      *
index 0334499..14eb6c6 100644 (file)
@@ -2208,35 +2208,19 @@ class file_storage {
         $rs->close();
         mtrace('done.');
 
-        // remove orphaned preview files (that is files in the core preview filearea without
-        // the existing original file)
-        mtrace('Deleting orphaned preview files... ', '');
+        // Remove orphaned files:
+        // * preview files in the core preview filearea without the existing original file.
+        // * document converted files in core documentconversion filearea without the existing original file.
+        mtrace('Deleting orphaned preview, and document conversion files... ', '');
         cron_trace_time_and_memory();
         $sql = "SELECT p.*
                   FROM {files} p
              LEFT JOIN {files} o ON (p.filename = o.contenthash)
-                 WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
-                       AND o.id IS NULL";
-        $syscontext = context_system::instance();
-        $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
-        foreach ($rs as $orphan) {
-            $file = $this->get_file_instance($orphan);
-            if (!$file->is_directory()) {
-                $file->delete();
-            }
-        }
-        $rs->close();
-        mtrace('done.');
-
-        // Remove orphaned converted files (that is files in the core documentconversion filearea without
-        // the existing original file).
-        mtrace('Deleting orphaned document conversion files... ', '');
-        cron_trace_time_and_memory();
-        $sql = "SELECT p.*
-                  FROM {files} p
-             LEFT JOIN {files} o ON (p.filename = o.contenthash)
-                 WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'documentconversion' AND p.itemid = 0
-                       AND o.id IS NULL";
+                 WHERE p.contextid = ?
+                   AND p.component = 'core'
+                   AND (p.filearea = 'preview' OR p.filearea = 'documentconversion')
+                   AND p.itemid = 0
+                   AND o.id IS NULL";
         $syscontext = context_system::instance();
         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
         foreach ($rs as $orphan) {
index a68f222..b3f4162 100644 (file)
@@ -709,6 +709,35 @@ function filter_set_global_state($filtername, $state, $move = 0) {
     $transaction->allow_commit();
 }
 
+/**
+ * Returns the active state for a filter in the given context.
+ *
+ * @param string $filtername The filter name, for example 'tex'.
+ * @param integer $contextid The id of the context to get the data for.
+ * @return int value of active field for the given filter.
+ */
+function filter_get_active_state(string $filtername, $contextid = null): int {
+    global $DB;
+
+    if ($contextid === null) {
+        $contextid = context_system::instance()->id;
+    }
+    if (is_object($contextid)) {
+        $contextid = $contextid->id;
+    }
+
+    if (strpos($filtername, 'filter/') === 0) {
+        $filtername = substr($filtername, 7);
+    } else if (strpos($filtername, '/') !== false) {
+        throw new coding_exception("Invalid filter name '$filtername' used in filter_is_enabled()");
+    }
+    if ($active = $DB->get_field('filter_active', 'active', array('filter' => $filtername, 'contextid' => $contextid))) {
+        return $active;
+    }
+
+    return TEXTFILTER_DISABLED;
+}
+
 /**
  * @param string $filtername The filter name, for example 'tex'.
  * @return boolean is this filter allowed to be used on this site. That is, the
index 91ed768..ee04ff0 100644 (file)
@@ -42,15 +42,15 @@ require_once($CFG->libdir . '/form/text.php');
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class MoodleQuickForm_duration extends MoodleQuickForm_group {
-   /**
-    * Control the fieldnames for form elements
-    * optional => if true, show a checkbox beside the element to turn it on (or off)
-    * @var array
-    */
-   protected $_options = array('optional' => false, 'defaultunit' => 60);
+    /**
+     * Control the fieldnames for form elements
+     * optional => if true, show a checkbox beside the element to turn it on (or off)
+     * @var array
+     */
+    protected $_options = array('optional' => false, 'defaultunit' => MINSECS);
 
-   /** @var array associative array of time units (days, hours, minutes, seconds) */
-   private $_units = null;
+    /** @var array associative array of time units (days, hours, minutes, seconds) */
+    private $_units = null;
 
    /**
     * constructor
@@ -58,12 +58,15 @@ class MoodleQuickForm_duration extends MoodleQuickForm_group {
     * @param string $elementName Element's name
     * @param mixed $elementLabel Label(s) for an element
     * @param array $options Options to control the element's display. Recognised values are
-    *              'optional' => true/false - whether to display an 'enabled' checkbox next to the element.
-    *              'defaultunit' => 1|60|3600|86400|604800 - the default unit to display when the time is blank.
-    *              If not specified, minutes is used.
+    *      'optional' => true/false - whether to display an 'enabled' checkbox next to the element.
+    *      'defaultunit' => 1|MINSECS|HOURSECS|DAYSECS|WEEKSECS - the default unit to display when
+    *              the time is blank. If not specified, minutes is used.
+    *      'units' => array containing some or all of 1, MINSECS, HOURSECS, DAYSECS and WEEKSECS
+    *              which unit choices to offer.
     * @param mixed $attributes Either a typical HTML attribute string or an associative array
     */
-    public function __construct($elementName = null, $elementLabel = null, $options = array(), $attributes = null) {
+    public function __construct($elementName = null, $elementLabel = null,
+            $options = array(), $attributes = null) {
         // TODO MDL-52313 Replace with the call to parent::__construct().
         HTML_QuickForm_element::__construct($elementName, $elementLabel, $attributes);
         $this->_persistantFreeze = true;
@@ -82,6 +85,24 @@ class MoodleQuickForm_duration extends MoodleQuickForm_group {
             }
             $this->_options['defaultunit'] = $options['defaultunit'];
         }
+        if (isset($options['units'])) {
+            if (!is_array($options['units'])) {
+                throw new coding_exception(
+                        'When creating a duration form field, units option must be an array.');
+            }
+            // Validate and register requested units.
+            $availableunits = $this->get_units();
+            $displayunits = [];
+            foreach ($options['units'] as $requestedunit) {
+                if (!isset($availableunits[$requestedunit])) {
+                    throw new coding_exception($requestedunit .
+                            ' is not a recognised unit in MoodleQuickForm_duration.');
+                }
+                $displayunits[$requestedunit] = $availableunits[$requestedunit];
+            }
+            krsort($displayunits, SORT_NUMERIC);
+            $this->_options['units'] = $displayunits;
+        }
     }
 
     /**
@@ -89,7 +110,8 @@ class MoodleQuickForm_duration extends MoodleQuickForm_group {
      *
      * @deprecated since Moodle 3.1
      */
-    public function MoodleQuickForm_duration($elementName = null, $elementLabel = null, $options = array(), $attributes = null) {
+    public function MoodleQuickForm_duration($elementName = null, $elementLabel = null,
+            $options = array(), $attributes = null) {
         debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
         self::__construct($elementName, $elementLabel, $options, $attributes);
     }
@@ -102,19 +124,34 @@ class MoodleQuickForm_duration extends MoodleQuickForm_group {
     public function get_units() {
         if (is_null($this->_units)) {
             $this->_units = array(
-                604800 => get_string('weeks'),
-                86400 => get_string('days'),
-                3600 => get_string('hours'),
-                60 => get_string('minutes'),
+                WEEKSECS => get_string('weeks'),
+                DAYSECS => get_string('days'),
+                HOURSECS => get_string('hours'),
+                MINSECS => get_string('minutes'),
                 1 => get_string('seconds'),
             );
         }
         return $this->_units;
     }
 
+    /**
+     * Get the units to be used for this field.
+     *
+     * The ones specified in the options passed to the constructor, or all by default.
+     *
+     * @return array number of seconds => lang string.
+     */
+    protected function get_units_used() {
+        if (!empty($this->_options['units'])) {
+            return $this->_options['units'];
+        } else {
+            return $this->get_units();
+        }
+    }
+
     /**
      * Converts seconds to the best possible time unit. for example
-     * 1800 -> array(30, 60) = 30 minutes.
+     * 1800 -> [30, MINSECS] = 30 minutes.
      *
      * @param int $seconds an amout of time in seconds.
      * @return array associative array ($number => $unit)
@@ -123,7 +160,7 @@ class MoodleQuickForm_duration extends MoodleQuickForm_group {
         if ($seconds == 0) {
             return array(0, $this->_options['defaultunit']);
         }
-        foreach ($this->get_units() as $unit => $notused) {
+        foreach ($this->get_units_used() as $unit => $notused) {
             if (fmod($seconds, $unit) == 0) {
                 return array($seconds / $unit, $unit);
             }
@@ -144,14 +181,17 @@ class MoodleQuickForm_duration extends MoodleQuickForm_group {
         }
         $this->_elements = array();
         // E_STRICT creating elements without forms is nasty because it internally uses $this
-        $number = $this->createFormElement('text', 'number', get_string('time', 'form'), $attributes, true);
+        $number = $this->createFormElement('text', 'number',
+                get_string('time', 'form'), $attributes, true);
         $number->set_force_ltr(true);
         $this->_elements[] = $number;
         unset($attributes['size']);
-        $this->_elements[] = $this->createFormElement('select', 'timeunit', get_string('timeunit', 'form'), $this->get_units(), $attributes, true);
+        $this->_elements[] = $this->createFormElement('select', 'timeunit',
+                get_string('timeunit', 'form'), $this->get_units_used(), $attributes, true);
         // If optional we add a checkbox which the user can use to turn if on
         if($this->_options['optional']) {
-            $this->_elements[] = $this->createFormElement('checkbox', 'enabled', null, get_string('enable'), $this->getAttributes(), true);
+            $this->_elements[] = $this->createFormElement('checkbox', 'enabled', null,
+                    get_string('enable'), $this->getAttributes(), true);
         }
         foreach ($this->_elements as $element){
             if (method_exists($element, 'setHiddenLabel')){
@@ -165,7 +205,7 @@ class MoodleQuickForm_duration extends MoodleQuickForm_group {
      *
      * @param string $event Name of event
      * @param mixed $arg event arguments
-     * @param object $caller calling object
+     * @param MoodleQuickForm $caller calling object
      * @return bool
      */
     function onQuickFormEvent($event, $arg, &$caller) {
index 7852905..e83e494 100644 (file)
@@ -1,5 +1,10 @@
 <div class="form-group row {{#error}}has-danger{{/error}} fitem {{#advanced}}advanced{{/advanced}} {{{element.extraclasses}}}">
     <div class="col-md-3">
+        <span class="float-sm-right text-nowrap">
+            {{#required}}<abbr class="initialism text-danger" title="{{#str}}required{{/str}}">{{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}</abbr>{{/required}}
+            {{#advanced}}<abbr class="initialism text-info" title="{{#str}}advanced{{/str}}">!</abbr>{{/advanced}}
+            {{{helpbutton}}}
+        </span>
         {{#text}}
             <label for="{{element.id}}">
                 {{{label}}}
index af5bba5..4ce9280 100644 (file)
@@ -41,109 +41,122 @@ require_once($CFG->libdir . '/form/duration.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class core_form_duration_testcase extends basic_testcase {
-    /** @var MoodleQuickForm Keeps reference of dummy form object */
-    private $mform;
-    /** @var MoodleQuickForm_duration Keeps reference of MoodleQuickForm_duration object */
-    private $element;
 
     /**
-     * Initalize test wide variable, it is called in start of the testcase
+     * Get a form that can be used for testing.
+     *
+     * @return MoodleQuickForm
      */
-    protected function setUp() {
-        parent::setUp();
-
-        // Get form data.
+    protected function get_test_form() {
         $form = new temp_form_duration();
-        $this->mform = $form->getform();
-        $this->element = $this->mform->addElement('duration', 'duration');
+        return $form->getform();
     }
 
     /**
-     * Clears the data set in the setUp() method call.
-     * @see duration_form_element_test::setUp()
+     * Get a form with a duration element that can be used for testing.
+     *
+     * @return array with two elements, a MoodleQuickForm and a MoodleQuickForm_duration.
      */
-    protected function tearDown() {
-        $this->element = null;
+    protected function get_test_form_and_element() {
+        $mform = $this->get_test_form();
+        $element = $mform->addElement('duration', 'duration');
+        return [$mform, $element];
     }
 
     /**
      * Testcase for testing contructor.
+     *
      * @expectedException coding_exception
-     * @retrun void
      */
     public function test_constructor() {
         // Test trying to create with an invalid unit.
-        $this->element = $this->mform->addElement('duration', 'testel', null, array('defaultunit' => 123, 'optional' => false));
+        $mform = $this->get_test_form();
+        $mform->addElement('duration', 'testel', null, ['defaultunit' => 123, 'optional' => false]);
+    }
+
+    /**
+     * Test contructor only some units.
+     */
+    public function test_constructor_limited_units() {
+        $mform = $this->get_test_form();
+        $mform->addElement('duration', 'testel', null, ['units' => [MINSECS, 1], 'optional' => false]);
+        $html = $mform->toHtml();
+        $html = preg_replace('~ +>~', '>', $html); // Clean HTML to avoid spurious errors.
+        $this->assertContains('<option value="60" selected>minutes</option>', $html);
+        $this->assertContains('<option value="1">seconds</option>', $html);
+        $this->assertNotContains('value="3600"', $html);
     }
 
     /**
      * Testcase for testing units (seconds, minutes, hours and days)
      */
     public function test_get_units() {
-        $units = $this->element->get_units();
-        ksort($units);
-        $this->assertEquals($units, array(1 => get_string('seconds'), 60 => get_string('minutes'),
-            3600 => get_string('hours'), 86400 => get_string('days'), 604800 => get_string('weeks')));
+        [$mform, $element] = $this->get_test_form_and_element();
+        $units = $element->get_units();
+        $this->assertEquals($units, [1 => get_string('seconds'), 60 => get_string('minutes'),
+                3600 => get_string('hours'), 86400 => get_string('days'), 604800 => get_string('weeks')]);
     }
 
     /**
      * Testcase for testing conversion of seconds to the best possible unit
      */
     public function test_seconds_to_unit() {
-        $this->assertEquals(array(0, 60), $this->element->seconds_to_unit(0)); // Zero minutes, for a nice default unit.
-        $this->assertEquals(array(1, 1), $this->element->seconds_to_unit(1));
-        $this->assertEquals(array(3601, 1), $this->element->seconds_to_unit(3601));
-        $this->assertEquals(array(1, 60), $this->element->seconds_to_unit(60));
-        $this->assertEquals(array(3, 60), $this->element->seconds_to_unit(180));
-        $this->assertEquals(array(1, 3600), $this->element->seconds_to_unit(3600));
-        $this->assertEquals(array(2, 3600), $this->element->seconds_to_unit(7200));
-        $this->assertEquals(array(1, 86400), $this->element->seconds_to_unit(86400));
-        $this->assertEquals(array(25, 3600), $this->element->seconds_to_unit(90000));
-
-        $this->element = $this->mform->addElement('duration', 'testel', null, array('defaultunit' => 86400, 'optional' => false));
-        $this->assertEquals(array(0, 86400), $this->element->seconds_to_unit(0)); // Zero minutes, for a nice default unit.
+        [$mform, $element] = $this->get_test_form_and_element();
+        $this->assertEquals([0, MINSECS], $element->seconds_to_unit(0)); // Zero minutes, for a nice default unit.
+        $this->assertEquals([1, 1], $element->seconds_to_unit(1));
+        $this->assertEquals([3601, 1], $element->seconds_to_unit(3601));
+        $this->assertEquals([1, MINSECS], $element->seconds_to_unit(60));
+        $this->assertEquals([3, MINSECS], $element->seconds_to_unit(180));
+        $this->assertEquals([1, HOURSECS], $element->seconds_to_unit(3600));
+        $this->assertEquals([2, HOURSECS], $element->seconds_to_unit(7200));
+        $this->assertEquals([1, DAYSECS], $element->seconds_to_unit(86400));
+        $this->assertEquals([25, HOURSECS], $element->seconds_to_unit(90000));
+
+        $element = $mform->addElement('duration', 'testel', null,
+                ['defaultunit' => DAYSECS, 'optional' => false]);
+        $this->assertEquals([0, DAYSECS], $element->seconds_to_unit(0)); // Zero minutes, for a nice default unit.
     }
 
     /**
      * Testcase to check generated timestamp
      */
     public function test_exportValue() {
-        /** @var MoodleQuickForm_duration $el */
-        $el = $this->mform->addElement('duration', 'testel');
-        $values = array('testel' => array('number' => 10, 'timeunit' => 1));
-        $this->assertEquals(array('testel' => 10), $el->exportValue($values, true));
+        $mform = $this->get_test_form();
+        $el = $mform->addElement('duration', 'testel');
+        $values = ['testel' => ['number' => 10, 'timeunit' => 1]];
+        $this->assertEquals(['testel' => 10], $el->exportValue($values, true));
         $this->assertEquals(10, $el->exportValue($values));
-        $values = array('testel' => array('number' => 3, 'timeunit' => 60));
-        $this->assertEquals(array('testel' => 180), $el->exportValue($values, true));
+        $values = ['testel' => ['number' => 3, 'timeunit' => MINSECS]];
+        $this->assertEquals(['testel' => 180], $el->exportValue($values, true));
         $this->assertEquals(180, $el->exportValue($values));
-        $values = array('testel' => array('number' => 1.5, 'timeunit' => 60));
-        $this->assertEquals(array('testel' => 90), $el->exportValue($values, true));
+        $values = ['testel' => ['number' => 1.5, 'timeunit' => MINSECS]];
+        $this->assertEquals(['testel' => 90], $el->exportValue($values, true));
         $this->assertEquals(90, $el->exportValue($values));
-        $values = array('testel' => array('number' => 2, 'timeunit' => 3600));
-        $this->assertEquals(array('testel' => 7200), $el->exportValue($values, true));
+        $values = ['testel' => ['number' => 2, 'timeunit' => HOURSECS]];
+        $this->assertEquals(['testel' => 7200], $el->exportValue($values, true));
         $this->assertEquals(7200, $el->exportValue($values));
-        $values = array('testel' => array('number' => 1, 'timeunit' => 86400));
-        $this->assertEquals(array('testel' => 86400), $el->exportValue($values, true));
+        $values = ['testel' => ['number' => 1, 'timeunit' => DAYSECS]];
+        $this->assertEquals(['testel' => 86400], $el->exportValue($values, true));
         $this->assertEquals(86400, $el->exportValue($values));
-        $values = array('testel' => array('number' => 0, 'timeunit' => 3600));
-        $this->assertEquals(array('testel' => 0), $el->exportValue($values, true));
+        $values = ['testel' => ['number' => 0, 'timeunit' => HOURSECS]];
+        $this->assertEquals(['testel' => 0], $el->exportValue($values, true));
         $this->assertEquals(0, $el->exportValue($values));
 
-        $el = $this->mform->addElement('duration', 'testel', null, array('optional' => true));
-        $values = array('testel' => array('number' => 10, 'timeunit' => 1));
-        $this->assertEquals(array('testel' => 0), $el->exportValue($values, true));
+        $el = $mform->addElement('duration', 'testel', null, ['optional' => true]);
+        $values = ['testel' => ['number' => 10, 'timeunit' => 1]];
+        $this->assertEquals(['testel' => 0], $el->exportValue($values, true));
         $this->assertEquals(0, $el->exportValue($values));
-        $values = array('testel' => array('number' => 20, 'timeunit' => 1, 'enabled' => 1));
-        $this->assertEquals(array('testel' => 20), $el->exportValue($values, true));
+        $values = ['testel' => ['number' => 20, 'timeunit' => 1, 'enabled' => 1]];
+        $this->assertEquals(['testel' => 20], $el->exportValue($values, true));
         $this->assertEquals(20, $el->exportValue($values));
 
         // Optional element.
-        $el2 = $this->mform->addElement('duration', 'testel', '', ['optional' => true]);
-        $values = array('testel' => array('number' => 10, 'timeunit' => 1, 'enabled' => 1));
-        $this->assertEquals(array('testel' => 10), $el2->exportValue($values, true));
+        $el2 = $mform->addElement('duration', 'testel', '', ['optional' => true]);
+        $values = ['testel' => ['number' => 10, 'timeunit' => 1, 'enabled' => 1]];
+        $this->assertEquals(['testel' => 10], $el2->exportValue($values, true));
         $this->assertEquals(10, $el2->exportValue($values));
-        $values = array('testel' => array('number' => 10, 'timeunit' => 1, 'enabled' => 0));
-        $this->assertEquals(array('testel' => 0), $el2->exportValue($values, true));
+        $values = ['testel' => ['number' => 10, 'timeunit' => 1, 'enabled' => 0]];
+        $this->assertEquals(['testel' => 0], $el2->exportValue($values, true));
         $this->assertEquals(null, $el2->exportValue($values));
     }
 }
@@ -158,6 +171,7 @@ class temp_form_duration extends moodleform {
     public function definition() {
         // No definition required.
     }
+
     /**
      * Returns form reference
      * @return MoodleQuickForm
index 4c6d4b9..8f86592 100644 (file)
@@ -4248,6 +4248,9 @@ function delete_user(stdClass $user) {
     // This might be slow but it is really needed - modules might do some extra cleanup!
     role_unassign_all(array('userid' => $user->id));
 
+    // Notify the competency subsystem.
+    \core_competency\api::hook_user_deleted($user->id);
+
     // Now do a brute force cleanup.
 
     // Delete all user events and subscription events.
index 6625044..0abc9f0 100644 (file)
@@ -1,10 +1,15 @@
-Description of PhpSpreadsheet 1.7.0 import into Moodle
+Description of PhpSpreadsheet 1.10.1 import into Moodle
 
 Last release package can be found in https://github.com/PHPOffice/PhpSpreadsheet/releases
 
+NOTICE:
+ * Before running composer command, make sure you have the composer version updated.
+ * Composer version 1.9.1 2019-11-01 17:20:17
+
 STEPS:
  * Create a temporary folder outside your moodle installation
  * Execute `composer require phpoffice/phpspreadsheet`
+ * Remove the old 'vendor' directory in lib/phpspreadsheet/
  * Copy contents of 'vendor' directory
  * Update lib/thirdpartylibs.xml
  * Apply the modifications described in the CHANGES section
@@ -18,7 +23,6 @@ CHANGES:
    - vendor/phpoffice/phpspreadsheet/samples
 
 * Remove the hidden folders and files in vendor/phpoffice/phpspreadsheet/:
-  - .github/*
   - .gitattributes
   - .gitignore
   - .php_cs.dist
@@ -26,14 +30,14 @@ CHANGES:
   - .scrutinizer.yml
   - .travis.yml
 
- * Add the Moodle hack in vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File/sys_get_temp_dir() because we
- can not guarantee sys_get_temp_dir() works everywhere:
-
-     // Moodle hack!
+ * Add the next Moodle hack at the beginning of the function sysGetTempDir()
+located in vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php
+    // Moodle hack!
      if (function_exists('make_temp_directory')) {
          $temp = make_temp_directory('phpspreadsheet');
          return realpath(dirname($temp));
      }
+  We need this hack because we can not guarantee sysGetTempDir() works everywhere.
 
  * Shared/OLE has been removed because OLE is not DFSG compliant and is not being used in core code.
    Remove the files/folders:
@@ -49,6 +53,6 @@ CHANGES:
    - PhpSpreadsheet/Writer/Xls.php
    - PhpSpreadsheet/Writer/Xls/*
 
-
-COMMENTS:
- * lib/excellib.class.php has been updated so that only xslx spreadsheets will be produced.
+* Remove the next files in vendor/markbaker/ related to external testing that we don't need matrix/:
+  - infection.json.dist (PHP mutation testing framework configuration file)
+  - phpstan.neon (PHP static analyzer configuration file)
index f3b1a2a..7e5f0d5 100644 (file)
@@ -4,4 +4,4 @@
 
 require_once __DIR__ . '/composer/autoload_real.php';
 
-return ComposerAutoloaderInitd2fe1bff8f5edf0dba7c9d9b0345247c::getLoader();
+return ComposerAutoloaderInit4f38f1469083ef94e54b2e3d87999c8f::getLoader();
index dc02dfb..fce8549 100644 (file)
@@ -279,7 +279,7 @@ class ClassLoader
      */
     public function setApcuPrefix($apcuPrefix)
     {
-        $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
     }
 
     /**
@@ -377,7 +377,7 @@ class ClassLoader
             $subPath = $class;
             while (false !== $lastPos = strrpos($subPath, '\\')) {
                 $subPath = substr($subPath, 0, $lastPos);
-                $search = $subPath.'\\';
+                $search = $subPath . '\\';
                 if (isset($this->prefixDirsPsr4[$search])) {
                     $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                     foreach ($this->prefixDirsPsr4[$search] as $dir) {
index f0157a6..f27399a 100644 (file)
@@ -1,56 +1,21 @@
-Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
-Upstream-Name: Composer
-Upstream-Contact: Jordi Boggiano <j.boggiano@seld.be>
-Source: https://github.com/composer/composer
 
-Files: *
-Copyright: 2016, Nils Adermann <naderman@naderman.de>
-           2016, Jordi Boggiano <j.boggiano@seld.be>
-License: Expat
+Copyright (c) Nils Adermann, Jordi Boggiano
 
-Files: src/Composer/Util/TlsHelper.php
-Copyright: 2016, Nils Adermann <naderman@naderman.de>
-           2016, Jordi Boggiano <j.boggiano@seld.be>
-           2013, Evan Coury <me@evancoury.com>
-License: Expat and BSD-2-Clause
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
 
-License: BSD-2-Clause
- Redistribution and use in source and binary forms, with or without modification,
- are permitted provided that the following conditions are met:
- .
-     * Redistributions of source code must retain the above copyright notice,
-       this list of conditions and the following disclaimer.
- .
-     * Redistributions in binary form must reproduce the above copyright notice,
-       this list of conditions and the following disclaimer in the documentation
-       and/or other materials provided with the distribution.
- .
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
 
-License: Expat
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is furnished
- to do so, subject to the following conditions:
- .
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- .
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.