Merge branch 'MDL-67953-master' of git://github.com/andrewnicols/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 18 Feb 2020 16:09:17 +0000 (17:09 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 18 Feb 2020 16:09:17 +0000 (17:09 +0100)
283 files changed:
Gruntfile.js
GruntfileComponents.js
admin/settings/h5p.php
admin/settings/security.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
install/lang/om/langconfig.php [new file with mode: 0644]
lang/en/admin.php
lang/en/course.php
lang/en/deprecated.txt
lang/en/h5p.php
lang/en/moodle.php
lang/en/timezones.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/behat/classes/behat_context_helper.php
lib/classes/date.php
lib/classes/event/user_password_policy_failed.php [new file with mode: 0644]
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/authlib_test.php
lib/tests/behat/behat_hooks.php
lib/tests/date_test.php
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/lang/en/forum.php
mod/forum/templates/social_discussion_list.mustache
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 5bc57fb..bd39185 100644 (file)
@@ -167,24 +167,39 @@ 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;
     const fullRunDir = fs.realpathSync(gruntFilePath + path.sep + runDir);
-    grunt.log.debug(`The cwd was detected as ${cwd} with a fullRunDir of ${fullRunDir}`);
+    grunt.log.debug('============================================================================');
+    grunt.log.debug(`= Node version:        ${process.versions.node}`);
+    grunt.log.debug(`= grunt version:       ${grunt.package.version}`);
+    grunt.log.debug(`= process.cwd:         '` + process.cwd() + `'`);
+    grunt.log.debug(`= process.env.PWD:     '${process.env.PWD}'`);
+    grunt.log.debug(`= path.sep             '${path.sep}'`);
+    grunt.log.debug('============================================================================');
+    grunt.log.debug(`= gruntFilePath:       '${gruntFilePath}'`);
+    grunt.log.debug(`= relativeCwd:         '${relativeCwd}'`);
+    grunt.log.debug(`= componentDirectory:  '${componentDirectory}'`);
+    grunt.log.debug(`= inComponent:         '${inComponent}'`);
+    grunt.log.debug(`= runDir:              '${runDir}'`);
+    grunt.log.debug(`= fullRunDir:          '${fullRunDir}'`);
+    grunt.log.debug('============================================================================');
 
     if (inComponent) {
         grunt.log.ok(`Running tasks for component directory ${componentDirectory}`);
     }
 
-    var files = null;
+    let files = null;
     if (grunt.option('files')) {
         // Accept a comma separated list of files to process.
         files = grunt.option('files').split(',');
     }
 
-    const inAMD = path.basename(cwd) == 'amd';
+    // If the cwd is the amd directory in the current component then it will be empty.
+    // If the cwd is a child of the component's AMD directory, the relative directory will not start with ..
+    const inAMD = !path.relative(`${componentDirectory}/amd`, cwd).startsWith('..');
 
     // Globbing pattern for matching all AMD JS source files.
     let amdSrc = [];
