Merge branch 'MDL-67850-seb-integration-3' of https://github.com/catalyst/moodle
authorJake Dallimore <jake@moodle.com>
Thu, 30 Apr 2020 01:13:14 +0000 (09:13 +0800)
committerJake Dallimore <jake@moodle.com>
Thu, 30 Apr 2020 01:13:14 +0000 (09:13 +0800)
86 files changed:
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/task/lang/en/tool_task.php
admin/tool/xmldb/lang/en/tool_xmldb.php
blocks/site_main_menu/tests/behat/add_url.feature
cache/classes/helper.php
cache/classes/loaders.php
cache/classes/store.php
cache/tests/cache_test.php
cache/upgrade.txt
composer.json
composer.lock
contentbank/contenttype/h5p/lang/en/contenttype_h5p.php
contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature
contentbank/tests/behat/delete_content.feature
course/tests/behat/activity_chooser.feature
grade/grading/form/guide/lang/en/gradingform_guide.php
h5p/h5plib/v124/lang/en/h5plib_v124.php
lang/en/admin.php
lang/en/cache.php
lang/en/contentbank.php
lang/en/course.php
lang/en/enrol.php
lang/en/error.php
lang/en/h5p.php
lang/en/moodle.php
lang/en/repository.php
lang/en/role.php
lib/amd/build/custom_interaction_events.min.js
lib/amd/build/custom_interaction_events.min.js.map
lib/amd/src/custom_interaction_events.js
lib/antivirus/clamav/lang/en/antivirus_clamav.php
lib/behat/lib.php
lib/classes/task/manager.php
lib/classes/task/messaging_cleanup_task.php
lib/moodlelib.php
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/build/local/dynamic/selectors.min.js
lib/table/amd/build/local/dynamic/selectors.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/repository.js
lib/table/amd/src/local/dynamic/selectors.js
lib/table/classes/external/dynamic/fetch.php
lib/table/tests/external/dynamic/fetch_test.php
lib/tablelib.php
lib/templates/single_select.mustache
lib/templates/url_select.mustache
message/amd/build/message_preferences.min.js [new file with mode: 0644]
message/amd/build/message_preferences.min.js.map [new file with mode: 0644]
message/amd/src/message_preferences.js [new file with mode: 0644]
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/lib.php
message/output/popup/db/upgrade.php
message/output/popup/message_output_popup.php
message/output/popup/tests/messaging_cleanup_test.php [new file with mode: 0644]
message/output/popup/version.php
message/templates/message_preferences.mustache [new file with mode: 0644]
message/templates/message_preferences_component.mustache [new file with mode: 0644]
message/templates/message_preferences_notification_processor.mustache [new file with mode: 0644]
message/tests/behat/message_preferences.feature [new file with mode: 0644]
message/upgrade.txt
mod/forum/report/summary/templates/bulk_action_menu.mustache
mod/h5pactivity/lang/en/h5pactivity.php
mod/h5pactivity/tests/behat/add_h5pactivity.feature
mod/h5pactivity/tests/behat/sending_attempt.feature
mod/h5pactivity/tests/privacy_test.php
mod/lti/lang/en/lti.php
mod/quiz/accessrule/timelimit/lang/en/quizaccess_timelimit.php
mod/quiz/lang/en/quiz.php
mod/quiz/tests/behat/attempt_begin.feature
mod/workshop/tests/behat/file_type_restriction.feature
question/type/ddimageortext/lang/en/qtype_ddimageortext.php
question/type/ddmarker/lang/en/qtype_ddmarker.php
user/amd/build/participants.min.js
user/amd/build/participants.min.js.map
user/amd/build/repository.min.js [new file with mode: 0644]
user/amd/build/repository.min.js.map [new file with mode: 0644]
user/amd/build/status_field.min.js
user/amd/build/status_field.min.js.map
user/amd/src/participants.js
user/amd/src/repository.js [new file with mode: 0644]
user/amd/src/status_field.js
user/classes/table/participants.php
user/tests/behat/course_preference.feature

index 8daab15..fd0b465 100644 (file)
@@ -24,7 +24,7 @@
 
 $string['classname'] = 'Class name';
 $string['component'] = 'Component';
-$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:587. If a port isn\'t specified, the default port for the type of mail server will be used.';
+$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:993. If a port isn\'t specified, the default port for the type of mail server will be used.';
 $string['defaultexpiration'] = 'Default address expiry period';
 $string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
 $string['description'] = 'Description';
index 9ccd292..299603f 100644 (file)
  */
 
 $string['asap'] = 'ASAP';
-$string['adhocempty'] = 'Adhoc task queue is empty';
-$string['adhocqueuesize'] = 'Adhoc task queue has {$a} tasks';
+$string['adhocempty'] = 'Ad hoc task queue is empty';
+$string['adhocqueuesize'] = 'Ad hoc task queue has {$a} tasks';
 $string['adhocqueueold'] = 'Oldest task is {$a->age} which is more than {$a->max}';
 $string['backtoscheduledtasks'] = 'Back to scheduled tasks';
 $string['blocking'] = 'Blocking';
 $string['cannotfindthepathtothecli'] = 'Cannot find the path to the PHP CLI executable so task execution aborted. Set the \'Path to PHP CLI\' setting in Site administration / Server / System paths.';
-$string['checkadhocqueue'] = 'Adhoc task queue';
+$string['checkadhocqueue'] = 'Ad hoc task queue';
 $string['checkcronrunning'] = 'Cron running';
 $string['checkmaxfaildelay'] = 'Tasks max fail delay';
 $string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail delay for task \'{$a}\'? After clearing the delay, the task will run according to its normal schedule.';
@@ -58,7 +58,7 @@ $string['runpattern'] = 'Run pattern';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
-$string['taskfailures'] = 'There are {$a} task(s) failing';
+$string['taskfailures'] = '{$a} task(s) failing';
 $string['tasklogs'] = 'Task logs';
 $string['tasknofailures'] = 'There are no tasks failing';
 $string['taskscheduleday'] = 'Day';
index 77ababc..e879835 100644 (file)
@@ -158,7 +158,7 @@ $string['newtablefrommysql'] = 'New table from MySQL';
 $string['new_table_from_mysql'] = 'New table from MySQL';
 $string['nofieldsspecified'] = 'No fields specified';
 $string['nomasterprimaryuniquefound'] = 'The column(s) that your foreign key references must be included in a primary or unique KEY in the referenced table. Note that the column being in a UNIQUE INDEX is not good enough.';
-$string['nomissingorextraindexesfound'] = 'No missing or extra indexes have been found, your DB doesn\'t need further actions.';
+$string['nomissingorextraindexesfound'] = 'No missing or extra indexes have been found, so no further action is required.';
 $string['noreffieldsspecified'] = 'No reference fields specified';
 $string['noreftablespecified'] = 'Specified reference table not found';
 $string['noviolatedforeignkeysfound'] = 'No violated foreign keys found';
