Merge branch 'MDL-67264-review-squashed' of https://github.com/Chocolate-lightning...
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 13 Feb 2020 03:43:41 +0000 (11:43 +0800)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Fri, 14 Feb 2020 10:16:31 +0000 (11:16 +0100)
52 files changed:
admin/settings/h5p.php
auth/shibboleth/auth.php
auth/shibboleth/index.php
auth/shibboleth/lang/en/auth_shibboleth.php
auth/shibboleth/settings.php
badges/classes/form/badge.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
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
lang/en/admin.php
lang/en/deprecated.txt
lang/en/h5p.php
lang/en/webservice.php
lib/classes/date.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/questionlib.php
lib/tests/filterlib_test.php
lib/tests/fixtures/testable_core_h5p.php
lib/tests/h5p_get_content_types_task_test.php
lib/tests/questionlib_test.php
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
pix/b/h5p_library.svg [new file with mode: 0644]
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/src/form-display-errors.js
webservice/wsdoc.php

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 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 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 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 a29697c..473305b 100644 (file)
@@ -480,6 +480,7 @@ $string['devicedetectregex_desc'] = '<p>By default, Moodle can detect devices of
 $string['devicedetectregexexpression'] = 'Regular expression';
 $string['devicedetectregexvalue'] = 'Return value';
 $string['devicetype'] = 'Device type';
+$string['disabled'] = 'Disabled';
 $string['disableuserimages'] = 'Disable user profile images';
 $string['displayerrorswarning'] = 'Enabling the PHP setting <em>display_errors</em> is not recommended on production sites because some error messages may reveal sensitive information about your server.';
 $string['displayloginfailures'] = 'Display login failures';
index abef9f9..7753147 100644 (file)
@@ -120,3 +120,4 @@ global,core_calendar
 globalevent,core_calendar
 globalevents,core_calendar
 eventtypeglobal,core_calendar
+documentation,core_webservice
index 2fc0a62..febe040 100644 (file)
@@ -30,6 +30,8 @@ $string['addedandupdatedss'] = 'Added {$a->%new} new H5P library and updated {$a
 $string['addednewlibraries'] = 'Added {$a->%new} new H5P libraries.';
 $string['addednewlibrary'] = 'Added {$a->%new} new H5P library.';
 $string['additionallicenseinfo'] = 'Any additional information about the license';
+$string['atto_h5p'] = 'Insert H5P button';
+$string['atto_h5p_description'] = 'The Insert H5P button in the Atto editor enables users to insert H5P content by either entering a URL or embed code, or by uploading an H5P file.';
 $string['author'] = 'Author';
 $string['authorcomments'] = 'Author comments';
 $string['authorcommentsdescription'] = 'Comments for the editor of the content. (This text will not be published as a part of the copyright info.)';
@@ -65,6 +67,7 @@ $string['couldNotParseJSONFromZip'] = 'Unable to parse JSON from the package: {$
 $string['couldNotReadFileFromZip'] = 'Unable to read file from the package: {$a->%fileName}';
 $string['creativecommons'] = 'Creative Commons';
 $string['date'] = 'Date';
+$string['description'] = 'Description';
 $string['disablefullscreen'] = 'Disable fullscreen';
 $string['download'] = 'Download';
 $string['downloadtitle'] = 'Download this content as a H5P file.';
@@ -73,8 +76,11 @@ $string['embed'] = 'Embed';
 $string['embedtitle'] = 'View the embed code for this content.';
 $string['eventh5pviewed'] = 'H5P content viewed';
 $string['eventh5pdeleted'] = 'H5P deleted';
+$string['feature'] = 'Feature';
 $string['fetchtypesfailure'] = 'No information could be obtained on the H5P content types available. H5P repository connection failure';
 $string['fileExceedsMaxSize'] = 'One of the files inside the package exceeds the maximum file size allowed. ({$a->%file} {$a->%used} > {$a->%max})';
+$string['filter_displayh5p'] = 'Display H5P filter';
+$string['filter_displayh5p_description'] = 'The Display H5P filter converts URLs into embedded H5P content.';
 $string['fullscreen'] = 'Fullscreen';
 $string['gpl'] = 'General Public License v3';
 $string['h5p'] = 'H5P';