@@ -235,7 +250,7 @@ module.exports = function(grunt) {
             const nodes = xpath.select("/libraries/library/location/text()", doc);
 
             nodes.forEach(function(node) {
-                let lib = path.join(dirname, node.toString());
+                let lib = path.posix.join(dirname, node.toString());
                 if (grunt.file.isDir(lib)) {
                     // Ensure trailing slash on dirs.
                     lib = lib.replace(/\/?$/, '/');
index 06ed999..74bd9de 100644 (file)
@@ -131,9 +131,11 @@ const getYuiSrcGlobList = relativeTo => {
  */
 const getThirdPartyLibsList = relativeTo => {
     const fs = require('fs');
+    const path = require('path');
 
     return fetchComponentData().pathList
-        .map(componentPath => componentPath.replace(relativeTo, '') + '/thirdpartylibs.xml')
+        .map(componentPath => path.relative(relativeTo, componentPath) + '/thirdpartylibs.xml')
+        .map(componentPath => componentPath.replace(/\\/g, '/'))
         .filter(path => fs.existsSync(path))
         .sort();
 };
@@ -157,18 +159,19 @@ const getComponentFromPath = path => {
 /**
  * Check whether the supplied path, relative to the Gruntfile.js, is in a known component.
  *
- * @param {String} checkPath The path to check
+ * @param {String} checkPath The path to check. This can be with either Windows, or Linux directory separators.
  * @returns {String|null}
  */
 const getOwningComponentDirectory = checkPath => {
     const path = require('path');
 
-    const pathList = fetchComponentData().components;
-    for (const componentPath of Object.keys(pathList)) {
-        if (checkPath === componentPath) {
-            return componentPath;
-        }
-        if (checkPath.startsWith(componentPath + path.sep)) {
+    // Fetch all components into a reverse sorted array.
+    // This ensures that components which are within the directory of another component match first.
+    const pathList = Object.keys(fetchComponentData().components).sort().reverse();
+    for (const componentPath of pathList) {
+        // If the componentPath is the directory being checked, it will be empty.
+        // If the componentPath is a parent of the directory being checked, the relative directory will not start with ..
+        if (!path.relative(componentPath, checkPath).startsWith('..')) {
             return componentPath;
         }
     }
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 1b91d6a..d8dc48a 100644 (file)
@@ -100,6 +100,9 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configtext('minpasswordupper', new lang_string('minpasswordupper', 'admin'), new lang_string('configminpasswordupper', 'admin'), 1, PARAM_INT));
     $temp->add(new admin_setting_configtext('minpasswordnonalphanum', new lang_string('minpasswordnonalphanum', 'admin'), new lang_string('configminpasswordnonalphanum', 'admin'), 1, PARAM_INT));
     $temp->add(new admin_setting_configtext('maxconsecutiveidentchars', new lang_string('maxconsecutiveidentchars', 'admin'), new lang_string('configmaxconsecutiveidentchars', 'admin'), 0, PARAM_INT));
+    $temp->add(new admin_setting_configcheckbox('passwordpolicycheckonlogin',
+        new lang_string('passwordpolicycheckonlogin', 'admin'),
+        new lang_string('configpasswordpolicycheckonlogin', 'admin'), 0));
 
     $temp->add(new admin_setting_configtext('passwordreuselimit',
         new lang_string('passwordreuselimit', 'admin'),
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';
diff --git a/install/lang/om/langconfig.php b/install/lang/om/langconfig.php
new file mode 100644 (file)
index 0000000..58f2c8d
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Afaan Oromoo';
index a29697c..f4d3e7d 100644 (file)
@@ -307,7 +307,9 @@ $string['confignotifyloginthreshold'] = 'If notifications about failed logins ar
 $string['confignotloggedinroleid'] = 'Users who are not logged in to the site will be treated as if they have this role granted to them at the site context.  Guest is almost always what you want here, but you might want to create roles that are less or more restrictive.  Things like creating posts still require the user to log in properly.';
 $string['configopentowebcrawlers'] = 'If you enable this setting, then search engines will be allowed to enter your site as a guest.  In addition, people coming in to your site via a search engine will automatically be logged in as a guest.  Note that this only provides transparent access to courses that already allow guest access.';
 $string['configoverride'] = 'Defined in config.php';
-$string['configpasswordpolicy'] = 'If enabled, user passwords will be checked against the password policy as specified in the settings below. Enabling the password policy will not affect existing users until they decide to, or are required to, change their password.';
+$string['configpasswordpolicy'] = 'If enabled, user passwords will be checked against the password policy as specified in the settings below. Enabling the password policy will not affect existing users until they decide to, or are required to, change their password, or the \'Check password on login\' setting is enabled.';
+$string['configpasswordpolicycheckonlogin'] = 'If enabled, user passwords will be checked against the password policy each time users log in. If the check fails, the user will be required to change their password before proceeding.
+It is useful to enable this setting after updating the password policy.';
 $string['configpasswordresettime'] = 'This specifies the amount of time people have to validate a password reset request before it expires. Usually 30 minutes is a good value.';
 $string['configpathtodu'] = 'Path to du. Probably something like /usr/bin/du. If you enter this, pages that display directory contents will run much faster for directories with a lot of files.';
 $string['configpathtophp'] = 'Path to PHP CLI. Probably something like /usr/bin/php. If you enter this, cron scripts can be executed from admin web interface.';
@@ -480,6 +482,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';
@@ -894,6 +897,7 @@ $string['passwordchangelogout_desc'] = 'If enabled, when a password is changed,
 $string['passwordchangetokendeletion'] = 'Remove web service access tokens after password change';
 $string['passwordchangetokendeletion_desc'] = 'If enabled, when a password is changed, all the user web service access tokens are deleted.';
 $string['passwordpolicy'] = 'Password policy';
+$string['passwordpolicycheckonlogin'] = 'Check password on login';
 $string['passwordresettime'] = 'Maximum time to validate password reset request';
 $string['passwordreuselimit'] = 'Password rotation limit';
 $string['passwordreuselimit_desc'] = 'Number of times a user must change their password before they are allowed to reuse a password. Hashes of previously used passwords are stored in local database table. This feature might not be compatible with some external authentication plugins.';
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..4bd15ab 100644 (file)
@@ -120,3 +120,22 @@ global,core_calendar
 globalevent,core_calendar
 globalevents,core_calendar
 eventtypeglobal,core_calendar
+documentation,core_webservice
+africa/asmera,core_timezones
+africa/timbuktu,core_timezones
+america/argentina/comodrivadavia,core_timezones
+america/indianapolis,core_timezones
+america/louisville,core_timezones
+america/montreal,core_timezones
+asia/calcutta,core_timezones
+asia/chongqing,core_timezones
+asia/harbin,core_timezones
+asia/kashgar,core_timezones
+asia/katmandu,core_timezones
+asia/rangoon,core_timezones
+asia/saigon,core_timezones
+atlantic/faeroe,core_timezones
+europe/belfast,core_timezones
+pacific/ponape,core_timezones
+pacific/truk,core_timezones
+pacific/yap,core_timezones
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..22682b5 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';
@@ -797,6 +799,7 @@ $string['eventusercreated'] = 'User created';
 $string['eventuserdeleted'] = 'User deleted';
 $string['eventuserlistviewed'] = 'User list viewed';
 $string['eventuserloggedout'] = 'User logged out';
+$string['eventuserpasswordpolicyfailed'] = 'User password failed password policy';
 $string['eventuserpasswordupdated'] = 'User password updated';
 $string['eventuserprofileviewed'] = 'User profile viewed';
 $string['eventuserupdated'] = 'User updated';
@@ -857,6 +860,10 @@ $string['forcepasswordchange_help'] = 'If this checkbox is ticked, the user will
 $string['forcepasswordchangecheckfull'] = 'Are you absolutely sure you want to force a password change to {$a} ?';
 $string['forcepasswordchangenot'] = 'Could not force a password change to {$a}';
 $string['forcepasswordchangenotice'] = 'You must change your password to proceed.';
+$string['forcepasswordresetfailurenotice'] = 'Your current password no longer passes the set password policy. Please contact your Moodle administrator for assistance.
+   {$a}';
+$string['forcepasswordresetnotice'] = 'Your current password no longer passes the set password policy, you must reset your password to login.
+   {$a}';
 $string['forcetheme'] = 'Force theme';
 $string['forgotaccount'] = 'Lost password?';
 $string['forgotten'] = 'Forgotten your username or password?';
@@ -1515,6 +1522,8 @@ $string['passwordforgotteninstructions'] = 'Your details must first be found in
 $string['passwordforgotteninstructions2'] = 'To reset your password, submit your username or your email address below. If we can find you in the database, an email will be sent to your email address, with instructions how to get access again.';
 $string['passwordchanged'] = 'Password has been changed';
 $string['passwordnohelp'] = 'No help is available to find your lost password. Please contact your Moodle administrator.';
+$string['passwordpolicynomatch'] = 'Your current password no longer matches the set password policy.
+   {$a}';
 $string['passwordrecovery'] = 'Yes, help me log in';
 $string['passwordsdiffer'] = 'These passwords do not match';
 $string['passwordsent'] = 'Password has been sent';
index 01a1fea..417a4fb 100644 (file)
@@ -27,7 +27,7 @@ $string['africa/abidjan'] = 'Africa/Abidjan';
 $string['africa/accra'] = 'Africa/Accra';
 $string['africa/addis_ababa'] = 'Africa/Addis_Ababa';
 $string['africa/algiers'] = 'Africa/Algiers';
-$string['africa/asmera'] = 'Africa/Asmera';
+$string['africa/asmara'] = 'Africa/Asmara';
 $string['africa/bamako'] = 'Africa/Bamako';
 $string['africa/bangui'] = 'Africa/Bangui';
 $string['africa/banjul'] = 'Africa/Banjul';
@@ -48,6 +48,7 @@ $string['africa/freetown'] = 'Africa/Freetown';
 $string['africa/gaborone'] = 'Africa/Gaborone';
 $string['africa/harare'] = 'Africa/Harare';
 $string['africa/johannesburg'] = 'Africa/Johannesburg';
+$string['africa/juba'] = 'Africa/Juba';
 $string['africa/kampala'] = 'Africa/Kampala';
 $string['africa/khartoum'] = 'Africa/Khartoum';
 $string['africa/kigali'] = 'Africa/Kigali';
@@ -71,32 +72,35 @@ $string['africa/nouakchott'] = 'Africa/Nouakchott';
 $string['africa/ouagadougou'] = 'Africa/Ouagadougou';
 $string['africa/porto-novo'] = 'Africa/Porto-Novo';
 $string['africa/sao_tome'] = 'Africa/Sao_Tome';
-$string['africa/timbuktu'] = 'Africa/Timbuktu';
 $string['africa/tripoli'] = 'Africa/Tripoli';
 $string['africa/tunis'] = 'Africa/Tunis';
 $string['africa/windhoek'] = 'Africa/Windhoek';
 $string['america/adak'] = 'America/Adak';
-$string['america/anguilla'] = 'America/Anguilla';
 $string['america/anchorage'] = 'America/Anchorage';
+$string['america/anguilla'] = 'America/Anguilla';
 $string['america/antigua'] = 'America/Antigua';
 $string['america/araguaina'] = 'America/Araguaina';
 $string['america/argentina/buenos_aires'] = 'America/Argentina/Buenos_Aires';
 $string['america/argentina/catamarca'] = 'America/Argentina/Catamarca';
-$string['america/argentina/comodrivadavia'] = 'America/Argentina/ComodRivadavia';
 $string['america/argentina/cordoba'] = 'America/Argentina/Cordoba';
 $string['america/argentina/jujuy'] = 'America/Argentina/Jujuy';
 $string['america/argentina/la_rioja'] = 'America/Argentina/La_Rioja';
 $string['america/argentina/mendoza'] = 'America/Argentina/Mendoza';
 $string['america/argentina/rio_gallegos'] = 'America/Argentina/Rio_Gallegos';
+$string['america/argentina/salta'] = 'America/Argentina/Salta';
 $string['america/argentina/san_juan'] = 'America/Argentina/San_Juan';
+$string['america/argentina/san_luis'] = 'America/Argentina/San_Luis';
 $string['america/argentina/tucuman'] = 'America/Argentina/Tucuman';
 $string['america/argentina/ushuaia'] = 'America/Argentina/Ushuaia';
 $string['america/aruba'] = 'America/Aruba';
 $string['america/asuncion'] = 'America/Asuncion';
+$string['america/atikokan'] = 'America/Atikokan';
 $string['america/bahia'] = 'America/Bahia';
+$string['america/bahia_banderas'] = 'America/Bahia_Banderas';
 $string['america/barbados'] = 'America/Barbados';
 $string['america/belem'] = 'America/Belem';
 $string['america/belize'] = 'America/Belize';
+$string['america/blanc-sablon'] = 'America/Blanc-Sablon';
 $string['america/boa_vista'] = 'America/Boa_Vista';
 $string['america/bogota'] = 'America/Bogota';
 $string['america/boise'] = 'America/Boise';
@@ -106,7 +110,10 @@ $string['america/cancun'] = 'America/Cancun';
 $string['america/caracas'] = 'America/Caracas';
 $string['america/cayenne'] = 'America/Cayenne';
 $string['america/cayman'] = 'America/Cayman';
+$string['america/chicago'] = 'America/Chicago';
+$string['america/chihuahua'] = 'America/Chihuahua';
 $string['america/costa_rica'] = 'America/Costa_Rica';
+$string['america/creston'] = 'America/Creston';
 $string['america/cuiaba'] = 'America/Cuiaba';
 $string['america/curacao'] = 'America/Curacao';
 $string['america/danmarkshavn'] = 'America/Danmarkshavn';
@@ -118,6 +125,7 @@ $string['america/dominica'] = 'America/Dominica';
 $string['america/edmonton'] = 'America/Edmonton';
 $string['america/eirunepe'] = 'America/Eirunepe';
 $string['america/el_salvador'] = 'America/El_Salvador';
+$string['america/fort_nelson'] = 'America/Fort_Nelson';
 $string['america/fortaleza'] = 'America/Fortaleza';
 $string['america/glace_bay'] = 'America/Glace_Bay';
 $string['america/godthab'] = 'America/Godthab';
@@ -131,40 +139,50 @@ $string['america/guyana'] = 'America/Guyana';
 $string['america/halifax'] = 'America/Halifax';
 $string['america/havana'] = 'America/Havana';
 $string['america/hermosillo'] = 'America/Hermosillo';
-$string['america/chicago'] = 'America/Chicago';
-$string['america/chihuahua'] = 'America/Chihuahua';
+$string['america/indiana/indianapolis'] = 'America/Indiana/Indianapolis';
 $string['america/indiana/knox'] = 'America/Indiana/Knox';
 $string['america/indiana/marengo'] = 'America/Indiana/Marengo';
-$string['america/indianapolis'] = 'America/Indianapolis';
+$string['america/indiana/petersburg'] = 'America/Indiana/Petersburg';
+$string['america/indiana/tell_city'] = 'America/Indiana/Tell_City';
 $string['america/indiana/vevay'] = 'America/Indiana/Vevay';
+$string['america/indiana/vincennes'] = 'America/Indiana/Vincennes';
+$string['america/indiana/winamac'] = 'America/Indiana/Winamac';
 $string['america/inuvik'] = 'America/Inuvik';
 $string['america/iqaluit'] = 'America/Iqaluit';
 $string['america/jamaica'] = 'America/Jamaica';
 $string['america/juneau'] = 'America/Juneau';
+$string['america/kentucky/louisville'] = 'America/Kentucky/Louisville';
 $string['america/kentucky/monticello'] = 'America/Kentucky/Monticello';
+$string['america/kralendijk'] = 'America/Kralendijk';
 $string['america/la_paz'] = 'America/La_Paz';
 $string['america/lima'] = 'America/Lima';
 $string['america/los_angeles'] = 'America/Los_Angeles';
-$string['america/louisville'] = 'America/Louisville';
+$string['america/lower_princes'] = 'America/Lower_Princes';
 $string['america/maceio'] = 'America/Maceio';
 $string['america/managua'] = 'America/Managua';
 $string['america/manaus'] = 'America/Manaus';
+$string['america/marigot'] = 'America/Marigot';
 $string['america/martinique'] = 'America/Martinique';
+$string['america/matamoros'] = 'America/Matamoros';
 $string['america/mazatlan'] = 'America/Mazatlan';
 $string['america/menominee'] = 'America/Menominee';
 $string['america/merida'] = 'America/Merida';
+$string['america/metlakatla'] = 'America/Metlakatla';
 $string['america/mexico_city'] = 'America/Mexico_City';
 $string['america/miquelon'] = 'America/Miquelon';
+$string['america/moncton'] = 'America/Moncton';
 $string['america/monterrey'] = 'America/Monterrey';
 $string['america/montevideo'] = 'America/Montevideo';
-$string['america/montreal'] = 'America/Montreal';
 $string['america/montserrat'] = 'America/Montserrat';
 $string['america/nassau'] = 'America/Nassau';
 $string['america/new_york'] = 'America/New_York';
 $string['america/nipigon'] = 'America/Nipigon';
 $string['america/nome'] = 'America/Nome';
 $string['america/noronha'] = 'America/Noronha';
+$string['america/north_dakota/beulah'] = 'America/North_Dakota/Beulah';
 $string['america/north_dakota/center'] = 'America/North_Dakota/Center';
+$string['america/north_dakota/new_salem'] = 'America/North_Dakota/New_Salem';
+$string['america/ojinaga'] = 'America/Ojinaga';
 $string['america/panama'] = 'America/Panama';
 $string['america/pangnirtung'] = 'America/Pangnirtung';
 $string['america/paramaribo'] = 'America/Paramaribo';
@@ -173,15 +191,20 @@ $string['america/port-au-prince'] = 'America/Port-au-Prince';
 $string['america/port_of_spain'] = 'America/Port_of_Spain';
 $string['america/porto_velho'] = 'America/Porto_Velho';
 $string['america/puerto_rico'] = 'America/Puerto_Rico';
+$string['america/punta_arenas'] = 'America/Punta_Arenas';
 $string['america/rainy_river'] = 'America/Rainy_River';
 $string['america/rankin_inlet'] = 'America/Rankin_Inlet';
 $string['america/recife'] = 'America/Recife';
 $string['america/regina'] = 'America/Regina';
+$string['america/resolute'] = 'America/Resolute';
 $string['america/rio_branco'] = 'America/Rio_Branco';
+$string['america/santarem'] = 'America/Santarem';
 $string['america/santiago'] = 'America/Santiago';
 $string['america/santo_domingo'] = 'America/Santo_Domingo';
 $string['america/sao_paulo'] = 'America/Sao_Paulo';
 $string['america/scoresbysund'] = 'America/Scoresbysund';
+$string['america/sitka'] = 'America/Sitka';
+$string['america/st_barthelemy'] = 'America/St_Barthelemy';
 $string['america/st_johns'] = 'America/St_Johns';
 $string['america/st_kitts'] = 'America/St_Kitts';
 $string['america/st_lucia'] = 'America/St_Lucia';
@@ -202,12 +225,15 @@ $string['america/yellowknife'] = 'America/Yellowknife';
 $string['antarctica/casey'] = 'Antarctica/Casey';
 $string['antarctica/davis'] = 'Antarctica/Davis';
 $string['antarctica/dumontdurville'] = 'Antarctica/DumontDUrville';
+$string['antarctica/macquarie'] = 'Antarctica/Macquarie';
 $string['antarctica/mawson'] = 'Antarctica/Mawson';
 $string['antarctica/mcmurdo'] = 'Antarctica/McMurdo';
 $string['antarctica/palmer'] = 'Antarctica/Palmer';
 $string['antarctica/rothera'] = 'Antarctica/Rothera';
 $string['antarctica/syowa'] = 'Antarctica/Syowa';
+$string['antarctica/troll'] = 'Antarctica/Troll';
 $string['antarctica/vostok'] = 'Antarctica/Vostok';
+$string['arctic/longyearbyen'] = 'Arctic/Longyearbyen';
 $string['asia/aden'] = 'Asia/Aden';
 $string['asia/almaty'] = 'Asia/Almaty';
 $string['asia/amman'] = 'Asia/Amman';
@@ -215,34 +241,39 @@ $string['asia/anadyr'] = 'Asia/Anadyr';
 $string['asia/aqtau'] = 'Asia/Aqtau';
 $string['asia/aqtobe'] = 'Asia/Aqtobe';
 $string['asia/ashgabat'] = 'Asia/Ashgabat';
+$string['asia/atyrau'] = 'Asia/Atyrau';
 $string['asia/baghdad'] = 'Asia/Baghdad';
 $string['asia/bahrain'] = 'Asia/Bahrain';
 $string['asia/baku'] = 'Asia/Baku';
 $string['asia/bangkok'] = 'Asia/Bangkok';
+$string['asia/barnaul'] = 'Asia/Barnaul';
 $string['asia/beirut'] = 'Asia/Beirut';
 $string['asia/bishkek'] = 'Asia/Bishkek';
 $string['asia/brunei'] = 'Asia/Brunei';
-$string['asia/calcutta'] = 'Asia/Calcutta';
+$string['asia/chita'] = 'Asia/Chita';
+$string['asia/choibalsan'] = 'Asia/Choibalsan';
 $string['asia/colombo'] = 'Asia/Colombo';
 $string['asia/damascus'] = 'Asia/Damascus';
 $string['asia/dhaka'] = 'Asia/Dhaka';
 $string['asia/dili'] = 'Asia/Dili';
 $string['asia/dubai'] = 'Asia/Dubai';
 $string['asia/dushanbe'] = 'Asia/Dushanbe';
+$string['asia/famagusta'] = 'Asia/Famagusta';
 $string['asia/gaza'] = 'Asia/Gaza';
-$string['asia/harbin'] = 'Asia/Harbin';
+$string['asia/hebron'] = 'Asia/Hebron';
+$string['asia/ho_chi_minh'] = 'Asia/Ho_Chi_Minh';
 $string['asia/hong_kong'] = 'Asia/Hong_Kong';
 $string['asia/hovd'] = 'Asia/Hovd';
-$string['asia/choibalsan'] = 'Asia/Choibalsan';
-$string['asia/chongqing'] = 'Asia/Chongqing';
 $string['asia/irkutsk'] = 'Asia/Irkutsk';
+$string['asia/jakarta'] = 'Asia/Jakarta';
 $string['asia/jayapura'] = 'Asia/Jayapura';
 $string['asia/jerusalem'] = 'Asia/Jerusalem';
 $string['asia/kabul'] = 'Asia/Kabul';
 $string['asia/kamchatka'] = 'Asia/Kamchatka';
 $string['asia/karachi'] = 'Asia/Karachi';
-$string['asia/kashgar'] = 'Asia/Kashgar';
-$string['asia/katmandu'] = 'Asia/Katmandu';
+$string['asia/kathmandu'] = 'Asia/Kathmandu';
+$string['asia/khandyga'] = 'Asia/Khandyga';
+$string['asia/kolkata'] = 'Asia/Kolkata';
 $string['asia/krasnoyarsk'] = 'Asia/Krasnoyarsk';
 $string['asia/kuala_lumpur'] = 'Asia/Kuala_Lumpur';
 $string['asia/kuching'] = 'Asia/Kuching';
@@ -253,6 +284,7 @@ $string['asia/makassar'] = 'Asia/Makassar';
 $string['asia/manila'] = 'Asia/Manila';
 $string['asia/muscat'] = 'Asia/Muscat';
 $string['asia/nicosia'] = 'Asia/Nicosia';
+$string['asia/novokuznetsk'] = 'Asia/Novokuznetsk';
 $string['asia/novosibirsk'] = 'Asia/Novosibirsk';
 $string['asia/omsk'] = 'Asia/Omsk';
 $string['asia/oral'] = 'Asia/Oral';
@@ -260,42 +292,47 @@ $string['asia/phnom_penh'] = 'Asia/Phnom_Penh';
 $string['asia/pontianak'] = 'Asia/Pontianak';
 $string['asia/pyongyang'] = 'Asia/Pyongyang';
 $string['asia/qatar'] = 'Asia/Qatar';
+$string['asia/qostanay'] = 'Asia/Qostanay';
 $string['asia/qyzylorda'] = 'Asia/Qyzylorda';
-$string['asia/rangoon'] = 'Asia/Rangoon';
 $string['asia/riyadh'] = 'Asia/Riyadh';
-$string['asia/saigon'] = 'Asia/Saigon';
 $string['asia/sakhalin'] = 'Asia/Sakhalin';
 $string['asia/samarkand'] = 'Asia/Samarkand';
 $string['asia/seoul'] = 'Asia/Seoul';
 $string['asia/shanghai'] = 'Asia/Shanghai';
 $string['asia/singapore'] = 'Asia/Singapore';
+$string['asia/srednekolymsk'] = 'Asia/Srednekolymsk';
 $string['asia/taipei'] = 'Asia/Taipei';
 $string['asia/tashkent'] = 'Asia/Tashkent';
 $string['asia/tbilisi'] = 'Asia/Tbilisi';
 $string['asia/tehran'] = 'Asia/Tehran';
 $string['asia/thimphu'] = 'Asia/Thimphu';
 $string['asia/tokyo'] = 'Asia/Tokyo';
+$string['asia/tomsk'] = 'Asia/Tomsk';
 $string['asia/ulaanbaatar'] = 'Asia/Ulaanbaatar';
 $string['asia/urumqi'] = 'Asia/Urumqi';
+$string['asia/ust-nera'] = 'Asia/Ust-Nera';
 $string['asia/vientiane'] = 'Asia/Vientiane';
 $string['asia/vladivostok'] = 'Asia/Vladivostok';
 $string['asia/yakutsk'] = 'Asia/Yakutsk';
+$string['asia/yangon'] = 'Asia/Yangon';
 $string['asia/yekaterinburg'] = 'Asia/Yekaterinburg';
 $string['asia/yerevan'] = 'Asia/Yerevan';
 $string['atlantic/azores'] = 'Atlantic/Azores';
 $string['atlantic/bermuda'] = 'Atlantic/Bermuda';
 $string['atlantic/canary'] = 'Atlantic/Canary';
 $string['atlantic/cape_verde'] = 'Atlantic/Cape_Verde';
-$string['atlantic/faeroe'] = 'Atlantic/Faeroe';
+$string['atlantic/faroe'] = 'Atlantic/Faroe';
 $string['atlantic/madeira'] = 'Atlantic/Madeira';
 $string['atlantic/reykjavik'] = 'Atlantic/Reykjavik';
 $string['atlantic/south_georgia'] = 'Atlantic/South_Georgia';
-$string['atlantic/stanley'] = 'Atlantic/Stanley';
 $string['atlantic/st_helena'] = 'Atlantic/St_Helena';
+$string['atlantic/stanley'] = 'Atlantic/Stanley';
 $string['australia/adelaide'] = 'Australia/Adelaide';
 $string['australia/brisbane'] = 'Australia/Brisbane';
 $string['australia/broken_hill'] = 'Australia/Broken_Hill';
+$string['australia/currie'] = 'Australia/Currie';
 $string['australia/darwin'] = 'Australia/Darwin';
+$string['australia/eucla'] = 'Australia/Eucla';
 $string['australia/hobart'] = 'Australia/Hobart';
 $string['australia/lindeman'] = 'Australia/Lindeman';
 $string['australia/lord_howe'] = 'Australia/Lord_Howe';
@@ -304,51 +341,69 @@ $string['australia/perth'] = 'Australia/Perth';
 $string['australia/sydney'] = 'Australia/Sydney';
 $string['europe/amsterdam'] = 'Europe/Amsterdam';
 $string['europe/andorra'] = 'Europe/Andorra';
+$string['europe/astrakhan'] = 'Europe/Astrakhan';
 $string['europe/athens'] = 'Europe/Athens';
-$string['europe/belfast'] = 'Europe/Belfast';
 $string['europe/belgrade'] = 'Europe/Belgrade';
 $string['europe/berlin'] = 'Europe/Berlin';
+$string['europe/bratislava'] = 'Europe/Bratislava';
 $string['europe/brussels'] = 'Europe/Brussels';
-$string['europe/budapest'] = 'Europe/Budapest';
 $string['europe/bucharest'] = 'Europe/Bucharest';
+$string['europe/budapest'] = 'Europe/Budapest';
+$string['europe/busingen'] = 'Europe/Busingen';
+$string['europe/chisinau'] = 'Europe/Chisinau';
 $string['europe/copenhagen'] = 'Europe/Copenhagen';
 $string['europe/dublin'] = 'Europe/Dublin';
 $string['europe/gibraltar'] = 'Europe/Gibraltar';
+$string['europe/guernsey'] = 'Europe/Guernsey';
 $string['europe/helsinki'] = 'Europe/Helsinki';
-$string['europe/chisinau'] = 'Europe/Chisinau';
+$string['europe/isle_of_man'] = 'Europe/Isle_of_Man';
 $string['europe/istanbul'] = 'Europe/Istanbul';
+$string['europe/jersey'] = 'Europe/Jersey';
 $string['europe/kaliningrad'] = 'Europe/Kaliningrad';
 $string['europe/kiev'] = 'Europe/Kiev';
+$string['europe/kirov'] = 'Europe/Kirov';
 $string['europe/lisbon'] = 'Europe/Lisbon';
+$string['europe/ljubljana'] = 'Europe/Ljubljana';
 $string['europe/london'] = 'Europe/London';
 $string['europe/luxembourg'] = 'Europe/Luxembourg';
 $string['europe/madrid'] = 'Europe/Madrid';
 $string['europe/malta'] = 'Europe/Malta';
+$string['europe/mariehamn'] = 'Europe/Mariehamn';
 $string['europe/minsk'] = 'Europe/Minsk';
 $string['europe/monaco'] = 'Europe/Monaco';
 $string['europe/moscow'] = 'Europe/Moscow';
 $string['europe/oslo'] = 'Europe/Oslo';
 $string['europe/paris'] = 'Europe/Paris';
+$string['europe/podgorica'] = 'Europe/Podgorica';
 $string['europe/prague'] = 'Europe/Prague';
 $string['europe/riga'] = 'Europe/Riga';
 $string['europe/rome'] = 'Europe/Rome';
 $string['europe/samara'] = 'Europe/Samara';
+$string['europe/san_marino'] = 'Europe/San_Marino';
+$string['europe/sarajevo'] = 'Europe/Sarajevo';
+$string['europe/saratov'] = 'Europe/Saratov';
 $string['europe/simferopol'] = 'Europe/Simferopol';
+$string['europe/skopje'] = 'Europe/Skopje';
 $string['europe/sofia'] = 'Europe/Sofia';
 $string['europe/stockholm'] = 'Europe/Stockholm';
 $string['europe/tallinn'] = 'Europe/Tallinn';
 $string['europe/tirane'] = 'Europe/Tirane';
+$string['europe/ulyanovsk'] = 'Europe/Ulyanovsk';
 $string['europe/uzhgorod'] = 'Europe/Uzhgorod';
 $string['europe/vaduz'] = 'Europe/Vaduz';
+$string['europe/vatican'] = 'Europe/Vatican';
 $string['europe/vienna'] = 'Europe/Vienna';
 $string['europe/vilnius'] = 'Europe/Vilnius';
+$string['europe/volgograd'] = 'Europe/Volgograd';
 $string['europe/warsaw'] = 'Europe/Warsaw';
+$string['europe/zagreb'] = 'Europe/Zagreb';
 $string['europe/zaporozhye'] = 'Europe/Zaporozhye';
 $string['europe/zurich'] = 'Europe/Zurich';
 $string['indian/antananarivo'] = 'Indian/Antananarivo';
-$string['indian/comoro'] = 'Indian/Comoro';
 $string['indian/chagos'] = 'Indian/Chagos';
 $string['indian/christmas'] = 'Indian/Christmas';
+$string['indian/cocos'] = 'Indian/Cocos';
+$string['indian/comoro'] = 'Indian/Comoro';
 $string['indian/kerguelen'] = 'Indian/Kerguelen';
 $string['indian/mahe'] = 'Indian/Mahe';
 $string['indian/maldives'] = 'Indian/Maldives';
@@ -357,6 +412,9 @@ $string['indian/mayotte'] = 'Indian/Mayotte';
 $string['indian/reunion'] = 'Indian/Reunion';
 $string['pacific/apia'] = 'Pacific/Apia';
 $string['pacific/auckland'] = 'Pacific/Auckland';
+$string['pacific/bougainville'] = 'Pacific/Bougainville';
+$string['pacific/chatham'] = 'Pacific/Chatham';
+$string['pacific/chuuk'] = 'Pacific/Chuuk';
 $string['pacific/easter'] = 'Pacific/Easter';
 $string['pacific/efate'] = 'Pacific/Efate';
 $string['pacific/enderbury'] = 'Pacific/Enderbury';
@@ -368,7 +426,6 @@ $string['pacific/gambier'] = 'Pacific/Gambier';
 $string['pacific/guadalcanal'] = 'Pacific/Guadalcanal';
 $string['pacific/guam'] = 'Pacific/Guam';
 $string['pacific/honolulu'] = 'Pacific/Honolulu';
-$string['pacific/chatham'] = 'Pacific/Chatham';
 $string['pacific/kiritimati'] = 'Pacific/Kiritimati';
 $string['pacific/kosrae'] = 'Pacific/Kosrae';
 $string['pacific/kwajalein'] = 'Pacific/Kwajalein';
@@ -382,14 +439,33 @@ $string['pacific/noumea'] = 'Pacific/Noumea';
 $string['pacific/pago_pago'] = 'Pacific/Pago_Pago';
 $string['pacific/palau'] = 'Pacific/Palau';
 $string['pacific/pitcairn'] = 'Pacific/Pitcairn';
-$string['pacific/ponape'] = 'Pacific/Ponape';
+$string['pacific/pohnpei'] = 'Pacific/Pohnpei';
 $string['pacific/port_moresby'] = 'Pacific/Port_Moresby';
 $string['pacific/rarotonga'] = 'Pacific/Rarotonga';
 $string['pacific/saipan'] = 'Pacific/Saipan';
 $string['pacific/tahiti'] = 'Pacific/Tahiti';
 $string['pacific/tarawa'] = 'Pacific/Tarawa';
 $string['pacific/tongatapu'] = 'Pacific/Tongatapu';
-$string['pacific/truk'] = 'Pacific/Truk';
 $string['pacific/wake'] = 'Pacific/Wake';
 $string['pacific/wallis'] = 'Pacific/Wallis';
+$string['utc'] = 'UTC';
+// The following identifiers have been previous removed from TimeDateZone::listIdentifiers and are no longer used.
+// Deprecated since Moodle 3.9.
+$string['africa/asmera'] = 'Africa/Asmera';
+$string['africa/timbuktu'] = 'Africa/Timbuktu';
+$string['america/argentina/comodrivadavia'] = 'America/Argentina/ComodRivadavia';
+$string['america/indianapolis'] = 'America/Indianapolis';
+$string['america/louisville'] = 'America/Louisville';
+$string['america/montreal'] = 'America/Montreal';
+$string['asia/calcutta'] = 'Asia/Calcutta';
+$string['asia/chongqing'] = 'Asia/Chongqing';
+$string['asia/harbin'] = 'Asia/Harbin';
+$string['asia/kashgar'] = 'Asia/Kashgar';
+$string['asia/katmandu'] = 'Asia/Katmandu';
+$string['asia/rangoon'] = 'Asia/Rangoon';
+$string['asia/saigon'] = 'Asia/Saigon';
+$string['atlantic/faeroe'] = 'Atlantic/Faeroe';
+$string['europe/belfast'] = 'Europe/Belfast';
+$string['pacific/ponape'] = 'Pacific/Ponape';
+$string['pacific/truk'] = 'Pacific/Truk';
 $string['pacific/yap'] = 'Pacific/Yap';
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 d7a2b60..f8d3eed 100644 (file)
@@ -90,31 +90,54 @@ class behat_context_helper {
      * @return behat_base
      */
     public static function get($classname) {
-        if (self::$environment->hasContextClass($classname)) {
-            return self::$environment->getContext($classname);
+        $definedclassname = self::get_theme_override($classname);
+        if ($definedclassname) {
+            return self::$environment->getContext($definedclassname);
         }
 
+        // Just fall back on getContext to ensure that we throw the correct exception.
+        return self::$environment->getContext($classname);
+    }
+
+    /**
+     * Get the context for the specified component or subsystem.
+     *
+     * @param string $component The component or subsystem to find the context for
+     * @return behat_base|null
+     */
+    public static function get_component_context(string $component): ?behat_base {
+        $component = str_replace('core_', '', $component);
+
+        if ($classname = self::get_theme_override("behat_{$component}")) {
+            return self::get($classname);
+        }
+
+        return null;
+    }
+
+    /**
+     * Check for any theme override of the specified class name.
+     *
+     * @param string $classname
+     * @return string|null
+     */
+    protected static function get_theme_override(string $classname): ?string {
         $suitename = self::$environment->getSuite()->getName();
         // If default suite, then get the default theme name.
         if ($suitename == 'default') {
             $suitename = theme_config::DEFAULT_THEME;
         }
-        $overridencontextname = 'behat_theme_'.$suitename.'_'.$classname;
-
-        // If contexts has not been checked before and doesn't exist then just use core one.
-        if (!isset(self::$nonexistingcontexts[$overridencontextname])) {
-            try {
-                $subcontext = self::$environment->getContext($overridencontextname);
-
-                return $subcontext;
-            } catch (Behat\Behat\Context\Exception\ContextNotFoundException $e) {
-                // If context not found then it's not overridden.
-                self::$nonexistingcontexts[$overridencontextname] = 1;
-            }
+
+        $overrideclassname = "behat_theme_{$suitename}_{$classname}";
+        if (self::$environment->hasContextClass($overrideclassname)) {
+            return $overrideclassname;
         }
 
-        // Get the actual context.
-        return self::$environment->getContext($classname);
+        if (self::$environment->hasContextClass($classname)) {
+            return $classname;
+        }
+
+        return null;
     }
 
     /**
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/event/user_password_policy_failed.php b/lib/classes/event/user_password_policy_failed.php
new file mode 100644 (file)
index 0000000..18bee65
--- /dev/null
@@ -0,0 +1,89 @@
+<?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/>.
+
+/**
+ * Password policy failed event.
+ *
+ * @package    core
+ * @copyright  2020 Peter Burnett <peterburnett@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event when user's current password fails the password policy
+ *
+ * @package    core
+ * @since      Moodle 3.9
+ * @copyright  2020 Peter Burnett <peterburnett@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_password_policy_failed extends base {
+    /**
+     * Create event for user's current password failing password policy.
+     *
+     * @param \stdClass $user
+     * @return user_password_updated
+     */
+    public static function create_from_user(\stdClass $user) {
+        $data = array(
+            'context' => \context_user::instance($user->id),
+            'userid' => $user->id,
+            'relateduserid' => $user->id,
+        );
+        $event = self::create($data);
+        $event->add_record_snapshot('user', $user);
+        return $event;
+    }
+
+    /**
+     * Initialise required event data properties.
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserpasswordpolicyfailed');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The password for user with id '$this->userid' failed the current password policy.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/user/profile.php', array('id' => $this->userid));
+    }
+}
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