index 1b2a7df..ac42715 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" "button" should exist in the "Main menu" "block"
+    And "Add an activity" "button" should exist in the "Main menu" "block"
index 2776339..dc4821b 100644 (file)
@@ -361,20 +361,23 @@ class cache_helper {
     /**
      * Ensure that the stats array is ready to collect information for the given store and definition.
      * @param string $store
+     * @param string $storeclass
      * @param string $definition A string that identifies the definition.
      * @param int $mode One of cache_store::MODE_*. Since 2.9.
      */
-    protected static function ensure_ready_for_stats($store, $definition, $mode = cache_store::MODE_APPLICATION) {
+    protected static function ensure_ready_for_stats($store, $storeclass, $definition, $mode = cache_store::MODE_APPLICATION) {
         // This function is performance-sensitive, so exit as quickly as possible
         // if we do not need to do anything.
         if (isset(self::$stats[$definition]['stores'][$store])) {
             return;
         }
+
         if (!array_key_exists($definition, self::$stats)) {
             self::$stats[$definition] = array(
                 'mode' => $mode,
                 'stores' => array(
                     $store => array(
+                        'class' => $storeclass,
                         'hits' => 0,
                         'misses' => 0,
                         'sets' => 0,
@@ -383,6 +386,7 @@ class cache_helper {
             );
         } else if (!array_key_exists($store, self::$stats[$definition]['stores'])) {
             self::$stats[$definition]['stores'][$store] = array(
+                'class' => $storeclass,
                 'hits' => 0,
                 'misses' => 0,
                 'sets' => 0,
@@ -418,15 +422,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param cache_definition $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $hits The number of hits to record (by default 1)
      */
     public static function record_cache_hit($store, $definition, $hits = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['hits'] += $hits;
     }
 
@@ -436,15 +447,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param string $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $misses The number of misses to record (by default 1)
      */
     public static function record_cache_miss($store, $definition, $misses = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['misses'] += $misses;
     }
 
@@ -454,15 +472,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param string $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $sets The number of sets to record (by default 1)
      */
     public static function record_cache_set($store, $definition, $sets = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['sets'] += $sets;
     }
 
index 8cd7914..6236cb0 100644 (file)
@@ -414,7 +414,7 @@ class cache implements cache_loader {
         $setaftervalidation = false;
         if ($result === false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss($this->storetype, $this->definition);
+                cache_helper::record_cache_miss($this->store, $this->definition);
             }
             if ($this->loader !== false) {
                 // We must pass the original (unparsed) key to the next loader in the chain.
@@ -426,7 +426,7 @@ class cache implements cache_loader {
             }
             $setaftervalidation = ($result !== false);
         } else if ($this->perfdebug) {
-            cache_helper::record_cache_hit($this->storetype, $this->definition);
+            cache_helper::record_cache_hit($this->store, $this->definition);
         }
         // 5. Validate strictness.
         if ($strictness === MUST_EXIST && $result === false) {
@@ -580,8 +580,8 @@ class cache implements cache_loader {
                     $hits++;
                 }
             }
-            cache_helper::record_cache_hit($this->storetype, $this->definition, $hits);
-            cache_helper::record_cache_miss($this->storetype, $this->definition, $misses);
+            cache_helper::record_cache_hit($this->store, $this->definition, $hits);
+            cache_helper::record_cache_miss($this->store, $this->definition, $misses);
         }
 
         // Return the result. Phew!
@@ -607,7 +607,7 @@ class cache implements cache_loader {
      */
     public function set($key, $data) {
         if ($this->perfdebug) {
-            cache_helper::record_cache_set($this->storetype, $this->definition);
+            cache_helper::record_cache_set($this->store, $this->definition);
         }
         if ($this->loader !== false) {
             // We have a loader available set it there as well.
@@ -762,7 +762,7 @@ class cache implements cache_loader {
         }
         $successfullyset = $this->store->set_many($data);
         if ($this->perfdebug && $successfullyset) {
-            cache_helper::record_cache_set($this->storetype, $this->definition, $successfullyset);
+            cache_helper::record_cache_set($this->store, $this->definition, $successfullyset);
         }
         return $successfullyset;
     }
@@ -1112,7 +1112,7 @@ class cache implements cache_loader {
         }
         if ($result !== false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_hit('** static acceleration **', $this->definition);
+                cache_helper::record_cache_hit(cache_store::STATIC_ACCEL, $this->definition);
             }
             if ($this->staticaccelerationsize > 1 && $this->staticaccelerationcount > 1) {
                 // Check to see if this is the last item on the static acceleration keys array.
@@ -1126,7 +1126,7 @@ class cache implements cache_loader {
             return $result;
         } else {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss('** static acceleration **', $this->definition);
+                cache_helper::record_cache_miss(cache_store::STATIC_ACCEL, $this->definition);
             }
             return false;
         }
@@ -1830,7 +1830,7 @@ class cache_session extends cache {
         // 4. Load if from the loader/datasource if we don't already have it.
         if ($result === false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss($this->storetype, $this->get_definition());
+                cache_helper::record_cache_miss($this->get_store(), $this->get_definition());
             }
             if ($this->get_loader() !== false) {
                 // We must pass the original (unparsed) key to the next loader in the chain.
@@ -1845,7 +1845,7 @@ class cache_session extends cache {
                 $this->set($key, $result);
             }
         } else if ($this->perfdebug) {
-            cache_helper::record_cache_hit($this->storetype, $this->get_definition());
+            cache_helper::record_cache_hit($this->get_store(), $this->get_definition());
         }
         // 5. Validate strictness.
         if ($strictness === MUST_EXIST && $result === false) {
@@ -1889,7 +1889,7 @@ class cache_session extends cache {
             $loader->set($key, $data);
         }
         if ($this->perfdebug) {
-            cache_helper::record_cache_set($this->storetype, $this->get_definition());
+            cache_helper::record_cache_set($this->get_store(), $this->get_definition());
         }
         if (is_object($data) && $data instanceof cacheable_object) {
             $data = new cache_cached_object($data);
@@ -2019,8 +2019,8 @@ class cache_session extends cache {
                     $hits++;
                 }
             }
-            cache_helper::record_cache_hit($this->storetype, $this->get_definition(), $hits);
-            cache_helper::record_cache_miss($this->storetype, $this->get_definition(), $misses);
+            cache_helper::record_cache_hit($this->get_store(), $this->get_definition(), $hits);
+            cache_helper::record_cache_miss($this->get_store(), $this->get_definition(), $misses);
         }
         return $return;
 
@@ -2097,7 +2097,7 @@ class cache_session extends cache {
         }
         $successfullyset = $this->get_store()->set_many($data);
         if ($this->perfdebug && $successfullyset) {
-            cache_helper::record_cache_set($this->storetype, $this->get_definition(), $successfullyset);
+            cache_helper::record_cache_set($this->store, $this->get_definition(), $successfullyset);
         }
         return $successfullyset;
     }
index 4fcb03f..a2cfe3e 100644 (file)
@@ -144,6 +144,10 @@ abstract class cache_store implements cache_store_interface {
      * Request caches. Static caches really.
      */
     const MODE_REQUEST = 4;
+    /**
+     * Static caches.
+     */
+    const STATIC_ACCEL = '** static accel. **';
 
     /**
      * Constructs an instance of the cache store.
index 9c0f1d9..0e6b203 100644 (file)
@@ -2092,15 +2092,15 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertFalse($request->get('missMe'));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(1, $endstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(1, $endstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2116,24 +2116,24 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertTrue($request->set('setMe4', 4));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2149,24 +2149,24 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertEquals($request->get('setMe4'), 4);
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2176,24 +2176,24 @@ class core_cache_testcase extends advanced_testcase {
         $request->get_many(array('setMe1', 'setMe2', 'setMe3', 'setMe4'));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
     }
 
     public function test_static_cache() {
@@ -2225,8 +2225,8 @@ class core_cache_testcase extends advanced_testcase {
 
         // Check that the static acceleration worked, even on empty arrays and the number 0.
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['** static acceleration **']['misses']);
-        $this->assertEquals(3, $endstats[$applicationid]['stores']['** static acceleration **']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['** static accel. **']['misses']);
+        $this->assertEquals(3, $endstats[$applicationid]['stores']['** static accel. **']['hits']);
     }
 
     public function test_performance_debug_off() {
index eaf6344..076bd53 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /cache/stores/* - cache store plugins.
 Information provided here is intended especially for developers.
 
+=== 3.9 ===
+* The record_cache_hit/miss/set methods now take a cache_store instead of a cache_definition object
+
 === 3.8 ===
 * The Redis cache store can now make use of the Zstandard compression algorithm (see MDL-66428).
 
index 9a3f1d0..3b08693 100644 (file)
@@ -13,7 +13,7 @@
     "require-dev": {
         "phpunit/phpunit": "7.5.*",
         "phpunit/dbunit": "4.0.*",
-        "moodlehq/behat-extension": "3.39.2",
+        "moodlehq/behat-extension": "3.39.3",
         "mikey179/vfsstream": "^1.6",
         "instaclick/php-webdriver": "dev-local as 1.x-dev"
     }
index 7fd4234..11dbff6 100644 (file)
@@ -4,42 +4,44 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "a2329ee2d14a351b74f99322f42722da",
+    "content-hash": "b1953ceec577434625a7aee12f650daa",
     "packages": [],
     "packages-dev": [
         {
             "name": "behat/behat",
-            "version": "v3.5.0",
+            "version": "v3.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Behat.git",
-                "reference": "e4bce688be0c2029dc1700e46058d86428c63cab"
+                "reference": "9bfe195b4745c32e068af03fa4df9558b4916d30"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Behat/zipball/e4bce688be0c2029dc1700e46058d86428c63cab",
-                "reference": "e4bce688be0c2029dc1700e46058d86428c63cab",
+                "url": "https://api.github.com/repos/Behat/Behat/zipball/9bfe195b4745c32e068af03fa4df9558b4916d30",
+                "reference": "9bfe195b4745c32e068af03fa4df9558b4916d30",
                 "shasum": ""
             },
             "require": {
-                "behat/gherkin": "^4.5.1",
+                "behat/gherkin": "^4.6.0",
                 "behat/transliterator": "^1.2",
                 "container-interop/container-interop": "^1.2",
                 "ext-mbstring": "*",
                 "php": ">=5.3.3",
                 "psr/container": "^1.0",
-                "symfony/class-loader": "~2.1||~3.0",
-                "symfony/config": "~2.3||~3.0||~4.0",
-                "symfony/console": "~2.7.40||^2.8.33||~3.3.15||^3.4.3||^4.0.3",
-                "symfony/dependency-injection": "~2.1||~3.0||~4.0",
-                "symfony/event-dispatcher": "~2.1||~3.0||~4.0",
-                "symfony/translation": "~2.3||~3.0||~4.0",
-                "symfony/yaml": "~2.1||~3.0||~4.0"
+                "symfony/config": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/console": "^2.7.51 || ^2.8.33 || ^3.3.15 || ^3.4.3 || ^4.0.3 || ^5.0",
+                "symfony/dependency-injection": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/event-dispatcher": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/translation": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/yaml": "^2.7.51 || ^3.0 || ^4.0 || ^5.0"
             },
             "require-dev": {
                 "herrera-io/box": "~1.6.1",
-                "phpunit/phpunit": "^4.8.36|^6.3",
-                "symfony/process": "~2.5|~3.0|~4.0"
+                "phpunit/phpunit": "^4.8.36 || ^6.3",
+                "symfony/process": "~2.5 || ^3.0 || ^4.0 || ^5.0"
+            },
+            "suggest": {
+                "ext-dom": "Needed to output test results in JUnit format."
             },
             "bin": [
                 "bin/behat"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.5.x-dev"
+                    "dev-master": "3.6.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Behat\\Behat": "src/",
-                    "Behat\\Testwork": "src/"
+                "psr-4": {
+                    "Behat\\Behat\\": "src/Behat/Behat/",
+                    "Behat\\Testwork\\": "src/Behat/Testwork/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -83,7 +85,7 @@
                 "symfony",
                 "testing"
             ],
-            "time": "2018-08-10T18:56:51+00:00"
+            "time": "2020-02-06T09:54:48+00:00"
         },
         {
             "name": "behat/gherkin",
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.5.2",
+            "version": "6.5.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82"
+                "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82",
-                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aab4ebd862aa7d04f01a4b51849d657db56d882e",
+                "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "guzzlehttp/promises": "^1.0",
                 "guzzlehttp/psr7": "^1.6.1",
-                "php": ">=5.5"
+                "php": ">=5.5",
+                "symfony/polyfill-intl-idn": "^1.11"
             },
             "require-dev": {
                 "ext-curl": "*",
                 "psr/log": "^1.1"
             },
             "suggest": {
-                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
                 "psr/log": "Required for using the Log middleware"
             },
             "type": "library",
                 "rest",
                 "web service"
             ],
-            "time": "2019-12-23T11:57:10+00:00"
+            "time": "2020-04-18T10:38:46+00:00"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.39.2",
+            "version": "v3.39.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "7a2df2124ba8a85ccf21e517d18c78f932bdbbce"
+                "reference": "d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/7a2df2124ba8a85ccf21e517d18c78f932bdbbce",
-                "reference": "7a2df2124ba8a85ccf21e517d18c78f932bdbbce",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b",
+                "reference": "d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b",
                 "shasum": ""
             },
             "require": {
-                "behat/behat": "3.5.*",
+                "behat/behat": "3.6.*",
                 "behat/mink": "~1.8",
-                "behat/mink-extension": "~2.2",
+                "behat/mink-extension": "~2.3",
                 "behat/mink-goutte-driver": "~1.2",
-                "behat/mink-selenium2-driver": "~1.3",
+                "behat/mink-selenium2-driver": "~1.4",
                 "php": ">=7.2.0",
-                "symfony/process": "2.8.*"
+                "symfony/process": "^4.0 || ^5.0"
             },
             "type": "library",
             "autoload": {
                 "Behat",
                 "moodle"
             ],
-            "time": "2020-04-09T16:06:14+00:00"
+            "time": "2020-04-20T09:32:44+00:00"
         },
         {
             "name": "myclabs/deep-copy",
             "time": "2017-02-14T16:28:37+00:00"
         },
         {
-            "name": "psr/http-message",
-            "version": "1.0.1",
+            "name": "psr/event-dispatcher",
+            "version": "1.0.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/php-fig/http-message.git",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+                "url": "https://github.com/php-fig/event-dispatcher.git",
+                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.0"
+                "php": ">=7.2.0"
             },
             "type": "library",
             "extra": {
             },
             "autoload": {
                 "psr-4": {
-                    "Psr\\Http\\Message\\": "src/"
+                    "Psr\\EventDispatcher\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "homepage": "http://www.php-fig.org/"
                 }
             ],
-            "description": "Common interface for HTTP messages",
-            "homepage": "https://github.com/php-fig/http-message",
+            "description": "Standard interfaces for event handling.",
             "keywords": [
-                "http",
-                "http-message",
+                "events",
                 "psr",
-                "psr-7",
-                "request",
-                "response"
+                "psr-14"
             ],
-            "time": "2016-08-06T14:39:51+00:00"
+            "time": "2019-01-08T18:20:26+00:00"
         },
         {
-            "name": "psr/log",
-            "version": "1.1.3",
+            "name": "psr/http-message",
+            "version": "1.0.1",
             "source": {
                 "type": "git",
-                "url": "https://github.com/php-fig/log.git",
-                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
-                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1.x-dev"
+                    "dev-master": "1.0.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Psr\\Log\\": "Psr/Log/"
+                    "Psr\\Http\\Message\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "homepage": "http://www.php-fig.org/"
                 }
             ],
-            "description": "Common interface for logging libraries",
-            "homepage": "https://github.com/php-fig/log",
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
             "keywords": [
-                "log",
+                "http",
+                "http-message",
                 "psr",
-                "psr-3"
+                "psr-7",
+                "request",
+                "response"
             ],
-            "time": "2020-03-23T09:12:05+00:00"
+            "time": "2016-08-06T14:39:51+00:00"
         },
         {
             "name": "ralouphie/getallheaders",
             ],
             "time": "2020-03-28T10:15:50+00:00"
         },
-        {
-            "name": "symfony/class-loader",
-            "version": "v3.4.39",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/class-loader.git",
-                "reference": "e4636a4f23f157278a19e5db160c63de0da297d8"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/e4636a4f23f157278a19e5db160c63de0da297d8",
-                "reference": "e4636a4f23f157278a19e5db160c63de0da297d8",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.5.9|>=7.0.8"
-            },
-            "require-dev": {
-                "symfony/finder": "~2.8|~3.0|~4.0",
-                "symfony/polyfill-apcu": "~1.1"
-            },
-            "suggest": {
-                "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.4-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\ClassLoader\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony ClassLoader Component",
-            "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-03-15T09:38:08+00:00"
-        },
         {
             "name": "symfony/config",
             "version": "v4.4.7",
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
             "time": "2020-03-27T16:54:36+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.18",
+            "version": "v5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "af7ec995de93671c03cc1b4e3176c8588bc79dcc"
+                "reference": "5fa1caadc8cdaa17bcfb25219f3b53fe294a9935"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/af7ec995de93671c03cc1b4e3176c8588bc79dcc",
-                "reference": "af7ec995de93671c03cc1b4e3176c8588bc79dcc",
+                "url": "https://api.github.com/repos/symfony/console/zipball/5fa1caadc8cdaa17bcfb25219f3b53fe294a9935",
+                "reference": "5fa1caadc8cdaa17bcfb25219f3b53fe294a9935",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "symfony/debug": "~2.8|~3.0",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.2.5",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php73": "^1.8",
+                "symfony/service-contracts": "^1.1|^2"
             },
             "conflict": {
-                "symfony/dependency-injection": "<3.3"
+                "symfony/dependency-injection": "<4.4",
+                "symfony/event-dispatcher": "<4.4",
+                "symfony/lock": "<4.4",
+                "symfony/process": "<4.4"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~3.3",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/event-dispatcher": "~2.8|~3.0",
-                "symfony/filesystem": "~2.8|~3.0",
-                "symfony/process": "~2.8|~3.0"
+                "symfony/config": "^4.4|^5.0",
+                "symfony/dependency-injection": "^4.4|^5.0",
+                "symfony/event-dispatcher": "^4.4|^5.0",
+                "symfony/lock": "^4.4|^5.0",
+                "symfony/process": "^4.4|^5.0",
+                "symfony/var-dumper": "^4.4|^5.0"
             },
             "suggest": {
                 "psr/log": "For using the console logger",
                 "symfony/event-dispatcher": "",
-                "symfony/filesystem": "",
+                "symfony/lock": "",
                 "symfony/process": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-29T09:02:23+00:00"
+            "time": "2020-03-30T11:42:42+00:00"
         },
         {
             "name": "symfony/css-selector",
             ],
             "time": "2020-03-27T16:56:45+00:00"
         },
-        {
-            "name": "symfony/debug",
-            "version": "v3.4.39",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/debug.git",
-                "reference": "ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29",
-                "reference": "ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "psr/log": "~1.0"
-            },
-            "conflict": {
-                "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
-            },
-            "require-dev": {
-                "symfony/http-kernel": "~2.8|~3.0|~4.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.4-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Debug\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony Debug Component",
-            "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-03-23T10:22:40+00:00"
-        },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.18",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "54243abc4e1a1a15e274e391bd6f7090b44711f1"
+                "reference": "755b18859be26b90f4bf63753432d3387458bf31"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/54243abc4e1a1a15e274e391bd6f7090b44711f1",
-                "reference": "54243abc4e1a1a15e274e391bd6f7090b44711f1",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/755b18859be26b90f4bf63753432d3387458bf31",
+                "reference": "755b18859be26b90f4bf63753432d3387458bf31",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "psr/container": "^1.0"
+                "php": "^7.1.3",
+                "psr/container": "^1.0",
+                "symfony/service-contracts": "^1.1.6|^2"
             },
             "conflict": {
-                "symfony/config": "<3.3.7",
-                "symfony/finder": "<3.3",
-                "symfony/yaml": "<3.3"
+                "symfony/config": "<4.3|>=5.0",
+                "symfony/finder": "<3.4",
+                "symfony/proxy-manager-bridge": "<3.4",
+                "symfony/yaml": "<3.4"
             },
             "provide": {
-                "psr/container-implementation": "1.0"
+                "psr/container-implementation": "1.0",
+                "symfony/service-implementation": "1.0"
             },
             "require-dev": {
-                "symfony/config": "~3.3",
-                "symfony/expression-language": "~2.8|~3.0",
-                "symfony/yaml": "~3.3"
+                "symfony/config": "^4.3",
+                "symfony/expression-language": "^3.4|^4.0|^5.0",
+                "symfony/yaml": "^3.4|^4.0|^5.0"
             },
             "suggest": {
                 "symfony/config": "",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.4-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-29T09:02:23+00:00"
+            "time": "2020-03-30T10:09:30+00:00"
         },
         {
             "name": "symfony/dom-crawler",
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.39",
+            "version": "v5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "9d4e22943b73acc1ba50595b7de1a01fe9dbad48"
+                "reference": "24f40d95385774ed5c71dbf014edd047e2f2f3dc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9d4e22943b73acc1ba50595b7de1a01fe9dbad48",
-                "reference": "9d4e22943b73acc1ba50595b7de1a01fe9dbad48",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/24f40d95385774ed5c71dbf014edd047e2f2f3dc",
+                "reference": "24f40d95385774ed5c71dbf014edd047e2f2f3dc",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^7.2.5",
+                "symfony/event-dispatcher-contracts": "^2"
             },
             "conflict": {
-                "symfony/dependency-injection": "<3.3"
+                "symfony/dependency-injection": "<4.4"
+            },
+            "provide": {
+                "psr/event-dispatcher-implementation": "1.0",
+                "symfony/event-dispatcher-implementation": "2.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0|~4.0",
-                "symfony/dependency-injection": "~3.3|~4.0",
-                "symfony/expression-language": "~2.8|~3.0|~4.0",
-                "symfony/stopwatch": "~2.8|~3.0|~4.0"
+                "symfony/config": "^4.4|^5.0",
+                "symfony/dependency-injection": "^4.4|^5.0",
+                "symfony/expression-language": "^4.4|^5.0",
+                "symfony/http-foundation": "^4.4|^5.0",
+                "symfony/service-contracts": "^1.1|^2",
+                "symfony/stopwatch": "^4.4|^5.0"
             },
             "suggest": {
                 "symfony/dependency-injection": "",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.4-dev"
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-03-15T09:38:08+00:00"
+            "time": "2020-03-27T16:56:45+00:00"
+        },
+        {
+            "name": "symfony/event-dispatcher-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+                "reference": "af23c2584d4577d54661c434446fb8fbed6025dd"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/af23c2584d4577d54661c434446fb8fbed6025dd",
+                "reference": "af23c2584d4577d54661c434446fb8fbed6025dd",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5",
+                "psr/event-dispatcher": "^1"
+            },
+            "suggest": {
+                "symfony/event-dispatcher-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\EventDispatcher\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to dispatching event",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
         },
         {
             "name": "symfony/filesystem",
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
             "time": "2020-03-27T16:56:45+00:00"
         },
         {
             "time": "2020-02-27T09:26:54+00:00"
         },
         {
-            "name": "symfony/polyfill-mbstring",
+            "name": "symfony/polyfill-intl-idn",
             "version": "v1.15.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
+                "url": "https://github.com/symfony/polyfill-intl-idn.git",
+                "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
-                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
+                "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": ">=5.3.3",
+                "symfony/polyfill-mbstring": "^1.3",
+                "symfony/polyfill-php72": "^1.10"
             },
             "suggest": {
-                "ext-mbstring": "For best performance"
+                "ext-intl": "For best performance"
             },
             "type": "library",
             "extra": {
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Polyfill\\Mbstring\\": ""
+                    "Symfony\\Polyfill\\Intl\\Idn\\": ""
                 },
                 "files": [
                     "bootstrap.php"
             ],
             "authors": [
                 {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
+                    "name": "Laurent Bassin",
+                    "email": "laurent@bassin.info"
                 },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony polyfill for the Mbstring extension",
+            "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
             "homepage": "https://symfony.com",
             "keywords": [
                 "compatibility",
-                "mbstring",
+                "idn",
+                "intl",
                 "polyfill",
                 "portable",
                 "shim"
             "time": "2020-03-09T19:04:49+00:00"
         },
         {
-            "name": "symfony/process",
-            "version": "v2.8.52",
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.15.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/process.git",
-                "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8"
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.15-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-09T19:04:49+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php72",
+            "version": "v1.15.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php72.git",
+                "reference": "37b0976c78b94856543260ce09b460a7bc852747"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747",
+                "reference": "37b0976c78b94856543260ce09b460a7bc852747",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.15-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php72\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-02-27T09:26:54+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php73",
+            "version": "v1.15.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php73.git",
+                "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8",
-                "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8",
+                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
+                "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.9"
+                "php": ">=5.3.3"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.8-dev"
+                    "dev-master": "1.15-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php73\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-02-27T09:26:54+00:00"
+        },
+        {
+            "name": "symfony/process",
+            "version": "v5.0.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/process.git",
+                "reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/process/zipball/c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e",
+                "reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-11T11:18:13+00:00"
+            "time": "2020-03-27T16:56:45+00:00"
+        },
+        {
+            "name": "symfony/service-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/service-contracts.git",
+                "reference": "144c5e51266b281231e947b51223ba14acf1a749"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749",
+                "reference": "144c5e51266b281231e947b51223ba14acf1a749",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5",
+                "psr/container": "^1.0"
+            },
+            "suggest": {
+                "symfony/service-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Service\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to writing services",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.18",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "90cb5ca3eb84b3053fef876e11e405fd819487fc"
+                "reference": "4e54d336f2eca5facad449d0b0118bb449375b76"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/90cb5ca3eb84b3053fef876e11e405fd819487fc",
-                "reference": "90cb5ca3eb84b3053fef876e11e405fd819487fc",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/4e54d336f2eca5facad449d0b0118bb449375b76",
+                "reference": "4e54d336f2eca5facad449d0b0118bb449375b76",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/translation-contracts": "^1.1.6|^2"
             },
             "conflict": {
-                "symfony/config": "<2.8",
-                "symfony/yaml": "<3.3"
+                "symfony/config": "<3.4",
+                "symfony/dependency-injection": "<3.4",
+                "symfony/http-kernel": "<4.4",
+                "symfony/yaml": "<3.4"
+            },
+            "provide": {
+                "symfony/translation-implementation": "1.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/intl": "^2.8.18|^3.2.5",
-                "symfony/yaml": "~3.3"
+                "symfony/config": "^3.4|^4.0|^5.0",
+                "symfony/console": "^3.4|^4.0|^5.0",
+                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+                "symfony/finder": "~2.8|~3.0|~4.0|^5.0",
+                "symfony/http-kernel": "^4.4",
+                "symfony/intl": "^3.4|^4.0|^5.0",
+                "symfony/service-contracts": "^1.1.2|^2",
+                "symfony/yaml": "^3.4|^4.0|^5.0"
             },
             "suggest": {
-                "psr/log": "To use logging capability in translator",
+                "psr/log-implementation": "To use logging capability in translator",
                 "symfony/config": "",
                 "symfony/yaml": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.4-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-18T14:19:00+00:00"
+            "time": "2020-03-27T16:54:36+00:00"
+        },
+        {
+            "name": "symfony/translation-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/translation-contracts.git",
+                "reference": "8cc682ac458d75557203b2f2f14b0b92e1c744ed"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/8cc682ac458d75557203b2f2f14b0b92e1c744ed",
+                "reference": "8cc682ac458d75557203b2f2f14b0b92e1c744ed",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5"
+            },
+            "suggest": {
+                "symfony/translation-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Translation\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to translation",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.18",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "af615970e265543a26ee712c958404eb9b7ac93d"
+                "reference": "ef166890d821518106da3560086bfcbeb4fadfec"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/af615970e265543a26ee712c958404eb9b7ac93d",
-                "reference": "af615970e265543a26ee712c958404eb9b7ac93d",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/ef166890d821518106da3560086bfcbeb4fadfec",
+                "reference": "ef166890d821518106da3560086bfcbeb4fadfec",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^7.1.3",
+                "symfony/polyfill-ctype": "~1.8"
+            },
+            "conflict": {
+                "symfony/console": "<3.4"
             },
             "require-dev": {
-                "symfony/console": "~2.8|~3.0"
+                "symfony/console": "^3.4|^4.0|^5.0"
             },
             "suggest": {
                 "symfony/console": "For validating YAML files using the lint command"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.4-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-20T15:04:53+00:00"
+            "time": "2020-03-30T11:41:10+00:00"
         },
         {
             "name": "theseer/tokenizer",
         },
         {
             "name": "webmozart/assert",
-            "version": "1.7.0",
+            "version": "1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "aed98a490f9a8f78468232db345ab9cf606cf598"
+                "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598",
-                "reference": "aed98a490f9a8f78468232db345ab9cf606cf598",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
+                "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
                 "shasum": ""
             },
             "require": {
                 "symfony/polyfill-ctype": "^1.8"
             },
             "conflict": {
-                "vimeo/psalm": "<3.6.0"
+                "vimeo/psalm": "<3.9.1"
             },
             "require-dev": {
                 "phpunit/phpunit": "^4.8.36 || ^7.5.13"
                 "check",
                 "validate"
             ],
-            "time": "2020-02-14T12:15:55+00:00"
+            "time": "2020-04-18T12:12:48+00:00"
         }
     ],
     "aliases": [
index 10ea721..179e80f 100644 (file)
@@ -25,5 +25,5 @@
 $string['pluginname'] = 'H5P';
 $string['pluginname_help'] = 'Content bank to upload and share H5P content';
 $string['privacy:metadata'] = 'The H5P content bank plugin does not store any personal data.';
-$string['h5p:access'] = 'Access to H5P content in the content bank';
-$string['h5p:upload'] = 'Upload new H5P content';
+$string['h5p:access'] = 'Access H5P content in the content bank';
+$string['h5p:upload'] = 'Upload new H5P content';
index 8d03118..61f28ea 100644 (file)
@@ -48,7 +48,7 @@ Feature: H5P file upload to content bank for admins
     And I click on "Save changes" "button"
     And I wait until the page is ready
     And I should see "filltheblanks.h5p"
-    And I navigate to "Plugins > Content bank > Manage content bank content types" in site administration
+    And I navigate to "Plugins > Content bank > Manage content types" in site administration
     And I click on "Disable" "icon" in the "H5P" "table_row"
     And I wait until the page is ready
     When I click on "Content bank" "link"
index 7280b1b..b49a79c 100644 (file)
@@ -31,7 +31,7 @@ Feature: Delete H5P file from the content bank
     When I open the action menu in "region-main-settings-menu" "region"
     Then I should see "Delete"
     And I choose "Delete" in the open action menu
-    And I should see "Are you sure you want to delete content 'filltheblanks.h5p'?"
+    And I should see "Are you sure you want to delete the content 'filltheblanks.h5p'"
     And I click on "Cancel" "button" in the "Delete content" "dialogue"
     And I should see "filltheblanks.h5p"
     And I open the action menu in "region-main-settings-menu" "region"
index 14f20e1..98ef06a 100644 (file)
@@ -18,13 +18,13 @@ Feature: Display and choose from the available activities in course
     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"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
+    Then I should see "Add an activity" 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"
+    Given I click on "Add an activity" "button" in the "Topic 3" "section"
+    When I click on "Add a new Assignment" "link" in the "Add an activity" "dialogue"
     Then I should see "Adding a new Assignment"
     And I set the following fields to these values:
       | Assignment name | Test Assignment Topic 3 |
@@ -32,19 +32,19 @@ Feature: Display and choose from the available activities in 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"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
+    When I click on "Information about the Assignment activity" "button" in the "Add an activity" "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"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
+    When I click on "Information about the Assignment activity" "button" in the "Add an activity" "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"
+    Given I click on "Add an activity" "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"
@@ -52,7 +52,7 @@ Feature: Display and choose from the available activities in course
     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"
+    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" "dialogue"
 
   Scenario: View recommended activities
     When I log out
@@ -65,85 +65,85 @@ Feature: Display and choose from the available activities in course
     And I log in as "teacher"
     And I am on "Course" course homepage with editing mode on
     And I open the activity chooser
-    Then I should see "Recommended" in the "Add an activity or resource" "dialogue"
-    And I click on "Recommended" "link" in the "Add an activity or resource" "dialogue"
+    Then I should see "Recommended" in the "Add an activity" "dialogue"
+    And I click on "Recommended" "link" in the "Add an activity" "dialogue"
     And I should see "Book" in the "recommended" "core_course > Activity chooser tab"
 
   Scenario: Favourite a module in the activity chooser
     Given I open the activity chooser
-    And I should not see "Starred" in the "Add an activity or resource" "dialogue"
-    And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
-    And I should see "Starred" in the "Add an activity or resource" "dialogue"
-    When I click on "Starred" "link" in the "Add an activity or resource" "dialogue"
+    And I should not see "Starred" in the "Add an activity" "dialogue"
+    And I click on "Star Assignment activity" "button" in the "Add an activity" "dialogue"
+    And I should see "Starred" in the "Add an activity" "dialogue"
+    When I click on "Starred" "link" in the "Add an activity" "dialogue"
     Then I should see "Assignment" in the "favourites" "core_course > Activity chooser tab"
     And I click on "Information about the Assignment activity" "button" in the "favourites" "core_course > Activity chooser tab"
     And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback."
 
   Scenario: Add a favourite module and check it exists when reopening the chooser
     Given I open the activity chooser
-    And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
-    And I click on "Star Forum module" "button" in the "Add an activity or resource" "dialogue"
-    And I should see "Starred" in the "Add an activity or resource" "dialogue"
-    And I click on "Close" "button" in the "Add an activity or resource" "dialogue"
-    When I click on "Add an activity or resource" "button" in the "Topic 3" "section"
-    And I click on "Starred" "link" in the "Add an activity or resource" "dialogue"
+    And I click on "Star Assignment activity" "button" in the "Add an activity" "dialogue"
+    And I click on "Star Forum activity" "button" in the "Add an activity" "dialogue"
+    And I should see "Starred" in the "Add an activity" "dialogue"
+    And I click on "Close" "button" in the "Add an activity" "dialogue"
+    When I click on "Add an activity" "button" in the "Topic 3" "section"
+    And I click on "Starred" "link" in the "Add an activity" "dialogue"
     Then I should see "Forum" in the "favourites" "core_course > Activity chooser tab"
 
   Scenario: Add a favourite and then remove it whilst checking the tabs work as expected
     Given I open the activity chooser
-    And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
-    And I click on "Starred" "link" in the "Add an activity or resource" "dialogue"
-    And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
-    Then I should not see "Starred" in the "Add an activity or resource" "dialogue"
+    And I click on "Star Assignment activity" "button" in the "Add an activity" "dialogue"
+    And I click on "Starred" "link" in the "Add an activity" "dialogue"
+    And I click on "Star Assignment activity" "button" in the "Add an activity" "dialogue"
+    Then I should not see "Starred" in the "Add an activity" "dialogue"
 
   Scenario: The teacher can search for an activity by it's name
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     When I set the field "search" to "Lesson"
-    Then I should see "1 results found" in the "Add an activity or resource" "dialogue"
-    And I should see "Lesson" in the "Add an activity or resource" "dialogue"
+    Then I should see "1 results found" in the "Add an activity" "dialogue"
+    And I should see "Lesson" in the "Add an activity" "dialogue"
 
   Scenario: The teacher can search for an activity by it's description
     Given I open the activity chooser
     When I set the field "search" to "The lesson activity module enables a teacher to deliver content"
-    Then I should see "1 results found" in the "Add an activity or resource" "dialogue"
-    And I should see "Lesson" in the "Add an activity or resource" "dialogue"
+    Then I should see "1 results found" in the "Add an activity" "dialogue"
+    And I should see "Lesson" in the "Add an activity" "dialogue"
 
   Scenario: Search results are not returned if the search query does not match any activity name or description
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     When I set the field "search" to "Random search query"
-    Then I should see "0 results found" in the "Add an activity or resource" "dialogue"
+    Then I should see "0 results found" in the "Add an activity" "dialogue"
     And ".option" "css_element" should not exist in the ".searchresultitemscontainer" "css_element"
 
   Scenario: Teacher can return to the default activity chooser state by manually removing the search query
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     And I set the field "search" to "Lesson"
-    And I should see "1 results found" in the "Add an activity or resource" "dialogue"
-    And I should see "Lesson" in the "Add an activity or resource" "dialogue"
+    And I should see "1 results found" in the "Add an activity" "dialogue"
+    And I should see "Lesson" in the "Add an activity" "dialogue"
     When I set the field "search" to ""
-    And I should not see "1 results found" in the "Add an activity or resource" "dialogue"
+    And I should not see "1 results found" in the "Add an activity" "dialogue"
     Then ".searchresultscontainer" "css_element" should not exist
     And ".optionscontainer" "css_element" should exist
 
   Scenario: Teacher can not see a "clear" button if a search query is not entered in the activity chooser search bar
-    When I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    When I click on "Add an activity" "button" in the "Topic 1" "section"
     Then "Clear search input" "button" should not exist
 
   Scenario: Teacher can see a "clear" button after entering a search query in the activity chooser search bar
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     When I set the field "search" to "Search query"
     Then "Clear search input" "button" should not exist
 
   Scenario: Teacher can not see a "clear" button if the search query is removed in the activity chooser search bar
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     And I set the field "search" to "Search query"
     And "Clear search input" "button" should exist
     When I set the field "search" to ""
     Then "Clear search input" "button" should not exist
 
   Scenario: Teacher can instantly remove the search query from the activity search bar by clicking on the "clear" button
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     And I set the field "search" to "Search query"
-    And I should see "results found" in the "Add an activity or resource" "dialogue"
+    And I should see "results found" in the "Add an activity" "dialogue"
     When I click on "Clear search input" "button"
     Then I should not see "Search query"
     And ".searchresultscontainer" "css_element" should not exist
index 6c9aebd..319ef65 100644 (file)
@@ -74,7 +74,7 @@ $string['insertcomment'] = 'Insert frequently used comment';
 $string['maxscore'] = 'Maximum score';
 $string['name'] = 'Name';
 $string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';
-$string['outof'] = 'Out of {$a}';
+$string['outof'] = 'Score out of {$a}';
 $string['pluginname'] = 'Marking guide';
 $string['previewmarkingguide'] = 'Preview marking guide';
 $string['privacy:metadata:criterionid'] = 'An identifier to a criterion for advanced marking.';
index ca5ea1a..222f128 100644 (file)
@@ -25,5 +25,5 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['pluginname'] = 'H5P framework v1.24';
-$string['pluginname_help'] = 'H5P framework. Version 1.24';
-$string['privacy:metadata'] = 'H5P framework v1.24 do not store any personal data.';
+$string['pluginname_help'] = 'H5P framework version 1.24.';
+$string['privacy:metadata'] = 'The H5P framework v1.24 does not store any personal data.';
index e86452b..9370d23 100644 (file)
@@ -329,7 +329,7 @@ $string['configrequestedstudentname'] = 'Word for student used in requested cour
 $string['configrequestedstudentsname'] = 'Word for students used in requested courses';
 $string['configrequestedteachername'] = 'Word for teacher used in requested courses';
 $string['configrequestedteachersname'] = 'Word for teachers used in requested courses';
-$string['configreverseproxyignore'] = 'If your server is behind multiple reverse proxies that append to the X-Forwarded-For header then you will need to specify a comma separated list of ip addresses or subnets of the reverse proxies to be ignored in order to find the users correct IP address.';
+$string['configreverseproxyignore'] = 'If your server is behind multiple reverse proxies that append to the X-Forwarded-For header, then specify a comma-separated list of IP addresses or subnets of the reverse proxies to be ignored in order to find the user\'s correct IP address.';
 $string['configsectioninterface'] = 'Interface';
 $string['configsectionmail'] = 'Mail';
 $string['configsectionmaintenance'] = 'Maintenance';
@@ -1218,10 +1218,10 @@ $string['task_scheduled_concurrency_limit'] = 'Scheduled task concurrency limit'
 $string['task_scheduled_concurrency_limit_desc'] = 'The number of scheduled task runners allowed to run concurrently. If the limit is high then the server may experience high load which affects performance. A setting of 0 will disable processing of scheduled tasks completely.';
 $string['task_scheduled_max_runtime'] = 'Scheduled task runner lifetime';
 $string['task_scheduled_max_runtime_desc'] = 'The age of a scheduled task runner before it is freed.';
-$string['task_adhoc_concurrency_limit'] = 'Adhoc task concurrency limit';
-$string['task_adhoc_concurrency_limit_desc'] = 'The number of adhoc task runners allowed to run concurrently. If the limit is high then scheduled tasks may not run regularly when there are lots of adhoc tasks. A setting of 0 will disable processing of adhoc tasks completely.';
-$string['task_adhoc_max_runtime'] = 'Adhoc task runner lifetime';
-$string['task_adhoc_max_runtime_desc'] = 'The age of an adhoc task runner before it is freed. A low duration is recommended as there is no limit to the number of adhoc tasks queued. If this number is too high and you have a large adhoc task queue then scheduled tasks may not be run regularly.';
+$string['task_adhoc_concurrency_limit'] = 'Ad hoc task concurrency limit';
+$string['task_adhoc_concurrency_limit_desc'] = 'The number of ad hoc task runners allowed to run concurrently. If the limit is high then scheduled tasks may not run regularly when there are lots of ad hoc tasks. A setting of 0 will disable processing of ad hoc tasks completely.';
+$string['task_adhoc_max_runtime'] = 'Ad hoc task runner lifetime';
+$string['task_adhoc_max_runtime_desc'] = 'The age of an ad hoc task runner before it is freed. A low duration is recommended as there is no limit to the number of ad hoc tasks queued. If this number is too high and you have a large ad hoc task queue then scheduled tasks may not be run regularly.';
 $string['task_logmode'] = 'When to log';
 $string['task_logmode_desc'] = 'You can choose when you wish task logging to take place. By default logs are always captured. You can disable logging entirely, or change to only log tasks which fail.';
 $string['task_logmode_none'] = 'Do not log anything';
index 862ed68..eb3dc7b 100644 (file)
@@ -81,7 +81,7 @@ $string['cachedef_string'] = 'Language string cache';
 $string['cachedef_tags'] = 'Tags collections and areas';
 $string['cachedef_temp_tables'] = 'Temporary tables cache';
 $string['cachedef_userselections'] = 'Data used to persist user selections throughout Moodle';
-$string['cachedef_user_favourite_course_content_items'] = 'User\'s favourite content items (activities, resources and their subtypes)';
+$string['cachedef_user_favourite_course_content_items'] = 'User\'s starred items';
 $string['cachedef_user_group_groupings'] = 'User\'s groupings and groups per course';
 $string['cachedef_user_course_content_items'] = 'User\'s content items (activities, resources and their subtypes) per course';
 $string['cachedef_yuimodules'] = 'YUI Module definitions';
index 80b39b0..4916a4d 100644 (file)
@@ -26,14 +26,14 @@ $string['author'] = 'Author';
 $string['contentdeleted'] = 'The content has been deleted.';
 $string['contentnotdeleted'] = 'An error was encountered while trying to delete the content.';
 $string['deletecontent'] = 'Delete content';
-$string['deletecontentconfirm'] = '<p>Are you sure you want to delete content <em>\'{$a->name}\'</em>? It will remove the content and all its files.</p><p>This operation can not be undone.</p>';
+$string['deletecontentconfirm'] = 'Are you sure you want to delete the content <em>\'{$a->name}\'</em> and all associated files? This action cannot be undone.';
 $string['file'] = 'Upload content';
 $string['file_help'] = 'Files may be stored in the content bank for use in courses. Only files used by content types enabled on the site may be uploaded.';
 $string['name'] = 'Content';
-$string['nopermissiontodelete'] = 'You have no permissions to delete the content.';
+$string['nopermissiontodelete'] = 'You do not have permission to delete content.';
 $string['privacy:metadata:userid'] = 'The ID of the user creating or modifying content bank content.';
-$string['privacy:metadata:content:usercreated'] = 'The user has created the content.';
-$string['privacy:metadata:content:usermodified'] = 'Last user has modified the content.';
+$string['privacy:metadata:content:usercreated'] = 'The user who created the content.';
+$string['privacy:metadata:content:usermodified'] = 'The last user who modified the content.';
 $string['timecreated'] = 'Time created';
-$string['unsupported'] = 'This content type is not supported';
+$string['unsupported'] = 'This content type is not supported.';
 $string['upload'] = 'Upload';
index c373515..c6a8d9a 100644 (file)
@@ -27,11 +27,11 @@ $string['aria:coursecategory'] = 'Course category';
 $string['aria:courseimage'] = 'Course image';
 $string['aria:courseshortname'] = 'Course short name';
 $string['aria:coursename'] = 'Course name';
-$string['aria:defaulttab'] = 'The default modules';
+$string['aria:defaulttab'] = 'Default activities';
 $string['aria:favourite'] = 'Course is starred';
-$string['aria:favouritestab'] = 'Your starred modules';
-$string['aria:recommendedtab'] = 'The recommended modules';
-$string['aria:modulefavourite'] = 'Star {$a} module';
+$string['aria:favouritestab'] = 'Starred activities';
+$string['aria:recommendedtab'] = 'Recommended activities';
+$string['aria:modulefavourite'] = 'Star {$a} activity';
 $string['coursealreadyfinished'] = 'Course already finished';
 $string['coursenotyetstarted'] = 'The course has not yet started';
 $string['coursenotyetfinished'] = 'The course has not yet finished';
@@ -48,7 +48,7 @@ $string['errorendbeforestart'] = 'The end date ({$a}) is before the course start
 $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['module'] = 'Module';
+$string['module'] = '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 9e70441..6fcb077 100644 (file)
@@ -64,6 +64,7 @@ $string['enrolmentnewuser'] = '{$a->user} has enrolled in course "{$a->course}"'
 $string['enrolmentmethod'] = 'Enrolment method';
 $string['enrolments'] = 'Enrolments';
 $string['enrolmentoptions'] = 'Enrolment options';
+$string['enrolmentupdatedforuser'] = 'The enrolment for user "{$a->fullname}" has been updated';
 $string['enrolnotpermitted'] = 'You do not have permission or are not allowed to enrol someone in this course';
 $string['enrolperiod'] = 'Enrolment duration';
 $string['enrolusage'] = 'Instances / enrolments';
@@ -136,6 +137,7 @@ $string['totalunenrolledusers'] = '{$a} unenrolled users';
 $string['totalotherusers'] = '{$a} other users';
 $string['unassignnotpermitted'] = 'You do not have permission to unassign roles in this course';
 $string['unenrol'] = 'Unenrol';
+$string['unenrolleduser'] = 'The user "{$a->fullname}" was unenrolled from the course';
 $string['unenrolconfirm'] = 'Do you really want to unenrol "{$a->user}" (previously enrolled via "{$a->enrolinstancename}") from "{$a->course}"?';
 $string['unenrolme'] = 'Unenrol me from {$a}';
 $string['unenrolnotpermitted'] = 'You do not have permission or can not unenrol this user from this course.';
index a1f5006..cf78c4c 100644 (file)
@@ -181,15 +181,15 @@ $string['commentmisconf'] = 'Comment ID is misconfigured';
 $string['componentisuptodate'] = 'Component is up-to-date';
 $string['confirmationnotenabled'] = 'User confirmation is not enabled on this site';
 $string['confirmsesskeybad'] = 'Sorry, but your session key could not be confirmed to carry out this action.  This security feature prevents against accidental or malicious execution of important functions in your name.  Please make sure you really wanted to execute this function.';
-$string['contenttypenotfound'] = 'The \'{$a}\' content bank type doesn\'t exist or is not recognized';
+$string['contenttypenotfound'] = 'The \'{$a}\' content bank type doesn\'t exist or is not recognised.';
 $string['couldnotassignrole'] = 'A serious but unspecified error occurred while trying to assign a role to you';
 $string['couldnotupdatenoexistinguser'] = 'Cannot update the user - user doesn\'t exist';
 $string['couldnotverifyagedigitalconsent'] = 'An error occurred while trying to verify the age of digital consent.<br />Please contact administrator.';
 $string['countriesphpempty'] = 'Error: The file countries.php in language pack {$a} is empty or missing.';
 $string['coursedoesnotbelongtocategory'] = 'The course doesn\'t belong to this category';
-$string['courseformatnotfound'] = 'The course format \'{$a}\' doesn\'t exist or is not recognized';
+$string['courseformatnotfound'] = 'The course format \'{$a}\' doesn\'t exist or is not recognised.';
 $string['coursegroupunknown'] = 'Course corresponding to group {$a} not specified';
-$string['courseidnotfound'] = 'Course id doesn\'t exist';
+$string['courseidnotfound'] = 'The course ID doesn\'t exist.';
 $string['courseidnumbertaken'] = 'ID number is already used for another course ({$a})';
 $string['coursemisconf'] = 'Course is misconfigured';
 $string['courserequestdisabled'] = 'Sorry, but course requests have been disabled by the administrator.';
index b9858b5..d640425 100644 (file)
@@ -152,7 +152,7 @@ $string['noextension'] = 'The file you uploaded is not a valid HTML5 Package. (I
 $string['noh5plibhandlerdefined'] = 'There isn\'t any H5P framework handler installed, so H5P content can\'t be displayed.';
 $string['nojson'] = 'The main h5p.json file is not valid';
 $string['nopermissiontodeploy'] = 'This file can\'t be displayed because it has been uploaded by a user without the required capability to deploy H5P content.';
-$string['nopermissiontoedit'] = 'You do not have permission to edit H5P content';
+$string['nopermissiontoedit'] = 'You do not have permission to edit H5P content.';
 $string['notrustablefile'] = 'This file can\'t be displayed because it has been uploaded by a user without the capability to update H5P content types.  Please contact your administrator to ask for the content type to be installed.';
 $string['nounzip'] = 'The file you uploaded is not a valid HTML5 Package. (It is not possible to unzip it.)';
 $string['offlineDialogBody'] = 'We were unable to send information about your completion of this task. Please check your internet connection.';
index eae72de..f0a53e9 100644 (file)
@@ -70,7 +70,7 @@ $string['addnewuser'] = 'Add a new user';
 $string['addnousersrecip'] = 'Add users who haven\'t accessed this {$a} to recipient list';
 $string['addpagehere'] = 'Add text here';
 $string['addresource'] = 'Add a resource...';
-$string['addresourceoractivity'] = 'Add an activity or resource';
+$string['addresourceoractivity'] = 'Add an activity';
 $string['addresourcetosection'] = 'Add a resource to section \'{$a}\'';
 $string['address'] = 'Address';
 $string['addsections'] = 'Add sections';
@@ -176,7 +176,7 @@ $string['backtohome'] = 'Back to the site home';
 $string['backtopageyouwereon'] = 'Back to the page you were on';
 $string['backup'] = 'Backup';
 $string['backupactivehelp'] = 'Choose whether or not to do automated backups.';
-$string['backupadhocpending'] = 'Course backup adhoc task pending';
+$string['backupadhocpending'] = 'Course backup ad hoc task pending';
 $string['backupcancelled'] = 'Backup cancelled';
 $string['backupcoursefileshelp'] = 'If enabled then course files will be included in automated backups';
 $string['backupdate'] = 'Backup date';
@@ -1002,7 +1002,7 @@ $string['changepassword'] = 'Change password';
 $string['changessaved'] = 'Changes saved';
 $string['check'] = 'Check';
 $string['checks'] = 'Checks';
-$string['checksok'] = 'All \'{$a}\' checks ok';
+$string['checksok'] = 'All \'{$a}\' checks OK';
 $string['checkall'] = 'Check all';
 $string['checkingbackup'] = 'Checking backup';
 $string['checkingcourse'] = 'Checking course';
@@ -1183,7 +1183,7 @@ $string['makethismyhome'] = 'Make this my home page';
 $string['makeunavailable'] = 'Make unavailable';
 $string['manageblocks'] = 'Blocks';
 $string['managecategorythis'] = 'Manage this category';
-$string['managecontentbanktypes'] = 'Manage content bank content types';
+$string['managecontentbanktypes'] = 'Manage content types';
 $string['managecourses'] = 'Manage courses';
 $string['managedataformats'] = 'Manage data formats';
 $string['managedatabase'] = 'Database';
@@ -1297,7 +1297,7 @@ $string['moodleversion'] = 'Moodle version';
 $string['moodlerelease'] = 'Moodle release';
 $string['more'] = 'more';
 $string['morehelp'] = 'More help';
-$string['morehelpaboutmodule'] = 'More help about the {$a} module';
+$string['morehelpaboutmodule'] = 'More help about the {$a} activity';
 $string['moreinfo'] = 'More info';
 $string['moreinformation'] = 'More information about this error';
 $string['moreprofileinfoneeded'] = 'Please tell us more about yourself';
@@ -1619,7 +1619,7 @@ $string['privacy:metadata:log:module'] = 'module';
 $string['privacy:metadata:log:time'] = 'The time when the action took place';
 $string['privacy:metadata:log:url'] = 'The URL related to the event';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action';
-$string['privacy:metadata:task_adhoc'] = 'The status of adhoc tasks.';
+$string['privacy:metadata:task_adhoc'] = 'The status of ad hoc tasks.';
 $string['privacy:metadata:task_adhoc:component'] = 'The component owning the task.';
 $string['privacy:metadata:task_adhoc:nextruntime'] = 'The earliest time to run this task.';
 $string['privacy:metadata:task_adhoc:userid'] = 'The user to run the task as.';
index bc6d094..7ed41e6 100644 (file)
@@ -188,8 +188,8 @@ $string['norepositoriesexternalavailable'] = 'Sorry, none of your current reposi
 $string['notyourinstances'] = 'You can not view/edit repository instances of another user';
 $string['off'] = 'Enabled but hidden';
 $string['original'] = 'Original';
-$string['originalextensionchange'] = 'The original file extension has been modified as a part of the file name change. Changing the extension from ".{$a->originalextension}" to ".{$a->newextension}" could potentially cause some side effects.';
-$string['originalextensionremove'] = 'The original file extension has been removed as a part of the file name change. Removing the extension ".{$a}" could potentially cause some side effects.';
+$string['originalextensionchange'] = 'The original file extension has been modified as a part of the file name change. Changing the extension from ".{$a->originalextension}" to ".{$a->newextension}" may result in a file which cannot be opened.';
+$string['originalextensionremove'] = 'The original file extension has been removed as a part of the file name change. Removing the extension ".{$a}" is likely to result in a file which cannot be opened.';
 $string['openpicker'] = 'Choose a file...';
 $string['operation'] = 'Operation';
 $string['on'] = 'Enabled and visible';
index a37ebed..12a250e 100644 (file)
@@ -151,9 +151,9 @@ $string['confirmunassigntitle'] = 'Confirm role change';
 $string['confirmunassignyes'] = 'Remove';
 $string['confirmunassignno'] = 'Cancel';
 $string['contentbank:access'] = 'Access the content bank';
-$string['contentbank:deleteanycontent'] = 'Delete any content from the content bank on the site';
-$string['contentbank:deleteowncontent'] = 'Delete content from the content bank created by the user';
-$string['contentbank:upload'] = 'Upload new content in the content bank';
+$string['contentbank:deleteanycontent'] = 'Delete any content from the content bank';
+$string['contentbank:deleteowncontent'] = 'Delete content from own content bank';
+$string['contentbank:upload'] = 'Upload content to the content bank';
 $string['context'] = 'Context';
 $string['course:activityvisibility'] = 'Hide/show activities';
 $string['course:bulkmessaging'] = 'Send a message to many people';
@@ -183,7 +183,7 @@ $string['course:markcomplete'] = 'Mark users as complete in course completion';
 $string['course:movesections'] = 'Move sections';
 $string['course:overridecompletion'] = 'Override activity completion status';
 $string['course:renameroles'] = 'Rename roles';
-$string['course:recommendactivity'] = 'Recommend activities to the activity chooser';
+$string['course:recommendactivity'] = 'Recommend activities in the activity chooser';
 $string['course:request'] = 'Request new courses';
 $string['course:reset'] = 'Reset course';
 $string['course:reviewotherusers'] = 'Review other users';
index 969d782..f2ddae7 100644 (file)
Binary files a/lib/amd/build/custom_interaction_events.min.js and b/lib/amd/build/custom_interaction_events.min.js differ
index 5db8445..7e05919 100644 (file)
Binary files a/lib/amd/build/custom_interaction_events.min.js.map and b/lib/amd/build/custom_interaction_events.min.js.map differ
index 98725b8..674fffa 100644 (file)
@@ -43,6 +43,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
         ctrlPageUp: 'cie:ctrlPageUp',
         ctrlPageDown: 'cie:ctrlPageDown',
         enter: 'cie:enter',
+        accessibleChange: 'cie:accessibleChange',
     };
     // Static cache of jQuery events that have been handled. This should
     // only be populated by JavaScript generated events (which will keep it
@@ -415,6 +416,48 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
         addKeyboardEvent(element, events.enter, keyCodes.enter);
     };
 
+    /**
+     * Trigger the AccessibleChange event on the given element if the value of the element is changed.
+     *
+     * @method addAccessibleChangeListener
+     * @private
+     * @param {object} element jQuery object to add event listeners to
+     */
+    var addAccessibleChangeListener = function(element) {
+        var onMac = navigator.userAgent.indexOf('Macintosh') !== -1;
+        var touchEnabled = ('ontouchstart' in window) || (('msMaxTouchPoints' in navigator) && (navigator.msMaxTouchPoints > 0));
+        if (onMac || touchEnabled) {
+            element.on('change', function(e) {
+                triggerEvent(events.accessibleChange, e);
+            });
+        } else {
+            element.on('focus', function() {
+                $(this).data('initValue', this.value);
+            });
+            element.on('blur', function(e) {
+                var initValue = $(this).data('initValue');
+                $(this).removeData('initValue');
+                if (this.value !== initValue) {
+                    triggerEvent(events.accessibleChange, e);
+                }
+            });
+            element.on('keydown', function(e) {
+                if ((e.which === keyCodes.enter) && this.value !== $(this).data('initValue')) {
+                    triggerEvent(events.accessibleChange, e);
+                } else if (e.which === keyCodes.escape) {
+                    this.value = $(this).data('initValue');
+                }
+            });
+            element.on('click', function(e) {
+                var initValue = $(this).data('initValue');
+                // Some browsers trigger onclick before onblur, therefore it is possible that initValue is undefined.
+                if (typeof initValue !== 'undefined' && initValue != this.value) {
+                    triggerEvent(events.accessibleChange, e);
+                }
+            });
+        }
+    };
+
     /**
      * Get the list of events and their handlers.
      *
@@ -441,6 +484,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
         handlers[events.ctrlPageUp] = addCtrlPageUpListener;
         handlers[events.ctrlPageDown] = addCtrlPageDownListener;
         handlers[events.enter] = addEnterListener;
+        handlers[events.accessibleChange] = addAccessibleChangeListener;
 
         return handlers;
     };
index c9eeb6c..31d6679 100644 (file)
@@ -51,6 +51,6 @@ $string['tcpsocketport'] = 'TCP socket port';
 $string['tcpsocketportdesc'] = 'The port to use when connecting to ClamAV';
 $string['unknownerror'] = 'There was an unknown error with ClamAV.';
 $string['tries'] = 'Scanning attempts';
-$string['tries_desc'] = 'Number of attempts clamav will try when there is an error during scanning process';
+$string['tries_desc'] = 'Number of attempts made by ClamAV if there is an error during the scanning process.';
 $string['tries_notice'] = 'Clamav scanning has tried {$a->tries} time(s).
 {$a->notice}';
index 313cd08..03880b0 100644 (file)
@@ -517,7 +517,14 @@ function cli_execute_parallel($cmds, $cwd = null, $delay = 0) {
 
     // Create child process.
     foreach ($cmds as $name => $cmd) {
-        $process = new Symfony\Component\Process\Process($cmd);
+        if (method_exists('\\Symfony\\Component\\Process\\Process', 'fromShellCommandline')) {
+            // Process 4.2 and up.
+            $process = Symfony\Component\Process\Process::fromShellCommandline($cmd);
+        } else {
+            // Process 4.1 and older.
+            $process = new Symfony\Component\Process\Process(null);
+            $process->setCommandLine($cmd);
+        }
 
         $process->setWorkingDirectory($cwd);
         $process->setTimeout(null);
index b7fd89c..fdf4e3e 100644 (file)
@@ -568,8 +568,7 @@ class manager {
 
         $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
         $params = array('timestart1' => $timestart);
-        $records = $DB->get_records_select('task_adhoc', $where, $params, 'nextruntime ASC, id ASC');
-
+        $records = $DB->get_records_select('task_adhoc', $where, $params, 'nextruntime ASC, id ASC', '*', 0, 2000);
         $records = self::ensure_adhoc_task_qos($records);
 
         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
index 15b781f..eecc092 100644 (file)
@@ -38,7 +38,7 @@ class messaging_cleanup_task extends scheduled_task {
     }
 
     /**
-     * Do the job.
+     * Do the job. Each message processor also gets the chance to perform it's own cleanup.
      * Throw exceptions on errors (the job will be retried).
      */
     public function execute() {
@@ -46,9 +46,17 @@ class messaging_cleanup_task extends scheduled_task {
 
         $timenow = time();
 
+        $processors = get_message_processors(true);
+
         // Cleanup read and unread notifications.
         if (!empty($CFG->messagingdeleteallnotificationsdelay)) {
             $notificationdeletetime = $timenow - $CFG->messagingdeleteallnotificationsdelay;
+
+            /** @var \message_output $processor */
+            foreach (array_column($processors, 'object') as $processor) {
+                $processor->cleanup_all_notifications($notificationdeletetime);
+            }
+
             $params = array('notificationdeletetime' => $notificationdeletetime);
             $DB->delete_records_select('notifications', 'timecreated < :notificationdeletetime', $params);
         }
@@ -56,6 +64,12 @@ class messaging_cleanup_task extends scheduled_task {
         // Cleanup read notifications.
         if (!empty($CFG->messagingdeletereadnotificationsdelay)) {
             $notificationdeletetime = $timenow - $CFG->messagingdeletereadnotificationsdelay;
+
+            /** @var \message_output $processor */
+            foreach (array_column($processors, 'object') as $processor) {
+                $processor->cleanup_read_notifications($notificationdeletetime);
+            }
+
             $params = array('notificationdeletetime' => $notificationdeletetime);
             $DB->delete_records_select('notifications', 'timeread < :notificationdeletetime', $params);
         }
index 6d4a2be..542fd0d 100644 (file)
@@ -9546,37 +9546,83 @@ function get_performance_info() {
     }
 
     $info['html'] .= '</ul>';
+    $html = '';
     if ($stats = cache_helper::get_stats()) {
-        $html = '<ul class="cachesused list-unstyled ml-1 row">';
-        $html .= '<li class="cache-stats-heading font-weight-bold">Caches used (hits/misses/sets)</li>';
-        $html .= '</ul><ul class="cachesused list-unstyled ml-1">';
+
+        $table = new html_table();
+        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-striped';
+        $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S'];
+        $table->data = [];
+        $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right'];
+
         $text = 'Caches used (hits/misses/sets): ';
         $hits = 0;
         $misses = 0;
         $sets = 0;
+        $maxstores = 0;
+
+        // We want to align static caches into their own column.
+        $hasstatic = false;
+        foreach ($stats as $definition => $details) {
+            $numstores = count($details['stores']);
+            $first = key($details['stores']);
+            if ($first !== cache_store::STATIC_ACCEL) {
+                $numstores++; // Add a blank space for the missing static store.
+            }
+            $maxstores = max($maxstores, $numstores);
+        }
+
+        $storec = 0;
+
+        while ($storec++ < ($maxstores - 2)) {
+            if ($storec == ($maxstores - 2)) {
+                $table->head[] = get_string('mappingfinal', 'cache');
+            } else {
+                $table->head[] = "Store $storec";
+            }
+            $table->align[] = 'left';
+            $table->align[] = 'right';
+            $table->align[] = 'right';
+            $table->align[] = 'right';
+            $table->head[] = 'H';
+            $table->head[] = 'M';
+            $table->head[] = 'S';
+        }
+
+        ksort($stats);
+
         foreach ($stats as $definition => $details) {
             switch ($details['mode']) {
                 case cache_store::MODE_APPLICATION:
                     $modeclass = 'application';
-                    $mode = ' <span title="application cache">[a]</span>';
+                    $mode = ' <span title="application cache">App</span>';
                     break;
                 case cache_store::MODE_SESSION:
                     $modeclass = 'session';
-                    $mode = ' <span title="session cache">[s]</span>';
+                    $mode = ' <span title="session cache">Ses</span>';
                     break;
                 case cache_store::MODE_REQUEST:
                     $modeclass = 'request';
-                    $mode = ' <span title="request cache">[r]</span>';
+                    $mode = ' <span title="request cache">Req</span>';
                     break;
             }
-            $html .= '<li class="d-inline-flex"><ul class="cache-definition-stats list-unstyled ml-1 mb-1 cache-mode-'.$modeclass.' card d-inline-block">';
-            $html .= '<li class="cache-definition-stats-heading p-t-1 card-header bg-dark bg-inverse font-weight-bold">' .
-                $definition . $mode.'</li>';
+            $row = [$mode, $definition];
+
             $text .= "$definition {";
+
+            $storec = 0;
             foreach ($details['stores'] as $store => $data) {
-                $hits += $data['hits'];
+
+                if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
+                    $row[] = '';
+                    $row[] = '';
+                    $row[] = '';
+                    $storec++;
+                }
+
+                $hits   += $data['hits'];
                 $misses += $data['misses'];
-                $sets += $data['sets'];
+                $sets   += $data['sets'];
                 if ($data['hits'] == 0 and $data['misses'] > 0) {
                     $cachestoreclass = 'nohits text-danger';
                 } else if ($data['hits'] < $data['misses']) {
@@ -9585,18 +9631,100 @@ function get_performance_info() {
                     $cachestoreclass = 'hihits text-success';
                 }
                 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
-                $html .= "<li class=\"cache-store-stats $cachestoreclass p-x-1\">" .
-                    "$store: $data[hits] / $data[misses] / $data[sets]</li>";
-                // This makes boxes of same sizes.
-                if (count($details['stores']) == 1) {
-                    $html .= "<li class=\"cache-store-stats $cachestoreclass p-x-1\">&nbsp;</li>";
+                $cell = new html_table_cell($store);
+                $cell->attributes = ['class' => $cachestoreclass];
+                $row[] = $cell;
+                $cell = new html_table_cell($data['hits']);
+                $cell->attributes = ['class' => $cachestoreclass];
+                $row[] = $cell;
+                $cell = new html_table_cell($data['misses']);
+                $cell->attributes = ['class' => $cachestoreclass];
+                $row[] = $cell;
+
+                if ($store !== cache_store::STATIC_ACCEL) {
+                    // The static cache is never set.
+                    $cell = new html_table_cell($data['sets']);
+                    $cell->attributes = ['class' => $cachestoreclass];
+                    $row[] = $cell;
                 }
+                $storec++;
+            }
+            while ($storec++ < $maxstores) {
+                $row[] = '';
+                $row[] = '';
+                $row[] = '';
+                $row[] = '';
             }
-            $html .= '</ul></li>';
             $text .= '} ';
+
+            $table->data[] = $row;
         }
-        $html .= '</ul> ';
-        $html .= "<div class='cache-total-stats row'>Total: $hits / $misses / $sets</div>";
+
+        $html .= html_writer::table($table);
+
+        // Now lets also show sub totals for each cache store.
+        $storetotals = [];
+        $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0];
+        foreach ($stats as $definition => $details) {
+            foreach ($details['stores'] as $store => $data) {
+                if (!array_key_exists($store, $storetotals)) {
+                    $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0];
+                }
+                $storetotals[$store]['class']   = $data['class'];
+                $storetotals[$store]['hits']   += $data['hits'];
+                $storetotals[$store]['misses'] += $data['misses'];
+                $storetotals[$store]['sets']   += $data['sets'];
+                $storetotal['hits']   += $data['hits'];
+                $storetotal['misses'] += $data['misses'];
+                $storetotal['sets']   += $data['sets'];
+            }
+        }
+
+        $table = new html_table();
+        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-striped';
+        $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S'];
+        $table->data = [];
+        $table->align = ['left', 'left', 'right', 'right', 'right'];
+
+        ksort($storetotals);
+
+        foreach ($storetotals as $store => $data) {
+            $row = [];
+            if ($data['hits'] == 0 and $data['misses'] > 0) {
+                $cachestoreclass = 'nohits text-danger';
+            } else if ($data['hits'] < $data['misses']) {
+                $cachestoreclass = 'lowhits text-warning';
+            } else {
+                $cachestoreclass = 'hihits text-success';
+            }
+            $cell = new html_table_cell($store);
+            $cell->attributes = ['class' => $cachestoreclass];
+            $row[] = $cell;
+            $cell = new html_table_cell($data['class']);
+            $cell->attributes = ['class' => $cachestoreclass];
+            $row[] = $cell;
+            $cell = new html_table_cell($data['hits']);
+            $cell->attributes = ['class' => $cachestoreclass];
+            $row[] = $cell;
+            $cell = new html_table_cell($data['misses']);
+            $cell->attributes = ['class' => $cachestoreclass];
+            $row[] = $cell;
+            $cell = new html_table_cell($data['sets']);
+            $cell->attributes = ['class' => $cachestoreclass];
+            $row[] = $cell;
+            $table->data[] = $row;
+        }
+        $row = [
+            get_string('total'),
+            '',
+            $storetotal['hits'],
+            $storetotal['misses'],
+            $storetotal['sets'],
+        ];
+        $table->data[] = $row;
+
+        $html .= html_writer::table($table);
+
         $info['cachesused'] = "$hits / $misses / $sets";
         $info['html'] .= $html;
         $info['txt'] .= $text.'. ';
index 96cc2ca..4054b65 100644 (file)
Binary files a/lib/table/amd/build/dynamic.min.js and b/lib/table/amd/build/dynamic.min.js differ
index d457b4f..7334d4e 100644 (file)
Binary files a/lib/table/amd/build/dynamic.min.js.map and b/lib/table/amd/build/dynamic.min.js.map differ
index db1dc71..a377b54 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/repository.min.js and b/lib/table/amd/build/local/dynamic/repository.min.js differ
index d1d3f8b..1f58475 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/repository.min.js.map and b/lib/table/amd/build/local/dynamic/repository.min.js.map differ
index f4dc44a..d250c63 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/selectors.min.js and b/lib/table/amd/build/local/dynamic/selectors.min.js differ
index 73059fc..4e7d102 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/selectors.min.js.map and b/lib/table/amd/build/local/dynamic/selectors.min.js.map differ
index c3f264f..6333385 100644 (file)
@@ -78,6 +78,7 @@ export const refreshTableContent = tableRoot => {
             lastinitial: tableRoot.dataset.tableLastInitial,
             pageNumber: tableRoot.dataset.tablePageNumber,
             pageSize: tableRoot.dataset.tablePageSize,
+            hiddenColumns: JSON.parse(tableRoot.dataset.tableHiddenColumns),
         }
     )
     .then(data => {
@@ -97,6 +98,7 @@ export const updateTable = (tableRoot, {
     lastInitial = null,
     pageNumber = null,
     pageSize = null,
+    hiddenColumns = null,
 } = {}, refreshContent = true) => {
     checkTableIsDynamic(tableRoot);
 
@@ -128,6 +130,11 @@ export const updateTable = (tableRoot, {
         tableRoot.dataset.tableFilters = JSON.stringify(filters);
     }
 
+    // Update hidden columns.
+    if (hiddenColumns) {
+        tableRoot.dataset.tableHiddenColumns = JSON.stringify(hiddenColumns);
+    }
+
     // Refresh.
     if (refreshContent) {
         return refreshTableContent(tableRoot);
@@ -203,6 +210,34 @@ export const setFirstInitial = (tableRoot, firstInitial, refreshContent = true)
 export const setLastInitial = (tableRoot, lastInitial, refreshContent = true) =>
     updateTable(tableRoot, {lastInitial}, refreshContent);
 
+/**
+ * Hide a column in the participants table.
+ *
+ * @param {HTMLElement} tableRoot
+ * @param {String} columnToHide
+ * @param {Bool} refreshContent
+ */
+export const hideColumn = (tableRoot, columnToHide, refreshContent = true) => {
+    const hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);
+    hiddenColumns.push(columnToHide);
+
+    updateTable(tableRoot, {hiddenColumns}, refreshContent);
+};
+
+/**
+ * Make a hidden column visible in the participants table.
+ *
+ * @param {HTMLElement} tableRoot
+ * @param {String} columnToShow
+ * @param {Bool} refreshContent
+ */
+export const showColumn = (tableRoot, columnToShow, refreshContent = true) => {
+    let hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);
+    hiddenColumns = hiddenColumns.filter(columnName => columnName !== columnToShow);
+
+    updateTable(tableRoot, {hiddenColumns}, refreshContent);
+};
+
 /**
  * Set up listeners to handle table updates.
  */
@@ -247,5 +282,38 @@ export const init = () => {
 
             setPageNumber(tableRoot, pageItem.dataset.pageNumber);
         }
+
+        const hide = e.target.closest(Selectors.table.links.hide);
+        if (hide) {
+            e.preventDefault();
+
+            hideColumn(tableRoot, hide.dataset.column);
+        }
+
+        const show = e.target.closest(Selectors.table.links.show);
+        if (show) {
+            e.preventDefault();
+
+            showColumn(tableRoot, show.dataset.column);
+        }
+
     });
 };
+
+/**
+ * Fetch the table via its table region id
+ *
+ * @param {String} tableRegionId
+ * @returns {HTMLElement}
+ */
+export const getTableFromId = tableRegionId => {
+    const tableRoot = document.querySelector(Selectors.main.fromRegionId(tableRegionId));
+
+
+    if (!tableRoot) {
+        // The table is not a dynamic table.
+        throw new Error("The table specified is not a dynamic table and cannot be updated");
+    }
+
+    return tableRoot;
+};
index 9083c6f..344de98 100644 (file)
@@ -48,6 +48,7 @@ export const fetch = (component, handler, uniqueid, {
         lastinitial = null,
         pageNumber = null,
         pageSize = null,
+        hiddenColumns = {}
     } = {}
 ) => {
     return fetchMany([{
@@ -64,6 +65,7 @@ export const fetch = (component, handler, uniqueid, {
             lastinitial,
             pagenumber: pageNumber,
             pagesize: pageSize,
+            hiddencolumns: hiddenColumns,
         },
     }])[0];
 };
index 118f328..64c7554 100644 (file)
 export default {
     main: {
         region: '[data-region="core_table/dynamic"]',
+        fromRegionId: regionId => `[data-region="core_table/dynamic"][data-table-uniqueid="${regionId}"]`,
     },
     table: {
         links: {
             sortableColumn: 'a[data-sortable="1"]',
+            hide: 'a[data-action="hide"]',
+            show: 'a[data-action="show"]',
         },
     },
     initialsBar: {
index f49b6b4..6e117e9 100644 (file)
@@ -115,6 +115,14 @@ class fetch extends external_api {
                 VALUE_REQUIRED,
                 null
             ),
+            'hiddencolumns' => new external_multiple_structure(
+                new external_value(
+                    PARAM_ALPHANUMEXT,
+                    'Name of column',
+                    VALUE_REQUIRED,
+                    null
+                )
+            ),
         ]);
     }
 
@@ -146,7 +154,8 @@ class fetch extends external_api {
         ?string $firstinitial = null,
         ?string $lastinitial = null,
         ?int $pagenumber = null,
-        ?int $pagesize = null
+        ?int $pagesize = null,
+        ?array $hiddencolumns = null
     ) {
 
         global $PAGE;
@@ -163,6 +172,7 @@ class fetch extends external_api {
             'lastinitial' => $lastinitial,
             'pagenumber' => $pagenumber,
             'pagesize' => $pagesize,
+            'hiddencolumns' => $hiddencolumns,
         ] = self::validate_parameters(self::execute_parameters(), [
             'component' => $component,
             'handler' => $handler,
@@ -175,6 +185,7 @@ class fetch extends external_api {
             'lastinitial' => $lastinitial,
             'pagenumber' => $pagenumber,
             'pagesize' => $pagesize,
+            'hiddencolumns' => $hiddencolumns,
         ]);
 
         $tableclass = "\\{$component}\\table\\{$handler}";
@@ -221,6 +232,10 @@ class fetch extends external_api {
             $pagesize = 20;
         }
 
+        if ($hiddencolumns !== null) {
+            $instance->set_hidden_columns($hiddencolumns);
+        }
+
         $context = $instance->get_context();
         self::validate_context($context);
         $PAGE->set_url($instance->get_base_url());
index b5ae343..111b221 100644 (file)
@@ -65,7 +65,8 @@ class fetch_test extends advanced_testcase {
         $this->resetAfterTest();
 
         $this->expectException(\UnexpectedValueException::class);
-        fetch::execute("core_users", "participants", "", "email", "4", [], "1");
+        fetch::execute("core_users", "participants", "", "email", "4", [], (string)filter::JOINTYPE_ANY,
+            null, null, null, null, []);
     }
 
     /**
@@ -79,7 +80,8 @@ class fetch_test extends advanced_testcase {
         $this->expectExceptionMessage("Table handler class {$handler} not found. Please make sure that your table handler class is under the \\core_user\\table namespace.");
 
         // Tests that invalid users_participants_table class gets an exception.
-        fetch::execute("core_user", "users_participants_table", "", "email", "4", [], "1");
+        fetch::execute("core_user", "users_participants_table", "", "email", "4", [], (string)filter::JOINTYPE_ANY,
+            null, null, null, null, []);
     }
 
     /**
@@ -129,7 +131,9 @@ class fetch_test extends advanced_testcase {
         ];
 
         $participantstable = fetch::execute("core_user", "participants",
-            "user-index-participants-{$course->id}", "firstname", "4", $filter, (string)filter::JOINTYPE_ANY);
+            "user-index-participants-{$course->id}", "firstname", "4", $filter, (string)filter::JOINTYPE_ANY,
+            null, null, null, null, []);
+
         $html = $participantstable['html'];
 
         $this->assertStringContainsString($user1->email, $html);
index 33375eb..7e669a2 100644 (file)
@@ -154,6 +154,9 @@ class flexible_table {
     /** @var $filename */
     protected $filename;
 
+    /** @var array $hiddencolumns List of hidden columns. */
+    protected $hiddencolumns;
+
     /**
      * Constructor
      * @param string $uniqueid all tables have to have a unique id, this is used
@@ -511,25 +514,7 @@ class flexible_table {
             $oldprefs = $this->prefs;
         }
 
-        if (($showcol = optional_param($this->request[TABLE_VAR_SHOW], '', PARAM_ALPHANUMEXT)) &&
-                isset($this->columns[$showcol])) {
-            $this->prefs['collapse'][$showcol] = false;
-
-        } else if (($hidecol = optional_param($this->request[TABLE_VAR_HIDE], '', PARAM_ALPHANUMEXT)) &&
-                isset($this->columns[$hidecol])) {
-            $this->prefs['collapse'][$hidecol] = true;
-            if (array_key_exists($hidecol, $this->prefs['sortby'])) {
-                unset($this->prefs['sortby'][$hidecol]);
-            }
-        }
-
-        // Now, update the column attributes for collapsed columns
-        foreach (array_keys($this->columns) as $column) {
-            if (!empty($this->prefs['collapse'][$column])) {
-                $this->column_style[$column]['width'] = '10px';
-            }
-        }
-
+        $this->set_hide_show_preferences();
         $this->set_sorting_preferences();
         $this->set_initials_preferences();
 
@@ -1205,14 +1190,18 @@ class flexible_table {
         if (!empty($this->prefs['collapse'][$column])) {
             $linkattributes = array('title' => get_string('show') . ' ' . strip_tags($this->headers[$index]),
                                     'aria-expanded' => 'false',
-                                    'aria-controls' => $ariacontrols);
+                                    'aria-controls' => $ariacontrols,
+                                    'data-action' => 'show',
+                                    'data-column' => $column);
             return html_writer::link($this->baseurl->out(false, array($this->request[TABLE_VAR_SHOW] => $column)),
                     $OUTPUT->pix_icon('t/switch_plus', get_string('show')), $linkattributes);
 
         } else if ($this->headers[$index] !== NULL) {
             $linkattributes = array('title' => get_string('hide') . ' ' . strip_tags($this->headers[$index]),
                                     'aria-expanded' => 'true',
-                                    'aria-controls' => $ariacontrols);
+                                    'aria-controls' => $ariacontrols,
+                                    'data-action' => 'hide',
+                                    'data-column' => $column);
             return html_writer::link($this->baseurl->out(false, array($this->request[TABLE_VAR_HIDE] => $column)),
                     $OUTPUT->pix_icon('t/switch_minus', get_string('hide')), $linkattributes);
         }
@@ -1380,6 +1369,43 @@ class flexible_table {
 
     }
 
+    /**
+     * Set hide and show preferences.
+     */
+    protected function set_hide_show_preferences(): void {
+
+        if ($this->hiddencolumns !== null) {
+            $this->prefs['collapse'] = array_fill_keys(array_filter($this->hiddencolumns, function($column) {
+                return array_key_exists($column, $this->columns);
+            }), true);
+        } else {
+            if ($column = optional_param($this->request[TABLE_VAR_HIDE], '', PARAM_ALPHANUMEXT)) {
+                if (isset($this->columns[$column])) {
+                    $this->prefs['collapse'][$column] = true;
+                }
+            }
+        }
+
+        if ($column = optional_param($this->request[TABLE_VAR_SHOW], '', PARAM_ALPHANUMEXT)) {
+            unset($this->prefs['collapse'][$column]);
+        }
+
+        foreach (array_keys($this->prefs['collapse']) as $column) {
+            if (array_key_exists($column, $this->prefs['sortby'])) {
+                unset($this->prefs['sortby'][$column]);
+            }
+        }
+    }
+
+    /**
+     * Set the list of hidden columns.
+     *
+     * @param array $columns The list of hidden columns.
+     */
+    public function set_hidden_columns(array $columns): void {
+        $this->hiddencolumns = $columns;
+    }
+
     /**
      * Set the preferred table sorting attributes.
      *
@@ -1540,6 +1566,7 @@ class flexible_table {
                 'data-table-last-initial' => $this->prefs['i_last'],
                 'data-table-page-number' => $this->currpage + 1,
                 'data-table-page-size' => $this->pagesize,
+                'data-table-hidden-columns' => json_encode(array_keys($this->prefs['collapse'])),
             ]);
         }
 
index 288fa7e..302375b 100644 (file)
     </form>
 </div>
 {{#js}}
-require(['jquery'], function($) {
-    $('#{{id}}').change(function() {
+require(['jquery', 'core/custom_interaction_events'], function($, CustomEvents) {
+    CustomEvents.define('#{{id}}', [CustomEvents.events.accessibleChange]);
+    $('#{{id}}').on(CustomEvents.events.accessibleChange, function() {
         var ignore = $(this).find(':selected').attr('data-ignore');
         if (typeof ignore === typeof undefined) {
             $('#{{formid}}').submit();
index 16fe626..4a10e23 100644 (file)
@@ -75,8 +75,9 @@
 </div>
 {{^showbutton}}
     {{#js}}
-        require(['jquery'], function($) {
-            $('#{{id}}').change(function() {
+        require(['jquery', 'core/custom_interaction_events'], function($, CustomEvents) {
+            CustomEvents.define('#{{id}}', [CustomEvents.events.accessibleChange]);
+            $('#{{id}}').on(CustomEvents.events.accessibleChange, function() {
                 if (!$(this).val()) {
                     return false;
                 }
diff --git a/message/amd/build/message_preferences.min.js b/message/amd/build/message_preferences.min.js
new file mode 100644 (file)
index 0000000..3378a0d
Binary files /dev/null and b/message/amd/build/message_preferences.min.js differ
diff --git a/message/amd/build/message_preferences.min.js.map b/message/amd/build/message_preferences.min.js.map
new file mode 100644 (file)
index 0000000..a83818b
Binary files /dev/null and b/message/amd/build/message_preferences.min.js.map differ
diff --git a/message/amd/src/message_preferences.js b/message/amd/src/message_preferences.js
new file mode 100644 (file)
index 0000000..c5944b6
--- /dev/null
@@ -0,0 +1,125 @@
+// 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/>.
+
+/**
+ * Controls the message preference page.
+ *
+ * @module     core_message/message_preferences
+ * @class      message_preferences
+ * @package    message
+ * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/ajax', 'core/notification',
+        'core_message/message_notification_preference', 'core/custom_interaction_events'],
+        function($, Ajax, Notification, MessageNotificationPreference, CustomEvents) {
+
+    var SELECTORS = {
+        PREFERENCE: '[data-state]',
+        PREFERENCES_CONTAINER: '[data-region="preferences-container"]',
+        CONTACTABLE_PRIVACY_CONTAINER: '[data-region="privacy-setting-container"]',
+    };
+
+    /**
+     * Constructor for the MessagePreferences.
+     *
+     * @param {object} element The root element for the message preferences
+     */
+    var MessagePreferences = function(element) {
+        this.root = $(element);
+        this.userId = this.root.find(SELECTORS.PREFERENCES_CONTAINER).attr('data-user-id');
+
+        this.registerEventListeners();
+    };
+
+    /**
+     * Check if the preferences have been disabled on this page.
+     *
+     * @method preferencesDisabled
+     * @return {bool}
+     */
+    MessagePreferences.prototype.preferencesDisabled = function() {
+        return this.root.find(SELECTORS.PREFERENCES_CONTAINER).hasClass('disabled');
+    };
+
+    /**
+     * Update the contactable privacy user preference in the DOM and
+     * send a request to update on the server.
+     *
+     * @return {Promise}
+     * @method saveContactablePrivacySetting
+     */
+    MessagePreferences.prototype.saveContactablePrivacySetting = function() {
+        var container = this.root.find(SELECTORS.CONTACTABLE_PRIVACY_CONTAINER);
+        var value = $("input[type='radio']:checked").val();
+
+        if (container.hasClass('loading')) {
+            return $.Deferred().resolve();
+        }
+
+        container.addClass('loading');
+
+        var request = {
+            methodname: 'core_user_update_user_preferences',
+            args: {
+                userid: this.userId,
+                preferences: [
+                    {
+                        type: container.attr('data-preference-key'),
+                        value: value,
+                    }
+                ]
+            }
+        };
+
+        return Ajax.call([request])[0]
+            .fail(Notification.exception)
+            .always(function() {
+                container.removeClass('loading');
+            });
+    };
+
+    /**
+     * Create all of the event listeners for the message preferences page.
+     *
+     * @method registerEventListeners
+     */
+    MessagePreferences.prototype.registerEventListeners = function() {
+        CustomEvents.define(this.root, [
+            CustomEvents.events.activate
+        ]);
+
+        this.root.on('change', function(e) {
+            // Add listener for privacy setting radio buttons change.
+            if (e.target.name == 'message_blocknoncontacts') {
+                this.saveContactablePrivacySetting();
+            } else {
+                // Add listener for processor preferences.
+                if (!this.preferencesDisabled()) {
+                    var preferencesContainer = $(e.target).closest(SELECTORS.PREFERENCES_CONTAINER);
+                    var preferenceElement = $(e.target).closest(SELECTORS.PREFERENCE);
+                    var messagePreference = new MessageNotificationPreference(preferencesContainer, this.userId);
+
+                    preferenceElement.addClass('loading');
+                    messagePreference.save().always(function() {
+                        preferenceElement.removeClass('loading');
+                    });
+                }
+            }
+        }.bind(this));
+    };
+
+    return MessagePreferences;
+});
index 5005377..8ddb469 100644 (file)
@@ -27,7 +27,7 @@ $string['airnotifierappname'] = 'Airnotifier app name';
 $string['airnotifiermobileappname'] = 'Mobile app name';
 $string['airnotifierport'] = 'Airnotifier port';
 $string['airnotifierurl'] = 'Airnotifier URL';
-$string['configairnotifierurl'] = 'The server url to connect to to send push notifications.';
+$string['configairnotifierurl'] = 'The server URL to connect to for sending push notifications.';
 $string['configairnotifierport'] = 'The port to use when connecting to the airnotifier server.';
 $string['configairnotifieraccesskey'] = 'The access key to use when connecting to the airnotifier server.';
 $string['configairnotifierappname'] = 'The app name identifier in Airnotifier.';
@@ -35,12 +35,12 @@ $string['configairnotifiermobileappname'] = 'The Mobile app unique identifier (u
 $string['deletecheckdevicename'] = 'Delete your device: {$a->name}';
 $string['deletedevice'] = 'Delete the device. Note that an app can register the device again. If the device keeps reappearing, disable it.';
 $string['devicetoken'] = 'Device token';
-$string['enableprocessor'] = 'Enable Mobile notifications.';
+$string['enableprocessor'] = 'Enable mobile notifications';
 $string['errorretrievingkey'] = 'An error occurred while retrieving the access key. Your site must be registered to use this service. If your site is already registered, please try updating your registration.';
 $string['keyretrievedsuccessfully'] = 'Key retrieved successfully';
 $string['nodevices'] = 'No registered devices. Devices will automatically appear after you install the Moodle app and add this site.';
 $string['nopermissiontomanagedevices'] = 'You don\'t have permission to manage devices.';
-$string['notconfigured'] = 'The Airnotifier server hasn\'t been configured so Airnotifier messages cannot be sent';
+$string['notconfigured'] = 'The Airnotifier server has not been configured so push notifications cannot be sent.';
 $string['pluginname'] = 'Mobile';
 $string['privacy:appiddescription'] = 'This is an identifier to the application being used.';
 $string['privacy:enableddescription'] = 'If this device is enabled for airnotifier.';
index ce7fd76..bd29b25 100644 (file)
@@ -121,7 +121,26 @@ abstract class message_output {
     public function force_process_messages() {
         return false;
     }
-}
-
 
+    /**
+     * Allow processors to perform cleanup tasks for all notifications by overriding this method
+     *
+     * @since Moodle 3.9
+     * @param int $notificationdeletetime
+     * @return void
+     */
+    public function cleanup_all_notifications(int $notificationdeletetime): void {
+        return;
+    }
 
+    /**
+     * Allow processors to perform cleanup tasks for read notifications by overriding this method
+     *
+     * @since Moodle 3.9
+     * @param int $notificationdeletetime
+     * @return void
+     */
+    public function cleanup_read_notifications(int $notificationdeletetime): void {
+        return;
+    }
+}
index 131dfa7..d1dbfa8 100644 (file)
@@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die();
  * @param int $oldversion The version that we are upgrading from
  */
 function xmldb_message_popup_upgrade($oldversion) {
-    global $CFG;
+    global $DB;
 
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
@@ -44,5 +44,13 @@ function xmldb_message_popup_upgrade($oldversion) {
     // Automatically generated Moodle v3.8.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2020020600) {
+        // Clean up orphaned popup notification records.
+        $DB->delete_records_select('message_popup_notifications', 'notificationid NOT IN (SELECT id FROM {notifications})');
+
+        // Reportbuilder savepoint reached.
+        upgrade_plugin_savepoint(true, 2020020600, 'message', 'popup');
+    }
+
     return true;
 }
index e8bb366..69a9cd3 100644 (file)
@@ -111,4 +111,30 @@ class message_output_popup extends message_output {
 
         return !empty($CFG->messaging);
     }
+
+    /**
+     * Remove all popup notifications up to specified time
+     *
+     * @param int $notificationdeletetime
+     * @return void
+     */
+    public function cleanup_all_notifications(int $notificationdeletetime): void {
+        global $DB;
+
+        $DB->delete_records_select('message_popup_notifications',
+            'notificationid IN (SELECT id FROM {notifications} WHERE timecreated < ?)', [$notificationdeletetime]);
+    }
+
+    /**
+     * Remove read popup notifications up to specified time
+     *
+     * @param int $notificationdeletetime
+     * @return void
+     */
+    public function cleanup_read_notifications(int $notificationdeletetime): void {
+        global $DB;
+
+        $DB->delete_records_select('message_popup_notifications',
+            'notificationid IN (SELECT id FROM {notifications} WHERE timeread < ?)', [$notificationdeletetime]);
+    }
 }
diff --git a/message/output/popup/tests/messaging_cleanup_test.php b/message/output/popup/tests/messaging_cleanup_test.php
new file mode 100644 (file)
index 0000000..6a2f4cb
--- /dev/null
@@ -0,0 +1,107 @@
+<?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/>.
+
+/**
+ * Test message popup messaging cleanup task
+ *
+ * @package     message_popup
+ * @category    test
+ * @copyright   2020 Paul Holden <paulh@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\task\messaging_cleanup_task;
+
+global $CFG;
+require_once($CFG->dirroot . '/message/output/popup/tests/base.php');
+
+/**
+ * Test class
+ *
+ * @package     message_popup
+ * @category    test
+ * @copyright   2020 Paul Holden <paulh@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class message_popup_messaging_cleanup_testcase extends advanced_testcase {
+
+    // Helper trait for sending fake popup notifications.
+    use message_popup_test_helper;
+
+    /**
+     * Test that all popup notifications are cleaned up
+     *
+     * @return void
+     */
+    public function test_cleanup_all_notifications() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $userfrom = $this->getDataGenerator()->create_user();
+        $userto = $this->getDataGenerator()->create_user();
+
+        $now = time();
+
+        $this->send_fake_unread_popup_notification($userfrom, $userto, 'Message 1', $now - 10);
+        $notificationid = $this->send_fake_unread_popup_notification($userfrom, $userto, 'Message 2', $now);
+
+        // Sanity check.
+        $this->assertEquals(2, $DB->count_records('message_popup_notifications'));
+
+        // Delete all notifications >5 seconds old.
+        set_config('messagingdeleteallnotificationsdelay', 5);
+        (new messaging_cleanup_task())->execute();
+
+        // We should have just one record now, matching the second notification we sent.
+        $records = $DB->get_records('message_popup_notifications');
+        $this->assertCount(1, $records);
+        $this->assertEquals($notificationid, reset($records)->notificationid);
+    }
+
+    /**
+     * Test that read popup notifications are cleaned up
+     *
+     * @return void
+     */
+    public function test_cleanup_read_notifications() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $userfrom = $this->getDataGenerator()->create_user();
+        $userto = $this->getDataGenerator()->create_user();
+
+        $now = time();
+
+        $this->send_fake_read_popup_notification($userfrom, $userto, 'Message 1', $now - 20, $now - 10);
+        $notificationid = $this->send_fake_read_popup_notification($userfrom, $userto, 'Message 2', $now - 15, $now);
+
+        // Sanity check.
+        $this->assertEquals(2, $DB->count_records('message_popup_notifications'));
+
+        // Delete read notifications >5 seconds old.
+        set_config('messagingdeletereadnotificationsdelay', 5);
+        (new messaging_cleanup_task())->execute();
+
+        // We should have just one record now, matching the second notification we sent.
+        $records = $DB->get_records('message_popup_notifications');
+        $this->assertCount(1, $records);
+        $this->assertEquals($notificationid, reset($records)->notificationid);
+    }
+}
index 40cda62..a4cef35 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2020012300;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2020020600;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2019111200;        // Requires this Moodle version
 $plugin->component = 'message_popup';  // Full name of the plugin (used for diagnostics)
diff --git a/message/templates/message_preferences.mustache b/message/templates/message_preferences.mustache
new file mode 100644 (file)
index 0000000..ebe1c40
--- /dev/null
@@ -0,0 +1,119 @@
+{{!
+    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_message/message_preferences
+
+    The message preferences page
+
+    Classes required for JS:
+    * None
+
+    Data attibutes required for JS:
+    * All data attributes are required
+
+    Context variables required for this template:
+    * userid The logged in user id
+    * disableall If the user has disabled notifications
+    * components The list of notification components
+    * privacychoices The choice options for the contactable privacy setting
+
+    Example context (json):
+    {
+        "userid": 1,
+        "disableall": 0,
+        "components": [
+            {
+                "notifications": [
+                    {
+                        "displayname": "Notices about minor problems",
+                        "preferencekey": "message_provider_moodle_notices",
+                        "onlinehelphtml": "<p>some help HTML</p>",
+                        "offlinehelphtml": "<p>some help HTML</p>",
+                        "processors": [
+                            {
+                                "displayname": "Popup notification",
+                                "name": "popup",
+                                "locked": 0,
+                                "userconfigured": 1,
+                                "loggedin": {
+                                    "name": "loggedin",
+                                    "displayname": "When I'm logged in",
+                                    "checked": 0,
+                                    "disableall": 0
+                                },
+                                "loggedoff": {
+                                    "name": "loggedoff",
+                                    "displayname": "When I'm offline",
+                                    "checked": 0,
+                                    "disableall": 0
+                                }
+                            }
+                        ]
+                    }
+                ]
+            }
+        ],
+        "privacychoices": [
+            {
+                "value": 1,
+                "text": "My contacts only",
+                "checked": 0
+            },
+            {
+                "value": 2,
+                "text": "Anyone within courses I am a member of",
+                "checked": 1
+            }
+        ]
+    }
+}}
+<div class="preferences-page-container" data-region="preferences-page-container">
+    <h2>{{#str}} messagepreferences, message {{/str}}</h2>
+    <div class="privacy-setting-container"
+         data-user-id="{{userid}}"
+         data-region="privacy-setting-container"
+         data-preference-key="message_blocknoncontacts">
+        <p>{{#str}} contactableprivacy, message {{/str}}</p>
+       {{#privacychoices}}
+        <input id="action-selection-option-{{value}}"
+               type="radio"
+               name="message_blocknoncontacts"
+               value="{{value}}"
+               {{#checked}}checked="checked"{{/checked}}/>
+        <label for="action-selection-option-{{value}}">{{text}}</label>
+        <br>
+       {{/privacychoices}}
+    </div><br>
+    <div class="preferences-container {{#disableall}}disabled{{/disableall}}"
+        data-user-id="{{userid}}"
+        data-region="preferences-container">
+        <table class="table table-hover preference-table" data-region="preference-table">
+            <tbody>
+                {{#components}}
+                    {{> message/message_preferences_component }}
+                {{/components}}
+            </tbody>
+        </table>
+    </div>
+</div>
+{{#js}}
+require(['jquery', 'core_message/message_preferences'],
+    function($, MessagePreferences) {
+
+    new MessagePreferences($('[data-region="preferences-page-container"]'));
+});
+{{/js}}
diff --git a/message/templates/message_preferences_component.mustache b/message/templates/message_preferences_component.mustache
new file mode 100644 (file)
index 0000000..52a63bd
--- /dev/null
@@ -0,0 +1,84 @@
+{{!
+    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_message/message_preferences_component
+
+    The message preferences page
+
+    Classes required for JS:
+    * None
+
+    Data attibutes required for JS:
+    * All data attributes are required
+
+    Context variables required for this template:
+    * notifications The list of notifications
+
+    Example context (json):
+    {
+        "notifications": [
+            {
+                "displayname": "Notices about minor problems",
+                "preferencekey": "message_provider_moodle_notices",
+                "onlinehelphtml": "<p>some help HTML</p>",
+                "offlinehelphtml": "<p>some help HTML</p>",
+                "processors": [
+                    {
+                        "displayname": "Popup notification",
+                        "name": "popup",
+                        "locked": 0,
+                        "userconfigured": 1,
+                        "loggedin": {
+                            "name": "loggedin",
+                            "displayname": "When I'm logged in",
+                            "checked": 0,
+                            "disableall": 0
+                        },
+                        "loggedoff": {
+                            "name": "loggedoff",
+                            "displayname": "When I'm offline",
+                            "checked": 0,
+                            "disableall": 0
+                        }
+                    }
+                ]
+            }
+        ]
+    }
+}}
+{{#notifications}}
+    <tr data-preference-key="{{preferencekey}}">
+        <th>{{displayname}}</th>
+        <td class="align-bottom">
+            <div class="container-fluid">
+                <div class="row-fluid">
+                    <div class="span6 col-6">
+                        {{#str}} loggedin, message {{/str}}
+                        {{#onlinehelphtml}}{{{.}}}{{/onlinehelphtml}}
+                    </div>
+                    <div class="span6 col-6">
+                        {{#str}} loggedoff, message {{/str}}
+                        {{#offlinehelphtml}}{{{.}}}{{/offlinehelphtml}}
+                    </div>
+                </div>
+            </div>
+        </td>
+    </tr>
+    {{#processors}}
+        {{> message/message_preferences_notification_processor }}
+    {{/processors}}
+{{/notifications}}
diff --git a/message/templates/message_preferences_notification_processor.mustache b/message/templates/message_preferences_notification_processor.mustache
new file mode 100644 (file)
index 0000000..2b5f08b
--- /dev/null
@@ -0,0 +1,127 @@
+{{!
+    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_message/message_preferences_notification_processor
+
+    The message preferences page
+
+    Classes required for JS:
+    * None
+
+    Data attibutes required for JS:
+    * All data attributes are required
+
+    Context variables required for this template:
+    * displayname   The display name of the processor
+    * name          The name of the processor
+    * locked        Whether the processor is locked
+    * loggedin      The logged in settings
+    * loggedoff     The logged off settings
+
+    Example context (json):
+    {
+        "displayname": "Notices about minor problems",
+        "preferencekey": "message_provider_moodle_notices",
+        "processors": [
+            {
+                "displayname": "Popup notification",
+                "name": "popup",
+                "locked": 0,
+                "userconfigured": 1,
+                "loggedin": {
+                    "name": "loggedin",
+                    "displayname": "When I'm logged in",
+                    "checked": 0,
+                    "disableall": 0
+                },
+                "loggedoff": {
+                    "name": "loggedoff",
+                    "displayname": "When I'm offline",
+                    "checked": 0,
+                    "disableall": 0
+                }
+            }
+        ]
+    }
+}}
+<tr class="preference-row" data-region="preference-row" data-preference-key="{{preferencekey}}">
+    <td class="preference-name">{{displayname}}</td>
+    <td {{^userconfigured}}class="disabled"{{/userconfigured}} data-processor-name="{{name}}">
+        {{#locked}}
+            <div class="dimmed_text">{{lockedmessage}}</div>
+        {{/locked}}
+        {{^locked}}
+            <div class="disabled-message">{{#str}} disabled, question {{/str}}</div>
+            <form>
+                <div class="container-fluid">
+                    <div class="row-fluid">
+                        <div class="span6 col-6">
+                            {{#loggedin}}
+                                {{< core/hover_tooltip }}
+                                    {{$anchor}}
+                                        <label class="preference-state"
+                                            title="{{displayname}}"
+                                            data-state="{{name}}">
+
+                                            <span class="accesshide">{{displayname}}</span>
+                                            <input type="checkbox"
+                                                tabindex="-1"
+                                                class="accesshide"
+                                                {{#checked}}checked{{/checked}}
+                                                {{#disableall}}disabled{{/disableall}} />
+                                            <div class="preference-state-status-container" tabindex="0">
+                                                <span class="on-text">{{#str}} on, message {{/str}}</span>
+                                                <span class="off-text">{{#str}} off, message {{/str}}</span>
+                                                {{> core/loading }}
+                                            </div>
+                                        </label>
+                                    {{/anchor}}
+                                    {{$tooltip}}{{displayname}}{{/tooltip}}
+                                {{/ core/hover_tooltip }}
+                            {{/loggedin}}
+                        </div>
+                        <div class="span6 col-6">
+                            {{#loggedoff}}
+                                {{< core/hover_tooltip }}
+                                    {{$anchor}}
+                                        <label class="preference-state"
+                                            title="{{displayname}}"
+                                            data-state="{{name}}">
+
+                                            <span class="accesshide">{{displayname}}</span>
+                                            <input type="checkbox"
+                                                tabindex="-1"
+                                                class="accesshide"
+                                                {{#checked}}checked{{/checked}}
+                                                {{#disableall}}disabled{{/disableall}} />
+                                            <div class="preference-state-status-container" tabindex="0">
+                                                <span class="on-text">{{#str}} on, message {{/str}}</span>
+                                                <span class="off-text">{{#str}} off, message {{/str}}</span>
+                                                {{> core/loading }}
+                                            </div>
+                                        </label>
+                                    {{/anchor}}
+                                    {{$tooltip}}{{displayname}}{{/tooltip}}
+                                {{/ core/hover_tooltip }}
+                            {{/loggedoff}}
+                        </div>
+                    </div>
+                </div>
+            </form>
+        {{/locked}}
+    </td>
+</tr>
diff --git a/message/tests/behat/message_preferences.feature b/message/tests/behat/message_preferences.feature
new file mode 100644 (file)
index 0000000..4aeac16
--- /dev/null
@@ -0,0 +1,30 @@
+@core @core_message
+Feature: To be able to see and save user message preferences as admin
+  As an admin
+  I need to be able to view and edit message preferences for other users
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | student1 | Student   | 1        | student1@emample.com |
+    And the following "user preferences" exist:
+      | user      | preference                                        | value |
+      | student1  | message_provider_moodle_instantmessage_loggedin   | none  |
+      | student1  | message_provider_moodle_instantmessage_loggedoff  | email |
+
+  @javascript
+  Scenario: As an admin I can view and edit message preferences for a user
+    Given I log in as "admin"
+    And I navigate to "Messaging > Notification settings" in site administration
+    And I set the field "email" to "1"
+    And I press "Save changes"
+    And I navigate to "Users > Accounts > Browse list of users" in site administration
+    And I click on "Student 1" "link" in the "Student 1" "table_row"
+    And I click on "Preferences" "link" in the "#region-main-box" "css_element"
+    And I click on "Message preferences" "link" in the "#region-main-box" "css_element"
+    And I click on "//label[@data-state='loggedoff']" "xpath_element"
+    And I log out
+    And I log in as "student1"
+    And I follow "Preferences" in the user menu
+    And I click on "Message preferences" "link"
+    And the field "Email" matches value "0"
\ No newline at end of file
index efc1f23..be6da94 100644 (file)
@@ -9,7 +9,10 @@ information provided here is intended especially for developers.
   - message_mark_message_read
   - message_can_delete_message
   - message_delete_message
-  * mark_all_read_for_user()
+  - mark_all_read_for_user()
+* Message processors can implement the following methods which will be executed as part of the messaging cleanup task:
+  - cleanup_all_notifications
+  - cleanup_read_notifications
 
 === 3.8 ===
 
index 6c6f64d..f2b1224 100644 (file)
@@ -87,8 +87,9 @@
 </div>
 
 {{#js}}
-require(['jquery', 'core_message/message_send_bulk'], function($, BulkSender) {
-    $('#{{id}}').on('change', function(e) {
+require(['jquery', 'core_message/message_send_bulk', 'core/custom_interaction_events'], function($, BulkSender, CustomEvents) {
+    CustomEvents.define('#{{id}}', [CustomEvents.events.accessibleChange]);
+    $('#{{id}}').on(CustomEvents.events.accessibleChange, function(e) {
         var action = $(e.target).val();
         if (action.indexOf('#') !== -1) {
             e.preventDefault();
index a20f998..c76316d 100644 (file)
@@ -34,20 +34,22 @@ $string['displaycopyright'] = 'Copyright button';
 $string['h5pactivity:addinstance'] = 'Add a new H5P';
 $string['h5pactivity:submit'] = 'Submit H5P attempts';
 $string['h5pactivity:view'] = 'View H5P';
-$string['h5pactivityfieldset'] = 'H5P Settings';
+$string['h5pactivityfieldset'] = 'H5P settings';
 $string['h5pactivityname'] = 'H5P';
 $string['h5pactivitysettings'] = 'Settings';
 $string['h5pdisplay'] = 'H5P options';
-$string['modulename'] = 'H5P activity';
-$string['modulename_help'] = 'Use this module to use a H5P compatible content as a course activity.';
+$string['modulename'] = 'H5P';
+$string['modulename_help'] = 'H5P is an abbreviation for HTML5 Package - interactive content such as presentations, videos and other multimedia, questions, quizzes, games and more. The H5P activity enables H5P to be uploaded and added to a course.
+
+Any question attempts are marked automatically, and the grade is recorded in the gradebook.';
 $string['modulename_link'] = 'mod/h5pactivity/view';
-$string['modulenameplural'] = 'H5P activities';
+$string['modulenameplural'] = 'H5P';
 $string['myattempts'] = 'My attempts';
 $string['package'] = 'Package file';
-$string['package_help'] = 'The package file is a h5pfile containing H5P dynamic content.';
+$string['package_help'] = 'The package file is a h5p file containing H5P interactive content.';
 $string['page-mod-h5pactivity-x'] = 'Any H5P module page';
 $string['pluginadministration'] = 'H5P administration';
-$string['pluginname'] = 'H5P activity';
+$string['pluginname'] = 'H5P';
 $string['previewmode'] = 'This content is displayed in preview mode. No attempt tracking will be stored.';
 $string['privacy:metadata:attempt'] = 'The attempt number';
 $string['privacy:metadata:rawscore'] = 'The score obtained';
index ab92d1e..b5d05e0 100644 (file)
@@ -22,7 +22,7 @@ Feature: Add H5P activity
 
   @javascript
   Scenario: Add a h5pactivity activity to a course
-    When I add a "H5P activity" to section "1"
+    When I add a "H5P" to section "1"
     And I set the following fields to these values:
       | Name        | Awesome H5P package |
       | Description | Description         |
@@ -39,7 +39,7 @@ Feature: Add H5P activity
 
   @javascript
   Scenario: Add a h5pactivity activity with download
-    When I add a "H5P activity" to section "1"
+    When I add a "H5P" to section "1"
     And I set the following fields to these values:
       | Name                       | Awesome H5P package |
       | Description                | Description         |
@@ -56,7 +56,7 @@ Feature: Add H5P activity
 
   @javascript
   Scenario: Add a h5pactivity activity with embed
-    When I add a "H5P activity" to section "1"
+    When I add a "H5P" to section "1"
     And I set the following fields to these values:
       | Name              | Awesome H5P package |
       | Description       | Description         |
@@ -73,7 +73,7 @@ Feature: Add H5P activity
 
   @javascript
   Scenario: Add a h5pactivity activity with copyright
-    When I add a "H5P activity" to section "1"
+    When I add a "H5P" to section "1"
     And I set the following fields to these values:
       | Name                  | Awesome H5P package |
       | Description           | Description         |
@@ -90,7 +90,7 @@ Feature: Add H5P activity
 
   @javascript
   Scenario: Add a h5pactivity activity with copyright in a content without copyright
-    When I add a "H5P activity" to section "1"
+    When I add a "H5P" to section "1"
     And I set the following fields to these values:
       | Name                  | Awesome H5P package |
       | Description           | Description         |
@@ -107,7 +107,7 @@ Feature: Add H5P activity
 
   @javascript
   Scenario: Add a h5pactivity activity to a course with all display options enabled
-    When I add a "H5P activity" to section "1"
+    When I add a "H5P" to section "1"
     And I set the following fields to these values:
       | Name                       | Awesome H5P package |
       | Description                | Description         |
index d6e35f1..929c77b 100644 (file)
@@ -21,7 +21,7 @@ Feature: Do a H5P attempt
       | moodle/h5p:updatelibraries | Allow      | editingteacher | System       |           |
     And I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
-    And I add a "H5P activity" to section "1"
+    And I add a "H5P" to section "1"
     And I set the following fields to these values:
       | Name        | Awesome H5P package |
       | Description | Description         |
index 3359618..bc3b77a 100644 (file)
@@ -123,7 +123,7 @@ class mod_h5pactivity_privacy_testcase extends provider_testcase {
         $this->export_context_data_for_user($this->student1->id, $this->context, $component);
 
         $data = $writer->get_data([]);
-        $this->assertEquals('H5P activity 1', $data->name);
+        $this->assertEquals('H5P 1', $data->name);
 
         $data = $writer->get_data($subcontextattempt1);
         $this->assertCount(1, (array) $data);
index 1d1284b..01adfc7 100644 (file)
@@ -233,8 +233,8 @@ $string['invalidid'] = 'LTI ID was incorrect';
 $string['jwtsecurity'] = 'LTI 1.3';
 $string['keytype'] = 'Public key type';
 $string['keytype_help'] = 'The authentication method used to validate the tool.';
-$string['keytype_keyset'] = 'Keyset Url';
-$string['keytype_rsa'] = 'RSA Key';
+$string['keytype_keyset'] = 'Keyset URL';
+$string['keytype_rsa'] = 'RSA key';
 $string['launch_in_moodle'] = 'Launch tool in Moodle';
 $string['launch_in_popup'] = 'Launch tool in a pop-up';
 $string['launch_url'] = 'Tool URL';
@@ -333,17 +333,17 @@ $string['notypes'] = 'There are currently no LTI tools set up in Moodle. Click t
 $string['noviewusers'] = 'No users were found with permissions to use this tool';
 $string['oauthsecurity'] = 'LTI 1.0/1.1';
 $string['optionalsettings'] = 'Optional settings';
-$string['organization'] = 'Organization details';
-$string['organizationdescr'] = 'Organization description';
-$string['organizationid_default'] = 'Default organization ID';
+$string['organization'] = 'Organisation details';
+$string['organizationdescr'] = 'Organisation description';
+$string['organizationid_default'] = 'Default organisation ID';
 $string['siteid'] = 'Site ID';
 $string['sitehost'] = 'Site hostname';
-$string['organizationid_default_help'] = 'Default value to use for Organization ID. Site ID identifies this installation of moodle.';
-$string['organizationidguid'] = 'Organization ID';
+$string['organizationid_default_help'] = 'The default value to use for Organisation ID. Site ID identifies this installation of Moodle.';
+$string['organizationidguid'] = 'Organisation ID';
 $string['organizationidguid_help'] = 'A unique identifier for this Moodle instance passed to the tool as the Platform Instance GUID.
 
 If this field is left blank, the default value will be used.';
-$string['organizationurl'] = 'Organization URL';
+$string['organizationurl'] = 'Organisation URL';
 $string['organizationurl_help'] = 'The base URL of this Moodle instance.
 
 If this field is left blank, a default value will be used based on the site configuration.';
@@ -404,7 +404,7 @@ $string['privacy:metadata:useridnumber'] = 'The ID number of the user accessing
 $string['privacy:metadata:username'] = 'The username of the user accessing the LTI Consumer';
 $string['publickey'] = 'Public key';
 $string['publickeyset'] = 'Public keyset';
-$string['publickeyset_help'] = 'Public keyset from where moodle will retrieve the tool\'s public key to allow signatures of incoming messages and service requests to be verified.';
+$string['publickeyset_help'] = 'Public keyset from where this site will retrieve the tool\'s public key to allow signatures of incoming messages and service requests to be verified.';
 $string['publickey_help'] = 'The public key (in PEM format) provided by the tool to allow signatures of incoming messages and service requests to be verified.';
 $string['quickgrade'] = 'Allow quick grading';
 $string['quickgrade_help'] = 'If enabled, multiple tools can be graded on one page. Add grades and comments then click the "Save all my feedback" button to save all changes for that page.';
@@ -589,7 +589,7 @@ $string['validurl'] = 'A valid URL must start with http(s)://';
 $string['viewsubmissions'] = 'View submissions and grading screen';
 
 // Deprecated since Moodle 3.9.
-$string['organizationid'] = 'Organization ID';
-$string['organizationid_help'] = 'A unique identifier for this Moodle instance. Typically, the DNS name of the organization is used.
+$string['organizationid'] = 'Organisation ID';
+$string['organizationid_help'] = 'A unique identifier for this Moodle instance. Typically, the DNS name of the organisation is used.
 
 If this field is left blank, the host name of this Moodle site will be used as the default value.';
index 3b67a43..e62d558 100644 (file)
@@ -27,8 +27,8 @@
 defined('MOODLE_INTERNAL') || die();
 
 
-$string['confirmstartheader'] = 'Timed quiz';
-$string['confirmstart'] = 'The quiz has a time limit of {$a}. Time will count down from the moment you start your attempt and you must submit before it expires. Are you sure that you wish to start now?';
+$string['confirmstartheader'] = 'Time limit';
+$string['confirmstart'] = 'Your attempt will have a time limit of {$a}. When you start, the timer will begin to count down and cannot be paused. You must finish your attempt before it expires. Are you sure you wish to start now?';
 $string['pluginname'] = 'Time limit quiz access rule';
 $string['privacy:metadata'] = 'The Time limit quiz access rule plugin does not store any personal data.';
 $string['quiztimelimit'] = 'Time limit: {$a}';
index 5b164e1..5353bc9 100644 (file)
@@ -156,13 +156,13 @@ $string['cannotstartmissingquestion'] = 'Cannot start an attempt at this quiz. T
 $string['cannotstartnoquestions'] = 'Cannot start an attempt at this quiz. The quiz has not been set up yet. No questions have been added.';
 $string['cannotwrite'] = 'Cannot write to export file ({$a})';
 $string['canredoquestions'] = 'Allow redo within an attempt';
-$string['canredoquestions_desc'] = 'If enabled, when a student has finished attempting a question, they will see a Redo question button. This allows them to attempt another version of the same question, without having to submit the entire quiz attempt and start another attempt. This option is useful for practice quizzes.
+$string['canredoquestions_desc'] = 'If enabled, after finishing attempting a question, a \'Try another question like this one\' button is displayed. This allows for a similar question (selected randomly) to be attempted, or the same question again, without the entire quiz attempt having to be submitted and another attempt started. This option is useful for practice quizzes.
 
-This setting only affects questions and behaviours (such as immediate feedback or interactive with multiple tries) where it is possible for student to finish the question before the attempt is submitted.';
-$string['canredoquestions_help'] = 'If enabled, when a student has finished attempting a question, they will see a Redo question button. This allows them to attempt another version of the same question, without having to submit the entire quiz attempt and start another attempt. This option is useful for practice quizzes.
+This setting only affects questions and behaviours (such as immediate feedback or interactive with multiple tries) where it is possible to finish a question before the attempt is submitted.';
+$string['canredoquestions_help'] = 'If enabled, after finishing attempting a question, a \'Try another question like this one\' button is displayed. This allows for a similar question (selected randomly) to be attempted, or the same question again, without the entire quiz attempt having to be submitted and another attempt started. This option is useful for practice quizzes.
 
-This setting only affects questions and behaviours (such as immediate feedback or interactive with multiple tries) where it is possible for student to finish the question before the attempt is submitted.';
-$string['canredoquestionsyes'] = 'Students may redo another version of any finished question';
+This setting only affects questions and behaviours (such as immediate feedback or interactive with multiple tries) where it is possible to finish a question before the attempt is submitted.';
+$string['canredoquestionsyes'] = 'Yes, provide the option to try another question';
 $string['caseno'] = 'No, case is unimportant';
 $string['casesensitive'] = 'Case sensitivity';
 $string['caseyes'] = 'Yes, case must match';
index 5ce5fb2..2f72ced 100644 (file)
@@ -44,7 +44,7 @@ Feature: The various checks that may happen when an attept is started
     When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
     And I press "Attempt quiz now"
     Then I should see "To attempt this quiz you need to know the quiz password" in the "Start attempt" "dialogue"
-    And I should see "The quiz has a time limit of 1 hour. Time will " in the "Start attempt" "dialogue"
+    And I should see "Your attempt will have a time limit of 1 hour. When you " in the "Start attempt" "dialogue"
     And I set the field "Quiz password" to "Frog"
     And I click on "Start attempt" "button" in the "Start attempt" "dialogue"
     And I should see "Text of the first question"
@@ -77,7 +77,7 @@ Feature: The various checks that may happen when an attept is started
     And I click on "Start attempt" "button" in the "Start attempt" "dialogue"
     Then I should see "Quiz 1 description"
     And I should see "To attempt this quiz you need to know the quiz password"
-    And I should see "The quiz has a time limit of 1 hour. Time will "
+    And I should see "Your attempt will have a time limit of 1 hour. When you "
     And I should see "The password entered was incorrect"
     And I set the field "Quiz password" to "Frog"
     # On Mac/FF tab key is needed as text field in dialogue and page have same id.
@@ -99,7 +99,7 @@ Feature: The various checks that may happen when an attept is started
     And I click on "Start attempt" "button" in the "Start attempt" "dialogue"
     And I should see "Quiz 1 description"
     And I should see "To attempt this quiz you need to know the quiz password"
-    And I should see "The quiz has a time limit of 1 hour. Time will "
+    And I should see "Your attempt will have a time limit of 1 hour. When you "
     And I should see "The password entered was incorrect"
     And I set the field "Quiz password" to "Frog"
     # On Mac/FF tab key is needed as text field in dialogue and page have same id.
index c5e04d8..0992e78 100644 (file)
@@ -49,7 +49,7 @@ Feature: File types of the submission and feedback attachments can be limitted
     And I follow "moodlelogo.png"
     And I set the field "Name" to "testable.php"
     And I press "Update"
-    And I should see "The original file extension has been modified as a part of the file name change. Changing the extension from \".png\" to \".php\" could potentially cause some side effects."
+    And I should see "The original file extension has been modified as a part of the file name change. Changing the extension from \".png\" to \".php\" may result in a file which cannot be opened."
     And I click on "OK" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element"
     When I press "Save changes"
     Then I should see "Some files (testable.php) cannot be uploaded. Only file types image are allowed."
@@ -112,7 +112,7 @@ Feature: File types of the submission and feedback attachments can be limitted
     And I follow "testable.php"
     And I set the field "Name" to "renamed.png"
     And I press "Update"
-    And I should see "The original file extension has been modified as a part of the file name change. Changing the extension from \".php\" to \".png\" could potentially cause some side effects."
+    And I should see "The original file extension has been modified as a part of the file name change. Changing the extension from \".php\" to \".png\" may result in a file which cannot be opened."
     And I click on "OK" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element"
     When I press "Save and close"
     Then I should see "Some files (renamed.png) cannot be uploaded. Only file types .php are allowed."
index 7c8c59f..674873e 100644 (file)
@@ -58,7 +58,9 @@ $string['pluginname_help'] = 'Drag and drop onto image questions require the res
 $string['pluginname_link'] = 'question/type/ddimageortext';
 $string['pluginnameadding'] = 'Adding drag and drop onto image';
 $string['pluginnameediting'] = 'Editing drag and drop onto image';
-$string['pluginnamesummary'] = 'Images or text labels are dragged and dropped into drop zones on a background image.';
+$string['pluginnamesummary'] = 'Images or text labels are dragged and dropped into drop zones on a background image.
+
+Note: This question type is not accessible to users who are visually impaired.';
 $string['previewareaheader'] = 'Preview';
 $string['previewareamessage'] = 'Select a background image, specify draggable items and define drop zones on the background image into which they must be dragged.';
 $string['privacy:metadata'] = 'The Drag and drop onto image question type plugin does not store any personal data.';
index eedd478..28d7d6f 100644 (file)
@@ -76,7 +76,9 @@ $string['pluginname_help'] = 'Drag and drop markers require the respondent to dr
 $string['pluginname_link'] = 'question/type/ddmarker';
 $string['pluginnameadding'] = 'Adding drag and drop markers';
 $string['pluginnameediting'] = 'Editing drag and drop markers';
-$string['pluginnamesummary'] = 'Markers are dragged and dropped onto a background image.';
+$string['pluginnamesummary'] = 'Markers are dragged and dropped onto a background image.
+
+Note: This question type is not accessible to users who are visually impaired.';
 $string['previewareaheader'] = 'Preview';
 $string['previewareamessage'] = 'Select a background image file, enter text labels for markers and define the drop zones on the background image to which they must be dragged.';
 $string['privacy:metadata'] = 'The Drag and drop markers question type plugin does not store any personal data.';
index 7e7eef5..56be84f 100644 (file)
Binary files a/user/amd/build/participants.min.js and b/user/amd/build/participants.min.js differ
index 2f05fe5..d204dba 100644 (file)
Binary files a/user/amd/build/participants.min.js.map and b/user/amd/build/participants.min.js.map differ
diff --git a/user/amd/build/repository.min.js b/user/amd/build/repository.min.js
new file mode 100644 (file)
index 0000000..5ff57f0
Binary files /dev/null and b/user/amd/build/repository.min.js differ
diff --git a/user/amd/build/repository.min.js.map b/user/amd/build/repository.min.js.map
new file mode 100644 (file)
index 0000000..7d60e60
Binary files /dev/null and b/user/amd/build/repository.min.js.map differ
index 7a8a14c..255f72e 100644 (file)
Binary files a/user/amd/build/status_field.min.js and b/user/amd/build/status_field.min.js differ
index 5dd1fb9..7a660a9 100644 (file)
Binary files a/user/amd/build/status_field.min.js.map and b/user/amd/build/status_field.min.js.map differ
index abb4a29..d2156d5 100644 (file)
@@ -22,8 +22,9 @@
  * @copyright  2017 Damyon Wiese
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/templates', 'core/notification', 'core/ajax'],
-        function($, Str, ModalFactory, ModalEvents, Templates, Notification, Ajax) {
+define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/templates', 'core/notification', 'core/ajax',
+        'core/custom_interaction_events'],
+        function($, Str, ModalFactory, ModalEvents, Templates, Notification, Ajax, CustomEvents) {
 
     var SELECTORS = {
         BULKACTIONSELECT: "#formactionid",
@@ -82,7 +83,8 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/t
      * @private
      */
     Participants.prototype.attachEventListeners = function() {
-        $(SELECTORS.BULKACTIONSELECT).on('change', function(e) {
+        CustomEvents.define(SELECTORS.BULKACTIONSELECT, [CustomEvents.events.accessibleChange]);
+        $(SELECTORS.BULKACTIONSELECT).on(CustomEvents.events.accessibleChange, function(e) {
             var action = $(e.target).val();
             if (action.indexOf('#') !== -1) {
                 e.preventDefault();
diff --git a/user/amd/src/repository.js b/user/amd/src/repository.js
new file mode 100644 (file)
index 0000000..7be9378
--- /dev/null
@@ -0,0 +1,53 @@
+// 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 to handle AJAX interactions.
+ *
+ * @module     core_user/repository
+ * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+import {call as fetchMany} from 'core/ajax';
+
+/**
+ * Unenrol the user with the specified user enrolmentid ID.
+ *
+ * @param {Number} userEnrolmentId
+ * @return {Promise}
+ */
+export const unenrolUser = userEnrolmentId => {
+    return fetchMany([{
+        methodname: 'core_enrol_unenrol_user_enrolment',
+        args: {
+            ueid: userEnrolmentId,
+        },
+    }])[0];
+};
+
+/**
+ * Submit the user enrolment form with the specified form data.
+ *
+ * @param {String} formdata
+ * @return {Promise}
+ */
+export const submitUserEnrolmentForm = formdata => {
+    return fetchMany([{
+        methodname: 'core_enrol_submit_user_enrolment_form',
+        args: {
+            formdata,
+        },
+    }])[0];
+};
index c03d9de..1563120 100644 (file)
  * @copyright  2017 Jun Pataleta
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['core/templates',
-        'jquery',
-        'core/str',
-        'core/config',
-        'core/notification',
-        'core/modal_factory',
-        'core/modal_events',
-        'core/fragment',
-        'core/ajax'
-    ],
-    function(Template, $, Str, Config, Notification, ModalFactory, ModalEvents, Fragment, Ajax) {
-
-        /**
-         * Action selectors.
-         *
-         * @access private
-         * @type {{EDIT_ENROLMENT: string, SHOW_DETAILS: string, UNENROL: string}}
-         */
-        var SELECTORS = {
-            EDIT_ENROLMENT: '[data-action="editenrolment"]',
-            SHOW_DETAILS: '[data-action="showdetails"]',
-            UNENROL: '[data-action="unenrol"]'
-        };
-
-        /**
-         * Constructor
-         *
-         * @param {Object} options Object containing options. The only valid option at this time is contextid.
-         * Each call to templates.render gets it's own instance of this class.
-         */
-        var StatusFieldActions = function(options) {
-            this.contextid = options.contextid;
-            this.courseid = options.courseid;
-
-            // Bind click event to editenrol buttons.
-            this.bindEditEnrol();
-
-            // Bind click event to unenrol buttons.
-            this.bindUnenrol();
-
-            // Bind click event to status details buttons.
-            this.bindStatusDetails();
-        };
-        // Class variables and functions.
-
-        /** @var {number} courseid The course ID. */
-        StatusFieldActions.prototype.courseid = 0;
-
-        /**
-         * Private method
-         *
-         * @method initModal
-         * @private
-         */
-        StatusFieldActions.prototype.bindEditEnrol = function() {
-            var statusFieldInstsance = this;
-
-            $(SELECTORS.EDIT_ENROLMENT).click(function(e) {
-                e.preventDefault();
 
-                // The particular edit button that was clicked.
-                var clickedEditTrigger = $(this);
-                // Get the parent container (it contains the data attributes associated with the status field).
-                var parentContainer = clickedEditTrigger.parent();
-                // Get the name of the user whose enrolment status is being edited.
-                var fullname = parentContainer.data('fullname');
-                // Get the user enrolment ID.
-                var ueid = clickedEditTrigger.attr('rel');
-
-                $.when(Str.get_string('edituserenrolment', 'enrol', fullname)).then(function(modalTitle) {
-                    return ModalFactory.create({
-                        large: true,
-                        title: modalTitle,
-                        type: ModalFactory.types.SAVE_CANCEL
-                    });
-                }).done(function(modal) {
-                    // Handle save event.
-                    modal.getRoot().on(ModalEvents.save, function(e) {
-                        // Don't close the modal yet.
-                        e.preventDefault();
-                        // Submit form data.
-                        statusFieldInstsance.submitEditFormAjax(modal);
-                    });
-
-                    // Handle hidden event.
-                    modal.getRoot().on(ModalEvents.hidden, function() {
-                        // Destroy when hidden.
-                        modal.destroy();
-                    });
-
-                    // Set the modal body.
-                    modal.setBody(statusFieldInstsance.getBody(ueid));
-
-                    // Show the modal!
-                    modal.show();
-                }).fail(Notification.exception);
-            });
-        };
-
-        /**
-         * Private method
-         *
-         * @method bindUnenrol
-         * @private
-         */
-        StatusFieldActions.prototype.bindUnenrol = function() {
-            var statusFieldInstsance = this;
-
-            $(SELECTORS.UNENROL).click(function(e) {
-                e.preventDefault();
-                var unenrolLink = $(this);
-                var parentContainer = unenrolLink.parent();
-                var strings = [
-                    {
-                        key: 'unenrol',
-                        component: 'enrol'
-                    },
-                    {
-                        key: 'unenrolconfirm',
-                        component: 'enrol',
-                        param: {
-                            user: parentContainer.data('fullname'),
-                            course: parentContainer.data('coursename'),
-                            enrolinstancename: parentContainer.data('enrolinstancename')
-                        }
-                    }
-                ];
-
-                var deleteModalPromise = ModalFactory.create({
-                    type: ModalFactory.types.SAVE_CANCEL
-                });
-
-                $.when(Str.get_strings(strings), deleteModalPromise).done(function(results, modal) {
-                    var title = results[0];
-                    var confirmMessage = results[1];
-                    modal.setTitle(title);
-                    modal.setBody(confirmMessage);
-                    modal.setSaveButtonText(title);
-
-                    // Handle confirm event.
-                    modal.getRoot().on(ModalEvents.save, function() {
-                        // Build params.
-                        var unenrolParams = {
-                            'ueid': $(unenrolLink).attr('rel')
-                        };
-                        // Don't close the modal yet.
-                        e.preventDefault();
-                        // Submit data.
-                        statusFieldInstsance.submitUnenrolFormAjax(modal, unenrolParams);
-                    });
-
-                    // Handle hidden event.
-                    modal.getRoot().on(ModalEvents.hidden, function() {
-                        // Destroy when hidden.
-                        modal.destroy();
-                    });
-
-                    // Display the delete confirmation modal.
-                    modal.show();
-                }).fail(Notification.exception);
-            });
-        };
-
-        /**
-         * Private method
-         *
-         * @method bindStatusDetails
-         * @private
-         */
-        StatusFieldActions.prototype.bindStatusDetails = function() {
-            $(SELECTORS.SHOW_DETAILS).click(function(e) {
-                e.preventDefault();
+import * as DynamicTable from 'core_table/dynamic';
+import * as Repository from './repository';
+import * as Str from 'core/str';
+import DynamicTableSelectors from 'core_table/local/dynamic/selectors';
+import Fragment from 'core/fragment';
+import ModalEvents from 'core/modal_events';
+import ModalFactory from 'core/modal_factory';
+import Notification from 'core/notification';
+import Templates from 'core/templates';
+import {add as notifyUser} from 'core/toast';
+
+const Selectors = {
+    editEnrolment: '[data-action="editenrolment"]',
+    showDetails: '[data-action="showdetails"]',
+    unenrol: '[data-action="unenrol"]',
+    statusElement: '[data-status]',
+};
 
-                var detailsButton = $(this);
-                var parentContainer = detailsButton.parent();
-                var context = {
-                    "fullname": parentContainer.data('fullname'),
-                    "coursename": parentContainer.data('coursename'),
-                    "enrolinstancename": parentContainer.data('enrolinstancename'),
-                    "status": parentContainer.data('status'),
-                    "statusclass": parentContainer.find('span').attr('class'),
-                    "timestart": parentContainer.data('timestart'),
-                    "timeend": parentContainer.data('timeend'),
-                    "timeenrolled": parentContainer.data('timeenrolled')
-                };
-
-                // Get default string for the modal and modal type.
-                var strings = [
-                    {
-                        key: 'enroldetails',
-                        component: 'enrol'
-                    }
-                ];
-
-                // Find the edit enrolment link.
-                var editEnrolLink = detailsButton.next(SELECTORS.EDIT_ENROLMENT);
-                if (editEnrolLink.length) {
-                    // If there's an edit enrolment link for this user, clone it into the context for the modal.
-                    context.editenrollink = $('<div>').append(editEnrolLink.clone()).html();
-                }
+/**
+ * Get the dynamic table from the specified link.
+ *
+ * @param {HTMLElement} link
+ * @returns {HTMLElement}
+ */
+const getDynamicTableFromLink = link => link.closest(DynamicTableSelectors.main.region);
 
-                var modalStringsPromise = Str.get_strings(strings);
-                var modalPromise = ModalFactory.create({large: true, type: ModalFactory.types.CANCEL});
-                $.when(modalStringsPromise, modalPromise).done(function(strings, modal) {
-                    var modalBodyPromise = Template.render('core_user/status_details', context);
-                    modal.setTitle(strings[0]);
-                    modal.setBody(modalBodyPromise);
-
-                    if (editEnrolLink.length) {
-                        modal.getRoot().on('click', SELECTORS.EDIT_ENROLMENT, function(e) {
-                            e.preventDefault();
-                            modal.hide();
-                            // Trigger click event for the edit enrolment link to show the edit enrolment modal.
-                            $(editEnrolLink).trigger('click');
-                        });
-                    }
-
-                    modal.show();
-
-                    // Handle hidden event.
-                    modal.getRoot().on(ModalEvents.hidden, function() {
-                        // Destroy when hidden.
-                        modal.destroy();
-                    });
-                }).fail(Notification.exception);
-            });
-        };
-
-        /**
-         * Private method
-         *
-         * @method submitEditFormAjax
-         * @param {Object} modal The the AMD modal object containing the form.
-         * @private
-         */
-        StatusFieldActions.prototype.submitEditFormAjax = function(modal) {
-            var statusFieldInstsance = this;
-            var form = modal.getRoot().find('form');
-
-            // User enrolment ID.
-            var ueid = $(form).find('[name="ue"]').val();
-
-            var request = {
-                methodname: 'core_enrol_submit_user_enrolment_form',
-                args: {
-                    formdata: form.serialize()
-                }
-            };
-
-            Ajax.call([request])[0].done(function(data) {
-                if (data.result) {
-                    // Dismiss the modal.
-                    modal.hide();
-
-                    // Reload the page, don't show changed data warnings.
-                    if (typeof window.M.core_formchangechecker !== "undefined") {
-                        window.M.core_formchangechecker.reset_form_dirty_state();
-                    }
-                    window.location.reload();
-                } else {
-                    // Serialise the form data and reload the form fragment to show validation errors.
-                    var formData = JSON.stringify(form.serialize());
-                    modal.setBody(statusFieldInstsance.getBody(ueid, formData));
-                }
-            }).fail(Notification.exception);
-        };
-
-        /**
-         * Private method
-         *
-         * @method submitUnenrolFormAjax
-         * @param {Object} modal The the AMD modal object containing the form.
-         * @param {Object} unenrolParams The unenrol parameters.
-         * @private
-         */
-        StatusFieldActions.prototype.submitUnenrolFormAjax = function(modal, unenrolParams) {
-            var request = {
-                methodname: 'core_enrol_unenrol_user_enrolment',
-                args: unenrolParams
-            };
-
-            Ajax.call([request])[0].done(function(data) {
-                if (data.result) {
-                    // Dismiss the modal.
-                    modal.hide();
-
-                    // Reload the page, don't show changed data warnings.
-                    if (typeof window.M.core_formchangechecker !== "undefined") {
-                        window.M.core_formchangechecker.reset_form_dirty_state();
-                    }
-                    window.location.reload();
-                } else {
-                    // Display an alert containing the error message
-                    Notification.alert(data.errors[0].key, data.errors[0].message);
+/**
+ * Get the status container from the specified link.
+ *
+ * @param {HTMLElement} link
+ * @returns {HTMLElement}
+ */
+const getStatusContainer = link => link.closest(Selectors.statusElement);
+
+/**
+ * Get user enrolment id from the specified link
+ *
+ * @param {HTMLElement} link
+ * @returns {Number}
+ */
+const getUserEnrolmentIdFromLink = link => link.getAttribute('rel');
+
+/**
+ * Register all event listeners for the status fields.
+ *
+ * @param {Number} contextId
+ * @param {Number} uniqueId
+ */
+const registerEventListeners = (contextId, uniqueId) => {
+    const getBodyFunction = (userEnrolmentId, formData) => getBody(contextId, userEnrolmentId, formData);
+
+    document.addEventListener('click', e => {
+        const tableRoot = e.target.closest(DynamicTableSelectors.main.fromRegionId(uniqueId));
+        if (!tableRoot) {
+            return;
+        }
+
+        const editLink = e.target.closest(Selectors.editEnrolment);
+        if (editLink) {
+            e.preventDefault();
+
+            showEditDialogue(editLink, getBodyFunction);
+        }
+
+        const unenrolLink = e.target.closest(Selectors.unenrol);
+        if (unenrolLink) {
+            e.preventDefault();
+
+            showUnenrolConfirmation(unenrolLink);
+        }
+
+        const showDetailsLink = e.target.closest(Selectors.showDetails);
+        if (showDetailsLink) {
+            e.preventDefault();
+
+            showStatusDetails(showDetailsLink);
+        }
+    });
+};
+
+/**
+ * Show the edit dialogue.
+ *
+ * @param {HTMLElement} link
+ * @param {Function} getBody Function to get the body for the specified user enrolment
+ */
+const showEditDialogue = (link, getBody) => {
+    const container = getStatusContainer(link);
+    const userEnrolmentId = getUserEnrolmentIdFromLink(link);
+
+    ModalFactory.create({
+        large: true,
+        title: Str.get_string('edituserenrolment', 'enrol', container.dataset.fullname),
+        type: ModalFactory.types.SAVE_CANCEL,
+        body: getBody(userEnrolmentId)
+    })
+    .then(modal => {
+        // Handle save event.
+        modal.getRoot().on(ModalEvents.save, e => {
+            // Don't close the modal yet.
+            e.preventDefault();
+
+            // Submit form data.
+            submitEditFormAjax(link, getBody, modal, userEnrolmentId, container.dataset);
+        });
+
+        // Handle hidden event.
+        modal.getRoot().on(ModalEvents.hidden, () => {
+            // Destroy when hidden.
+            modal.destroy();
+        });
+
+        // Show the modal.
+        modal.show();
+
+        return modal;
+    })
+    .catch(Notification.exception);
+};
+
+/**
+ * Show and handle the unenrolment confirmation dialogue.
+ *
+ * @param {HTMLElement} link
+ */
+const showUnenrolConfirmation = link => {
+    const container = getStatusContainer(link);
+    const userEnrolmentId = getUserEnrolmentIdFromLink(link);
+
+    ModalFactory.create({
+        type: ModalFactory.types.SAVE_CANCEL,
+    })
+    .then(modal => {
+        // Handle confirm event.
+        modal.getRoot().on(ModalEvents.save, e => {
+            // Don't close the modal yet.
+            e.preventDefault();
+
+            // Submit data.
+            submitUnenrolFormAjax(
+                link,
+                modal,
+                {
+                    ueid: userEnrolmentId,
+                },
+                container.dataset
+            );
+        });
+
+        // Handle hidden event.
+        modal.getRoot().on(ModalEvents.hidden, () => {
+            // Destroy when hidden.
+            modal.destroy();
+        });
+
+        // Display the delete confirmation modal.
+        modal.show();
+
+        const stringData = [
+            {
+                key: 'unenrol',
+                component: 'enrol',
+            },
+            {
+                key: 'unenrolconfirm',
+                component: 'enrol',
+                param: {
+                    user: container.dataset.fullname,
+                    course: container.dataset.coursename,
+                    enrolinstancename: container.dataset.enrolinstancename,
                 }
-            }).fail(Notification.exception);
-        };
-
-        /**
-         * Private method
-         *
-         * @method getBody
-         * @private
-         * @param {Number} ueid The user enrolment ID associated with the user.
-         * @param {string} formData Serialized string of the edit enrolment form data.
-         * @return {Promise}
-         */
-        StatusFieldActions.prototype.getBody = function(ueid, formData) {
-            var params = {
-                'ueid': ueid
-            };
-            if (typeof formData !== 'undefined') {
-                params.formdata = formData;
             }
-            return Fragment.loadFragment('enrol', 'user_enrolment_form', this.contextid, params).fail(Notification.exception);
-        };
-
-        return /** @alias module:core_user/editenrolment */ {
-            // Public variables and functions.
-            /**
-             * Every call to init creates a new instance of the class with it's own event listeners etc.
-             *
-             * @method init
-             * @public
-             * @param {object} config - config variables for the module.
-             */
-            init: function(config) {
-                (new StatusFieldActions(config));
-            }
-        };
+        ];
+
+        return Promise.all([Str.get_strings(stringData), modal]);
+    })
+    .then(([strings, modal]) => {
+        modal.setTitle(strings[0]);
+        modal.setSaveButtonText(strings[0]);
+        modal.setBody(strings[1]);
+
+        return modal;
+    })
+    .catch(Notification.exception);
+};
+
+/**
+ * Show the user details dialogue.
+ *
+ * @param {HTMLElement} link
+ */
+const showStatusDetails = link => {
+    const container = getStatusContainer(link);
+
+    const context = {
+        editenrollink: '',
+        statusclass: container.querySelector('span.badge').getAttribute('class'),
+        ...container.dataset,
+    };
+
+    // Find the edit enrolment link.
+    const editEnrolLink = container.querySelector(Selectors.editEnrolment);
+    if (editEnrolLink) {
+        // If there's an edit enrolment link for this user, clone it into the context for the modal.
+        context.editenrollink = editEnrolLink.outerHTML;
+    }
+
+    ModalFactory.create({
+        large: true,
+        type: ModalFactory.types.CANCEL,
+        title: Str.get_string('enroldetails', 'enrol'),
+        body: Templates.render('core_user/status_details', context),
+    })
+    .then(modal => {
+        if (editEnrolLink) {
+            modal.getRoot().on('click', Selectors.editEnrolment, e => {
+                e.preventDefault();
+                modal.hide();
+
+                // Trigger click event for the edit enrolment link to show the edit enrolment modal.
+                editEnrolLink.click();
+            });
+        }
+
+        modal.show();
+
+        // Handle hidden event.
+        modal.getRoot().on(ModalEvents.hidden, () => modal.destroy());
+
+        return modal;
+    })
+    .catch(Notification.exception);
+};
+
+/**
+ * Submit the edit dialogue.
+ *
+ * @param {HTMLElement} clickedLink
+ * @param {Function} getBody
+ * @param {Object} modal
+ * @param {Number} userEnrolmentId
+ * @param {Object} userData
+ */
+const submitEditFormAjax = (clickedLink, getBody, modal, userEnrolmentId, userData) => {
+    const form = modal.getRoot().find('form');
+
+    Repository.submitUserEnrolmentForm(form.serialize())
+    .then(data => {
+        if (!data.result) {
+            throw data.result;
+        }
+
+        // Dismiss the modal.
+        modal.hide();
+        modal.destroy();
+
+        return data;
+    })
+    .then(() => {
+        DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink));
+
+        return Str.get_string('enrolmentupdatedforuser', 'core_enrol', userData);
+    })
+    .then(notificationString => {
+        notifyUser(notificationString);
+
+        return;
+    })
+    .catch(() => {
+        modal.setBody(getBody(userEnrolmentId, JSON.stringify(form.serialize())));
+
+        return modal;
     });
+};
+
+/**
+ * Submit the unenrolment form.
+ *
+ * @param {HTMLElement} clickedLink
+ * @param {Object} modal
+ * @param {Object} args
+ * @param {Object} userData
+ */
+const submitUnenrolFormAjax = (clickedLink, modal, args, userData) => {
+    Repository.unenrolUser(args.ueid)
+    .then(data => {
+        if (!data.result) {
+            // Display an alert containing the error message
+            Notification.alert(data.errors[0].key, data.errors[0].message);
+
+            return data;
+        }
+
+        // Dismiss the modal.
+        modal.hide();
+        modal.destroy();
+
+        return data;
+    })
+    .then(() => {
+        DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink));
+
+        return Str.get_string('unenrolleduser', 'core_enrol', userData);
+    })
+    .then(notificationString => {
+        notifyUser(notificationString);
+
+        return;
+    })
+    .catch(Notification.exception);
+};
+
+/**
+ * Get the body fragment.
+ *
+ * @param {Number} contextId
+ * @param {Number} ueid The user enrolment id
+ * @param {Object} formdata
+ * @returns {Promise}
+ */
+const getBody = (contextId, ueid, formdata = null) => Fragment.loadFragment(
+    'enrol',
+    'user_enrolment_form',
+    contextId,
+    {
+        ueid,
+        formdata,
+    }
+);
+
+/**
+ * Initialise the statu field handler.
+ *
+ * @param {Number} contextid
+ * @param {Number} uniqueid
+ */
+export const init = ({contextid, uniqueid}) => {
+    registerEventListeners(contextid, uniqueid);
+};
index 2f9c5ca..b1f4395 100644 (file)
@@ -239,7 +239,10 @@ class participants extends \table_sql implements dynamic_table {
         parent::out($pagesize, $useinitialsbar, $downloadhelpbutton);
 
         if (has_capability('moodle/course:enrolreview', $this->context)) {
-            $params = ['contextid' => $this->context->id, 'courseid' => $this->course->id];
+            $params = [
+                'contextid' => $this->context->id,
+                'uniqueid' => $this->uniqueid,
+            ];
             $PAGE->requires->js_call_amd('core_user/status_field', 'init', [$params]);
         }
     }
index 6d0ed76..b86dee7 100644 (file)
@@ -18,9 +18,9 @@ Feature: As a user, "Course preferences" allows me to set my course preference(s
     Given the field "enableactivitychooser" matches value "1"
     # See that the "activity chooser" is actually shown by default in course page.
     When I am on "Course 1" course homepage
-    And I should not see "Add an activity or resource" in the "Topic 1" "section"
+    And I should not see "Add an activity" in the "Topic 1" "section"
     And I turn editing mode on
-    Then I should see "Add an activity or resource" in the "Topic 1" "section"
+    Then I should see "Add an activity" in the "Topic 1" "section"
     And I should not see "Add a resource..." in the "Topic 1" "section"
 
   @javascript
@@ -30,5 +30,4 @@ Feature: As a user, "Course preferences" allows me to set my course preference(s
     When I am on "Course 1" course homepage
     And I should not see "Add a resource..." in the "Topic 1" "section"
     And I turn editing mode on
-    Then I should see "Add a resource..." in the "Topic 1" "section"
-    And I should not see "Add an activity or resource" in the "Topic 1" "section"
\ No newline at end of file
+    Then I should see "Add a resource..." in the "Topic 1" "section"
\ No newline at end of file