@@ -83,6 +89,7 @@ $string['h5pfilenotfound'] = 'H5P file not found';
 $string['h5pinvalidurl'] = 'Invalid H5P content URL.';
 $string['h5pprivatefile'] = 'This H5P content can\'t be displayed because you don\'t have access to the .h5p file.';
 $string['h5pmanage'] = 'Manage H5P content types';
+$string['h5poverview'] = 'H5P overview';
 $string['h5ppackage'] = 'H5P content type';
 $string['h5ppackage_help'] = 'An H5P content type is a file with an H5P or ZIP extension containing all libraries required to display the content.';
 $string['hideadvanced'] = 'Hide advanced';
@@ -157,10 +164,13 @@ $string['reuseDescription'] = 'Reuse this content.';
 $string['showadvanced'] = 'Show advanced';
 $string['showless'] = 'Show less';
 $string['showmore'] = 'Show more';
+$string['status'] = 'Status';
 $string['size'] = 'Size';
 $string['source'] = 'Source';
 $string['startingover'] = 'You\'ll be starting over.';
 $string['sublevel'] = 'Sublevel';
+$string['task_h5p'] = 'H5P scheduled task';
+$string['task_h5p_description'] = 'The H5P scheduled task downloads available H5P content types from h5p.org.';
 $string['thumbnail'] = 'Thumbnail';
 $string['title'] = 'Title';
 $string['undisclosed'] = 'Undisclosed';
index 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 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',
index 836a2bf..29ed69b 100644 (file)
Binary files a/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-debug.js and b/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-debug.js differ
index f18c330..41c5f75 100644 (file)
Binary files a/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-min.js and b/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-min.js differ
index 836a2bf..29ed69b 100644 (file)
Binary files a/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button.js and b/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button.js differ
index 66971a8..4b753ba 100644 (file)
@@ -186,32 +186,11 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
             tagMatchRequiresAll: false
         });
 
-        this.editor.on(['keyup', 'cut'], this._clearH5P, this);
         this.editor.all('.h5p-placeholder').setAttribute('contenteditable', 'false');
         this.editor.delegate('dblclick', this._handleDblClick, '.h5p-placeholder', this);
         this.editor.delegate('click', this._handleClick, '.h5p-placeholder', this);
     },
 
-    /**
-     * Deletes elements with class .h5p-placeholder on backspace and delete.
-     *
-     * @method _clearH5P
-     * @param {EventFacade} e
-     * @private
-     */
-    _clearH5P: function(e) {
-        if (e.keyCode === 8 || e.keyCode === 46) {
-            var parentNodes = this.get('host').getSelectedNodes().get('parentNode');
-            if (parentNodes.hasOwnProperty('_nodes')) {
-                var placeholder = parentNodes.filter('.h5p-placeholder');
-                if (!placeholder.isEmpty()) {
-                    placeholder.remove();
-                }
-            }
-        }
-        e.preventDefault();
-    },
-
     /**
      * Handle a double click on a H5P Placeholder.
      *
index 0334499..14eb6c6 100644 (file)
@@ -2208,35 +2208,19 @@ class file_storage {
         $rs->close();
         mtrace('done.');
 
-        // remove orphaned preview files (that is files in the core preview filearea without
-        // the existing original file)
-        mtrace('Deleting orphaned preview files... ', '');
+        // Remove orphaned files:
+        // * preview files in the core preview filearea without the existing original file.
+        // * document converted files in core documentconversion filearea without the existing original file.
+        mtrace('Deleting orphaned preview, and document conversion files... ', '');
         cron_trace_time_and_memory();
         $sql = "SELECT p.*
                   FROM {files} p
              LEFT JOIN {files} o ON (p.filename = o.contenthash)
-                 WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
-                       AND o.id IS NULL";
-        $syscontext = context_system::instance();
-        $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
-        foreach ($rs as $orphan) {
-            $file = $this->get_file_instance($orphan);
-            if (!$file->is_directory()) {
-                $file->delete();
-            }
-        }
-        $rs->close();
-        mtrace('done.');
-
-        // Remove orphaned converted files (that is files in the core documentconversion filearea without
-        // the existing original file).
-        mtrace('Deleting orphaned document conversion files... ', '');
-        cron_trace_time_and_memory();
-        $sql = "SELECT p.*
-                  FROM {files} p
-             LEFT JOIN {files} o ON (p.filename = o.contenthash)
-                 WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'documentconversion' AND p.itemid = 0
-                       AND o.id IS NULL";
+                 WHERE p.contextid = ?
+                   AND p.component = 'core'
+                   AND (p.filearea = 'preview' OR p.filearea = 'documentconversion')
+                   AND p.itemid = 0
+                   AND o.id IS NULL";
         $syscontext = context_system::instance();
         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
         foreach ($rs as $orphan) {
index a68f222..b3f4162 100644 (file)
@@ -709,6 +709,35 @@ function filter_set_global_state($filtername, $state, $move = 0) {
     $transaction->allow_commit();
 }
 
+/**
+ * Returns the active state for a filter in the given context.
+ *
+ * @param string $filtername The filter name, for example 'tex'.
+ * @param integer $contextid The id of the context to get the data for.
+ * @return int value of active field for the given filter.
+ */
+function filter_get_active_state(string $filtername, $contextid = null): int {
+    global $DB;
+
+    if ($contextid === null) {
+        $contextid = context_system::instance()->id;
+    }
+    if (is_object($contextid)) {
+        $contextid = $contextid->id;
+    }
+
+    if (strpos($filtername, 'filter/') === 0) {
+        $filtername = substr($filtername, 7);
+    } else if (strpos($filtername, '/') !== false) {
+        throw new coding_exception("Invalid filter name '$filtername' used in filter_is_enabled()");
+    }
+    if ($active = $DB->get_field('filter_active', 'active', array('filter' => $filtername, 'contextid' => $contextid))) {
+        return $active;
+    }
+
+    return TEXTFILTER_DISABLED;
+}
+
 /**
  * @param string $filtername The filter name, for example 'tex'.
  * @return boolean is this filter allowed to be used on this site. That is, the
index 8f17df0..cfcf547 100644 (file)
@@ -511,7 +511,8 @@ function question_save_from_deletion($questionids, $newcontextid, $oldplace,
         $newcategory = new stdClass();
         $newcategory->parent = question_get_top_category($newcontextid, true)->id;
         $newcategory->contextid = $newcontextid;
-        $newcategory->name = get_string('questionsrescuedfrom', 'question', $oldplace);
+        // Max length of column name in question_categories is 255.
+        $newcategory->name = shorten_text(get_string('questionsrescuedfrom', 'question', $oldplace), 255);
         $newcategory->info = get_string('questionsrescuedfrominfo', 'question', $oldplace);
         $newcategory->sortorder = 999;
         $newcategory->stamp = make_unique_id_code();
@@ -1356,7 +1357,8 @@ function question_make_default_categories($contexts) {
             // Otherwise, we need to make one
             $category = new stdClass();
             $contextname = $context->get_context_name(false, true);
-            $category->name = get_string('defaultfor', 'question', $contextname);
+            // Max length of name field is 255.
+            $category->name = shorten_text(get_string('defaultfor', 'question', $contextname), 255);
             $category->info = get_string('defaultinfofor', 'question', $contextname);
             $category->contextid = $context->id;
             $category->parent = $topcategory->id;
index 8f8e5fa..90fcb65 100644 (file)
@@ -782,6 +782,61 @@ class core_filterlib_testcase extends advanced_testcase {
         $this->assertInstanceOf('performance_measuring_filter_manager', $filterman);
     }
 
+    public function test_filter_get_active_state_contextid_parameter() {
+        $this->resetAfterTest();
+
+        filter_set_global_state('glossary', TEXTFILTER_ON);
+        // Using system context by default.
+        $active = filter_get_active_state('glossary');
+        $this->assertEquals($active, TEXTFILTER_ON);
+
+        $systemcontext = context_system::instance();
+        // Passing $systemcontext object.
+        $active = filter_get_active_state('glossary', $systemcontext);
+        $this->assertEquals($active, TEXTFILTER_ON);
+
+        // Passing $systemcontext id.
+        $active = filter_get_active_state('glossary', $systemcontext->id);
+        $this->assertEquals($active, TEXTFILTER_ON);
+
+        // Not system context.
+        filter_set_local_state('glossary', '123', TEXTFILTER_ON);
+        $active = filter_get_active_state('glossary', '123');
+        $this->assertEquals($active, TEXTFILTER_ON);
+    }
+
+    public function test_filter_get_active_state_filtername_parameter() {
+        $this->resetAfterTest();
+
+        filter_set_global_state('glossary', TEXTFILTER_ON);
+        // Using full filtername.
+        $active = filter_get_active_state('filter/glossary');
+        $this->assertEquals($active, TEXTFILTER_ON);
+
+        // Wrong filtername.
+        $this->expectException('coding_exception');
+        $active = filter_get_active_state('mod/glossary');
+    }
+
+    public function test_filter_get_active_state_after_change() {
+        $this->resetAfterTest();
+
+        filter_set_global_state('glossary', TEXTFILTER_ON);
+        $systemcontextid = context_system::instance()->id;
+        $active = filter_get_active_state('glossary', $systemcontextid);
+        $this->assertEquals($active, TEXTFILTER_ON);
+
+        filter_set_global_state('glossary', TEXTFILTER_OFF);
+        $systemcontextid = context_system::instance()->id;
+        $active = filter_get_active_state('glossary', $systemcontextid);
+        $this->assertEquals($active, TEXTFILTER_OFF);
+
+        filter_set_global_state('glossary', TEXTFILTER_DISABLED);
+        $systemcontextid = context_system::instance()->id;
+        $active = filter_get_active_state('glossary', $systemcontextid);
+        $this->assertEquals($active, TEXTFILTER_DISABLED);
+    }
+
     public function test_filter_get_globally_enabled_default() {
         $this->resetAfterTest();
         $enabledfilters = filter_get_globally_enabled();
index 1914c4f..282ca89 100644 (file)
@@ -83,18 +83,22 @@ class h5p_test_core extends core {
     /**
      * Get the URL of the test endpoints instead of the H5P ones.
      *
-     * If $library is null, moodle_url is the endpoint of the json test file with the H5P content types definition. If library is
-     * the machine name of a content type, moodle_url is the test URL for downloading the content type file.
+     * 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 filename of the H5P content type file in external.
-     * @return \moodle_url The moodle_url of the file in external.
+     * @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 {
+    public function get_api_endpoint(?string $library = null, string $endpoint = 'content'): \moodle_url {
 
         if ($library) {
             $h5purl = $this->endpoint . '/' . $library . '.h5p';
+        } else if ($endpoint == 'content') {
+            $h5purl = $this->endpoint . '/h5pcontenttypes.json';
         } else {
-            $h5purl = $h5purl = $this->endpoint . '/h5pcontenttypes.json';
+            $h5purl = $this->endpoint . '/h5puuid.json';
         }
 
         return new \moodle_url($h5purl);
index 3ba655d..21dd185 100644 (file)
@@ -36,7 +36,7 @@ defined('MOODLE_INTERNAL') || die();
  *
  * @runTestsInSeparateProcesses
  */
-class h5p_get_content_types_task_test extends advanced_testcase {
+class h5p_get_content_types_task_testcase extends advanced_testcase {
 
     protected function setup() {
         global $CFG;
index ae1cd80..6f295e8 100644 (file)
@@ -481,6 +481,37 @@ class core_questionlib_testcase extends advanced_testcase {
         $this->assertEquals(1, $DB->count_records('question_categories', ['contextid' => $qcat->contextid, 'parent' => 0]));
     }
 
+    /**
+     * This function tests the question_save_from_deletion function when it is supposed to make a new category and
+     * move question categories to that new category when quiz name is very long but less than 256 characters.
+     */
+    public function test_question_save_from_deletion_quiz_with_long_name() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
+
+        // Moodle doesn't allow you to enter a name longer than 255 characters.
+        $quiz->name = shorten_text(str_repeat('123456789 ', 26), 255);
+
+        $DB->update_record('quiz', $quiz);
+
+        $context = context::instance_by_id($qcat->contextid);
+
+        $newcat = question_save_from_deletion(array_column($questions, 'id'),
+                $context->get_parent_context()->id, $context->get_context_name());
+
+        // Verifying that the inserted record's name is expected or not.
+        $this->assertEquals($DB->get_record('question_categories', ['id' => $newcat->id])->name, $newcat->name);
+
+        // Verify that the newcat itself is not a top level category.
+        $this->assertNotEquals(0, $newcat->parent);
+
+        // Verify there is just a single top-level category.
+        $this->assertEquals(1, $DB->count_records('question_categories', ['contextid' => $qcat->contextid, 'parent' => 0]));
+    }
+
     public function test_question_remove_stale_questions_from_category() {
         global $DB;
         $this->resetAfterTest(true);
index cd39407..c433452 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js differ
index 9eb3b20..58540b5 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js differ
index cd39407..c433452 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js differ
index 59d3283..df48580 100644 (file)
@@ -7,6 +7,19 @@
 /**
  * A utility to check for form changes before navigating away from a page.
  *
+ * To initialise, call M.core_formchangechecker.init({formid: 'myform'}); or perhaps
+ *
+ * Y.use('moodle-core-formchangechecker', function() {
+ *     M.core_formchangechecker.init({formid: 'myform'});
+ * });
+ *
+ * If you have some fields in your form that you don't want to have tracked, then add
+ * a data-formchangechecker-ignore-dirty to the field, or any parent element, and it
+ * will be ignored.
+ *
+ * If you have a submit button in your form that does not actually save the data,
+ * then add a data-formchangechecker-ignore-submit attribute to it.
+ *
  * @class M.core.formchangechecker
  * @constructor
  */
@@ -53,8 +66,19 @@ Y.extend(FORMCHANGECHECKER, Y.Base, {
             this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'textarea', this));
             this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'select', this));
 
-            // We need any submit buttons on the form to set the submitted flag
-            Y.one(formid).on('submit', M.core_formchangechecker.set_form_submitted, this);
+            currentform.delegate('click', function() {
+                currentform.setData('ignoreSubmission', true);
+            }, '[data-formchangechecker-ignore-submit]');
+
+            // We need any submit buttons on the form to set the submitted flag.
+            Y.one(formid).on('submit', function() {
+                if (currentform.getData('ignoreSubmission')) {
+                    // But not if we have been told to ignore this button.
+                    currentform.clearData('ignoreSubmission');
+                    return;
+                }
+                M.core_formchangechecker.set_form_submitted();
+            }, this);
 
             // YUI doesn't support onbeforeunload properly so we must use the DOM to set the onbeforeunload. As
             // a result, the has_changed must stay in the DOM too
@@ -73,10 +97,13 @@ Y.extend(FORMCHANGECHECKER, Y.Base, {
          */
         store_initial_value: function(e) {
             var thisevent;
-            if (e.target.hasClass('ignoredirty') || e.target.ancestor('.ignoredirty')) {
-                // Don't warn on elements with the ignoredirty class
+
+            // Don't warn on elements we have been told to ignore.
+            if (e.target.ancestor('.ignoredirty', true) ||
+                    e.target.ancestor('[data-formchangechecker-ignore-dirty]', true)) {
                 return;
             }
+
             if (M.core_formchangechecker.get_form_dirty_state()) {
                 // Detach all listen events to prevent duplicate initial value setting
                 while (this.initialvaluelisteners.length) {
@@ -125,10 +152,12 @@ M.core_formchangechecker.stateinformation = [];
  * Set the form changed state to true
  */
 M.core_formchangechecker.set_form_changed = function(e) {
-    if (e && e.target && (e.target.hasClass('ignoredirty') || e.target.ancestor('.ignoredirty'))) {
-        // Don't warn on elements with the ignoredirty class
+    // Don't warn on elements we have been told to ignore.
+    if (e && e.target && (e.target.ancestor('.ignoredirty', true) ||
+            e.target.ancestor('[data-formchangechecker-ignore-dirty]', true))) {
         return;
     }
+
     M.core_formchangechecker.stateinformation.formchanged = 1;
 
     // Once the form has been marked as dirty, we no longer need to keep track of form elements
index af1ae2c..9ce2b39 100644 (file)
@@ -157,6 +157,7 @@ class mod_feedback_responses_table extends table_sql {
         $this->define_headers($tableheaders);
 
         $this->sortable(true, 'lastname', SORT_ASC);
+        $this->no_sorting('groups');
         $this->collapsible(true);
         $this->set_attribute('id', 'showentrytable');
 
diff --git a/pix/b/h5p_library.svg b/pix/b/h5p_library.svg
new file mode 100644 (file)
index 0000000..fb1b4ae
--- /dev/null
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xml:space="preserve"
+   style="enable-background:new 0 0 400 225;"
+   viewBox="0 0 400 225"
+   y="0px"
+   x="0px"
+   id="Layer_1"
+   version="1.1"><metadata
+   id="metadata37"><rdf:RDF><cc:Work
+       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title>tweeter feed</dc:title></cc:Work></rdf:RDF></metadata><defs
+   id="defs35">
+       
+
+               
+               
+       
+                       
+                       
+                       
+                       
+               </defs>
+<style
+   id="style2"
+   type="text/css">
+       .st0{fill:none;}
+       .st1{fill:#8DB7E2;}
+       .st2{fill:#FFFFFF;}
+       .st3{opacity:0.2;}
+       .st4{fill:#2574A9;}
+       .st5{fill:#2376C1;}
+</style>
+<title
+   id="title4">tweeter feed</title>
+<g
+   transform="translate(7.6293945e-6)"
+   id="g879"><rect
+     style="fill:#ffffff;fill-opacity:1"
+     id="rect873"
+     width="186.86441"
+     height="186.86441"
+     x="106.56779"
+     y="19.067795"
+     rx="24.788136"
+     ry="24.788136" /><g
+     id="g848"
+     transform="matrix(0.4649826,0,0,0.4649826,119.81375,77.393814)"
+     style="fill:#2e5fbf;fill-opacity:1">
+       <path
+   id="path844"
+   d="M 325.7,14.7 C 317.6,6.9 305.3,3 289,3 H 245.5 234 v 31 h -66 l -5.4,22.2 c 4.5,-2.1 10.9,-4.2 15.3,-5.3 4.4,-1.1 8.8,-0.9 13.1,-0.9 14.6,0 26.5,4.5 35.6,13.3 9.1,8.8 13.6,20 13.6,33.4 0,9.4 -2.3,18.5 -7,27.2 -4.7,8.7 -11.3,15.4 -19.9,20 -3.1,1.6 -6.5,3.1 -10.2,4.1 H 245.5 259 V 95 h 25 c 18.2,0 31.7,-4.2 40.6,-12.5 8.9,-8.3 13.3,-19.9 13.3,-34.6 0,-14.3 -4.1,-25.4 -12.2,-33.2 z m -37,45.9 c -3.5,3 -9.6,4.4 -18.3,4.4 H 259 V 33 h 13.2 c 8.4,0 14.2,1.5 17.2,4.7 3.1,3.2 4.6,6.9 4.6,11.5 0,4.7 -1.8,8.4 -5.3,11.4 z"
+   style="fill:#2e5fbf;fill-opacity:1" />
+       <path
+   id="path846"
+   d="m 176.5,76.3 c -7.9,0 -14.7,4.6 -18,11.2 L 119,81.9 136.8,3 H 113.2 101 V 65 H 51 V 3 H 7 V 148 H 51 V 95 h 50 v 53 h 12.2 42 c -6.7,-2 -12.5,-4.6 -17.2,-8.1 -4.8,-3.6 -8.7,-7.7 -11.7,-12.3 -3,-4.6 -5.3,-9.7 -7.3,-16.5 l 39.6,-5.7 c 3.3,6.6 10.1,11.1 17.9,11.1 11.1,0 20.1,-9 20.1,-20.1 0,-11.1 -9.1,-20.1 -20.1,-20.1 z"
+   style="fill:#2e5fbf;fill-opacity:1" />
+</g></g><rect
+   style="fill:none"
+   y="0"
+   x="0"
+   id="rect6"
+   height="225"
+   width="400"
+   class="st0" />
+</svg>
\ No newline at end of file
index 5891d2a..d8f8043 100644 (file)
@@ -325,6 +325,9 @@ class question_type {
     public function save_question($question, $form) {
         global $USER, $DB, $OUTPUT;
 
+        // The actuall update/insert done with multiple DB access, so we do it in a transaction.
+        $transaction = $DB->start_delegated_transaction ();
+
         list($question->category) = explode(',', $form->category);
         $context = $this->get_context_by_category_id($question->category);
 
@@ -420,16 +423,6 @@ class question_type {
         }
         $DB->update_record('question', $question);
 
-        if ($newquestion) {
-            // Log the creation of this question.
-            $event = \core\event\question_created::create_from_question_instance($question, $context);
-            $event->trigger();
-        } else {
-            // Log the update of this question.
-            $event = \core\event\question_updated::create_from_question_instance($question, $context);
-            $event->trigger();
-        }
-
         // Now to save all the answers and type-specific options.
         $form->id = $question->id;
         $form->qtype = $question->qtype;
@@ -458,6 +451,18 @@ class question_type {
         $DB->set_field('question', 'version', question_hash($question),
                 array('id' => $question->id));
 
+        if ($newquestion) {
+            // Log the creation of this question.
+            $event = \core\event\question_created::create_from_question_instance($question, $context);
+            $event->trigger();
+        } else {
+            // Log the update of this question.
+            $event = \core\event\question_updated::create_from_question_instance($question, $context);
+            $event->trigger();
+        }
+
+        $transaction->allow_commit ();
+
         return $question;
     }
 
index c33746f..cd80f47 100644 (file)
Binary files a/theme/boost/amd/build/form-display-errors.min.js and b/theme/boost/amd/build/form-display-errors.min.js differ
index 4f30692..4ff6e98 100644 (file)
Binary files a/theme/boost/amd/build/form-display-errors.min.js.map and b/theme/boost/amd/build/form-display-errors.min.js.map differ
index 1adda2d..230cbc5 100644 (file)
@@ -25,6 +25,12 @@ define(['jquery', 'core/event'], function($, Event) {
     return {
         enhance: function(elementid) {
             var element = document.getElementById(elementid);
+            if (!element) {
+                // Some elements (e.g. static) don't have a form field.
+                // Hence there is no validation. So, no setup required here.
+                return;
+            }
+
             $(element).on(Event.Events.FORM_FIELD_VALIDATION, function(event, msg) {
                 event.preventDefault();
                 var parent = $(element).closest('.form-group');
@@ -44,6 +50,7 @@ define(['jquery', 'core/event'], function($, Event) {
                     feedback.html(msg);
 
                     // Only display and focus when the error was not already visible.
+                    // This is so that, when tabbing around the form, you don't get stuck.
                     if (!feedback.is(':visible')) {
                         feedback.show();
                         feedback.focus();
@@ -60,6 +67,17 @@ define(['jquery', 'core/event'], function($, Event) {
                     }
                 }
             });
+
+            var form = element.closest('form');
+            if (!('boostFormErrorsEnhanced' in form.dataset)) {
+                form.addEventListener('submit', function() {
+                    var visibleError = $('.form-control-feedback:visible');
+                    if (visibleError.length) {
+                        visibleError[0].focus();
+                    }
+                });
+                form.dataset.boostFormErrorsEnhanced = 1;
+            }
         }
     };
 });
index f5bb7ab..b3ed68e 100644 (file)
@@ -35,17 +35,18 @@ $tokenid = required_param('id', PARAM_INT);
 // PAGE settings
 $PAGE->set_context($usercontext);
 $PAGE->set_url('/user/wsdoc.php');
-$PAGE->set_title(get_string('documentation', 'webservice'));
-$PAGE->set_heading(get_string('documentation', 'webservice'));
+$PAGE->set_title(get_string('wsdocumentation', 'webservice'));
+$PAGE->set_heading(get_string('wsdocumentation', 'webservice'));
 $PAGE->set_pagelayout('standard');
 
 // nav bar
 $PAGE->navbar->ignore_active(true);
-$PAGE->navbar->add(get_string('usercurrentsettings'));
+$PAGE->navbar->add(get_string('preferences'), new moodle_url('/user/preferences.php'));
+$PAGE->navbar->add(get_string('useraccount'));
 $PAGE->navbar->add(get_string('securitykeys', 'webservice'),
         new moodle_url('/user/managetoken.php', 
                 array('id' => $tokenid, 'sesskey' => sesskey())));
-$PAGE->navbar->add(get_string('documentation', 'webservice'));
+$PAGE->navbar->add(get_string('wsdocumentation', 'webservice'));
 
 // check web service are enabled
 if (empty($CFG->enablewsdocumentation)) {