Merge branch 'MDL-48362-master' of git://github.com/damyon/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 29 Feb 2016 06:46:20 +0000 (14:46 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 29 Feb 2016 06:46:20 +0000 (14:46 +0800)
71 files changed:
admin/tool/task/scheduledtasks.php
admin/tool/templatelibrary/tests/externallib_test.php
admin/user/user_bulk_cohortadd.php
badges/action.php
cache/README.md
cache/classes/definition.php
cache/classes/dummystore.php
cache/classes/loaders.php
cache/classes/store.php
cache/forms.php
cache/locallib.php
cache/stores/file/lib.php
cache/stores/memcache/lib.php
cache/stores/memcached/lib.php
cache/stores/mongodb/lib.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
cache/upgrade.txt
course/delete.php
lang/en/cache.php
lib/amd/build/notification.min.js
lib/amd/src/notification.js
lib/classes/notification.php [new file with mode: 0644]
lib/classes/output/notification.php
lib/classes/session/manager.php
lib/db/caches.php
lib/db/services.php
lib/deprecatedlib.php
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/selection.js
lib/external/externallib.php
lib/outputrenderers.php
lib/templates/notification_error.mustache [moved from lib/templates/notification_redirect.mustache with 57% similarity]
lib/templates/notification_info.mustache [moved from lib/templates/notification_problem.mustache with 57% similarity]
lib/templates/notification_success.mustache
lib/templates/notification_warning.mustache [moved from lib/templates/notification_message.mustache with 57% similarity]
lib/tests/notification_test.php [new file with mode: 0644]
lib/tests/session_manager_test.php
lib/tests/sessionlib_test.php
lib/upgrade.txt
lib/weblib.php
mod/assign/feedback/comments/locallib.php
mod/assign/feedback/comments/tests/comments_test.php [new file with mode: 0644]
mod/assign/feedback/editpdf/locallib.php
mod/assign/feedback/editpdf/tests/editpdf_test.php
mod/assign/feedback/file/locallib.php
mod/assign/feedback/file/tests/file_test.php [new file with mode: 0644]
mod/assign/feedbackplugin.php
mod/assign/locallib.php
mod/assign/tests/locallib_test.php
mod/assign/upgrade.txt
mod/forum/index.php
mod/forum/maildigest.php
mod/forum/post.php
mod/forum/subscribe.php
mod/forum/tests/behat/discussion_subscriptions.feature
mod/forum/tests/behat/forum_subscriptions.feature
mod/forum/tests/behat/forum_subscriptions_default.feature
mod/imscp/view.php
mod/quiz/report/overview/report.php
my/tests/behat/reset_all_pages.feature
tag/tests/behat/flag_tags.feature
theme/base/templates/core/notification_error.mustache [moved from theme/base/templates/core/notification_problem.mustache with 100% similarity]
theme/base/templates/core/notification_info.mustache [moved from theme/base/templates/core/notification_redirect.mustache with 100% similarity]
theme/base/templates/core/notification_warning.mustache [moved from theme/base/templates/core/notification_message.mustache with 100% similarity]
theme/upgrade.txt
user/emailupdate.php
user/view.php
version.php

index 4b6f344..ed6e53c 100644 (file)
@@ -87,13 +87,9 @@ if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchange
 
         try {
             \core\task\manager::configure_scheduled_task($task);
-            $url = $PAGE->url;
-            $url->params(array('success'=>get_string('changessaved')));
-            redirect($url);
+            redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
         } catch (Exception $e) {
-            $url = $PAGE->url;
-            $url->params(array('error'=>$e->getMessage()));
-            redirect($url);
+            redirect($PAGE->url, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR);
         }
     } else {
         echo $OUTPUT->header();
@@ -104,19 +100,7 @@ if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchange
 
 } else {
     echo $OUTPUT->header();
-    $error = optional_param('error', '', PARAM_NOTAGS);
-    if ($error) {
-        echo $OUTPUT->notification($error, 'notifyerror');
-    }
-    $success = optional_param('success', '', PARAM_NOTAGS);
-    if ($success) {
-        echo $OUTPUT->notification($success, 'notifysuccess');
-    }
     $tasks = core\task\manager::get_all_scheduled_tasks();
     echo $renderer->scheduled_tasks_table($tasks);
     echo $OUTPUT->footer();
 }
-
-
-
-
index 9b09541..ac31ab6 100644 (file)
@@ -72,10 +72,10 @@ class tool_templatelibrary_external_testcase extends externallib_advanced_testca
         // Change the theme to 'base' because it overrides these templates.
         $CFG->theme = 'base';
 
-        $template = external::load_canonical_template('core', 'notification_problem');
+        $template = external::load_canonical_template('core', 'notification_error');
 
         // Only the base template should contain the docs.
-        $this->assertContains('@template core/notification_problem', $template);
+        $this->assertContains('@template core/notification_error', $template);
 
         // Restore the original theme.
         $CFG->theme = $originaltheme;
index 958d6c1..7e2d4c5 100644 (file)
@@ -60,12 +60,7 @@ foreach ($allcohorts as $c) {
 unset($allcohorts);
 
 if (count($cohorts) < 2) {
-    echo $OUTPUT->header();
-    echo $OUTPUT->heading(get_string('bulkadd', 'core_cohort'));
-    echo $OUTPUT->notification(get_string('bulknocohort', 'core_cohort'));
-    echo $OUTPUT->continue_button(new moodle_url('/admin/user/user_bulk.php'));
-    echo $OUTPUT->footer();
-    die;
+    redirect(new moodle_url('/admin/user/user_bulk.php'), get_string('bulknocohort', 'core_cohort'));
 }
 
 $countries = get_string_manager()->get_list_of_countries(true);
index 31843a2..db43172 100644 (file)
@@ -115,8 +115,7 @@ if ($activate) {
     $url = new moodle_url('/badges/action.php', $params);
 
     if (!$badge->has_criteria()) {
-        echo $OUTPUT->notification(get_string('error:cannotact', 'badges') . get_string('nocriteria', 'badges'));
-        echo $OUTPUT->continue_button($returnurl);
+        redirect($returnurl, get_string('error:cannotact', 'badges') . get_string('nocriteria', 'badges'), null, \core\output\notification::NOTIFY_ERROR);
     } else {
         $message = get_string('reviewconfirm', 'badges', $badge->name);
         echo $OUTPUT->confirm($message, $url, $returnurl);
index 71f44f5..7676171 100644 (file)
@@ -31,6 +31,7 @@ A definition:
             'invalidationevents' => array(            // Optional
                 'contextmarkeddirty'
             ),
+            'canuselocalstore' => false               // Optional
             'sharingoptions' => null                  // Optional
             'defaultsharing' => null                  // Optional
         )
@@ -151,7 +152,7 @@ The following optional settings can also be defined:
 * invalidationevents - An array of events that should trigger this cache to invalidate.
 * sharingoptions - The sum of the possible sharing options that are applicable to the definition. An advanced setting.
 * defaultsharing - The default sharing option to use. It's highly recommended that you don't set this unless there is a very specific reason not to use the system default.
-
+* canuselocalstore - The default is to required a shared cache location for all nodes in a multi webserver environment.  If the cache uses revisions and never updates key data, administrators can use a local storage cache for this cache.
 It's important to note that internally the definition is also aware of the component. This is picked up when the definition is read, based upon the location of the caches.php file.
 
 The staticacceleration option.
@@ -269,4 +270,4 @@ There are a couple of considerations to using this method:
 
 Please be aware that if you are using Memcache or Memcached it is recommended to use dedicated Memcached servers.
 When caches get purged the memcached servers you have configured get purged, any data stored within them whether it belongs to Moodle or not will be removed.
-If you are using Memcached for sessions as well as caching/testing and caches get purged your sessions will be removed prematurely and users will be need to start again.
\ No newline at end of file
+If you are using Memcached for sessions as well as caching/testing and caches get purged your sessions will be removed prematurely and users will be need to start again.
index a5fd64a..21c85f2 100644 (file)
@@ -100,6 +100,11 @@ defined('MOODLE_INTERNAL') || die();
  *     + defaultsharing
  *          [int] The default sharing option to use. It's highly recommended that you don't set this unless there is a very
  *          specific reason not to use the system default.
+ *     + canuselocalstore
+ *          [bool] The cache is able to safely run with multiple copies on different webservers without any need for administrator
+ *                 intervention to ensure that data stays in sync across nodes.  This is usually managed by a revision
+ *                 system as seen in modinfo cache or language cache.  Requiring purge on upgrade is not sufficient as
+ *                 it requires administrator intervention on each node to make it work.
  *
  * For examples take a look at lib/db/caches.php
  *
@@ -308,6 +313,12 @@ class cache_definition {
      */
     protected $sharingoptions;
 
+    /**
+     * Whether this cache supports local storages.
+     * @var bool
+     */
+    protected $canuselocalstore = false;
+
     /**
      * The selected sharing option.
      * @var int One of self::SHARING_*
@@ -367,6 +378,7 @@ class cache_definition {
         $sharingoptions = self::SHARING_DEFAULT;
         $selectedsharingoption = self::SHARING_DEFAULT;
         $userinputsharingkey = '';
+        $canuselocalstore = false;
 
         if (array_key_exists('simplekeys', $definition)) {
             $simplekeys = (bool)$definition['simplekeys'];
@@ -453,6 +465,9 @@ class cache_definition {
                 $selectedsharingoption = self::SHARING_ALL;
             }
         }
+        if (array_key_exists('canuselocalstore', $definition)) {
+            $canuselocalstore = (bool)$definition['canuselocalstore'];
+        }
 
         if (array_key_exists('userinputsharingkey', $definition) && !empty($definition['userinputsharingkey'])) {
             $userinputsharingkey = (string)$definition['userinputsharingkey'];
@@ -529,6 +544,7 @@ class cache_definition {
         $cachedefinition->sharingoptions = $sharingoptions;
         $cachedefinition->selectedsharingoption = $selectedsharingoption;
         $cachedefinition->userinputsharingkey = $userinputsharingkey;
+        $cachedefinition->canuselocalstore = $canuselocalstore;
 
         return $cachedefinition;
     }
@@ -732,6 +748,15 @@ class cache_definition {
         return $this->requirelockingwrite;
     }
 
+    /**
+     * Returns true if this definition allows local storage to be used for caching.
+     * @since Moodle 3.1.0
+     * @return bool
+     */
+    public function can_use_localstore() {
+        return $this->canuselocalstore;
+    }
+
     /**
      * Returns true if this definition requires a searchable cache.
      * @since Moodle 2.4.4
@@ -766,13 +791,14 @@ class cache_definition {
      * Sets the identifiers for this definition, or updates them if they have already been set.
      *
      * @param array $identifiers
+     * @return bool false if no identifiers where changed, true otherwise.
      * @throws coding_exception
      */
     public function set_identifiers(array $identifiers = array()) {
         // If we are setting the exact same identifiers then just return as nothing really changed.
         // We don't care about order as cache::make will use the same definition order all the time.
         if ($identifiers === $this->identifiers) {
-            return;
+            return false;
         }
 
         foreach ($this->requireidentifiers as $identifier) {
@@ -791,6 +817,8 @@ class cache_definition {
         // Reset the key prefix's they need updating now.
         $this->keyprefixsingle = null;
         $this->keyprefixmulti = null;
+
+        return true;
     }
 
     /**
index 14eecd8..4050838 100644 (file)
@@ -190,9 +190,9 @@ class cachestore_dummy extends cache_store {
             foreach ($keyvaluearray as $pair) {
                 $this->store[$pair['key']] = $pair['value'];
             }
-            return count($keyvaluearray);
+
         }
-        return 0;
+        return count($keyvaluearray);
     }
 
     /**
index e0a7723..544718c 100644 (file)
@@ -264,7 +264,15 @@ class cache implements cache_loader {
      * @param array $identifiers
      */
     public function set_identifiers(array $identifiers) {
-        $this->definition->set_identifiers($identifiers);
+        if ($this->definition->set_identifiers($identifiers)) {
+            // As static acceleration uses input keys and not parsed keys
+            // it much be cleared when the identifier set is changed.
+            $this->staticaccelerationarray = array();
+            if ($this->staticaccelerationsize !== false) {
+                $this->staticaccelerationkeys = array();
+                $this->staticaccelerationcount = 0;
+            }
+        }
     }
 
     /**
@@ -278,23 +286,19 @@ class cache implements cache_loader {
      * @throws coding_exception
      */
     public function get($key, $strictness = IGNORE_MISSING) {
-        // 1. Parse the key.
-        $parsedkey = $this->parse_key($key);
-        // 2. Get it from the static acceleration array if we can (only when it is enabled and it has already been requested/set).
-        $result = false;
-        if ($this->use_static_acceleration()) {
-            $result = $this->static_acceleration_get($parsedkey);
-        }
-        if ($result !== false) {
-            if (!is_scalar($result)) {
-                // If data is an object it will be a reference.
-                // If data is an array if may contain references.
-                // We want to break references so that the cache cannot be modified outside of itself.
-                // Call the function to unreference it (in the best way possible).
-                $result = $this->unref($result);
+        // 1. Get it from the static acceleration array if we can (only when it is enabled and it has already been requested/set).
+        $usesstaticacceleration = $this->use_static_acceleration();
+
+        if ($usesstaticacceleration) {
+            $result = $this->static_acceleration_get($key);
+            if ($result !== false) {
+                return $result;
             }
-            return $result;
         }
+
+        // 2. Parse the key.
+        $parsedkey = $this->parse_key($key);
+
         // 3. Get it from the store. Obviously wasn't in the static acceleration array.
         $result = $this->store->get($parsedkey);
         if ($result !== false) {
@@ -309,10 +313,11 @@ class cache implements cache_loader {
             if ($result instanceof cache_cached_object) {
                 $result = $result->restore_object();
             }
-            if ($this->use_static_acceleration()) {
-                $this->static_acceleration_set($parsedkey, $result);
+            if ($usesstaticacceleration) {
+                $this->static_acceleration_set($key, $result);
             }
         }
+
         // 4. Load if from the loader/datasource if we don't already have it.
         $setaftervalidation = false;
         if ($result === false) {
@@ -341,7 +346,7 @@ class cache implements cache_loader {
         }
         // 7. Make sure we don't pass back anything that could be a reference.
         //    We don't want people modifying the data in the cache.
-        if (!is_scalar($result)) {
+        if (!$this->store->supports_dereferencing_objects() && !is_scalar($result)) {
             // If data is an object it will be a reference.
             // If data is an array if may contain references.
             // We want to break references so that the cache cannot be modified outside of itself.
@@ -385,7 +390,7 @@ class cache implements cache_loader {
             $parsedkeys[$pkey] = $key;
             $keystofind[$pkey] = $key;
             if ($isusingpersist) {
-                $value = $this->static_acceleration_get($pkey);
+                $value = $this->static_acceleration_get($key);
                 if ($value !== false) {
                     $resultpersist[$pkey] = $value;
                     unset($keystofind[$pkey]);
@@ -409,7 +414,7 @@ class cache implements cache_loader {
                     $value = $value->restore_object();
                 }
                 if ($value !== false && $this->use_static_acceleration()) {
-                    $this->static_acceleration_set($key, $value);
+                    $this->static_acceleration_set($keystofind[$key], $value);
                 }
                 $resultstore[$key] = $value;
             }
@@ -450,6 +455,13 @@ class cache implements cache_loader {
         // Create an array with the original keys and the found values. This will be what we return.
         $fullresult = array();
         foreach ($result as $key => $value) {
+            if (!is_scalar($value)) {
+                // If data is an object it will be a reference.
+                // If data is an array if may contain references.
+                // We want to break references so that the cache cannot be modified outside of itself.
+                // Call the function to unreference it (in the best way possible).
+                $value = $this->unref($value);
+            }
             $fullresult[$parsedkeys[$key]] = $value;
         }
         unset($result);
@@ -507,22 +519,27 @@ class cache implements cache_loader {
             // We have to let the loader do its own parsing of data as it may be unique.
             $this->loader->set($key, $data);
         }
+        $usestaticacceleration = $this->use_static_acceleration();
+
         if (is_object($data) && $data instanceof cacheable_object) {
             $data = new cache_cached_object($data);
-        } else if (!is_scalar($data)) {
+        } else if (!$this->store->supports_dereferencing_objects() && !is_scalar($data)) {
             // If data is an object it will be a reference.
             // If data is an array if may contain references.
             // We want to break references so that the cache cannot be modified outside of itself.
             // Call the function to unreference it (in the best way possible).
             $data = $this->unref($data);
         }
+
+        if ($usestaticacceleration) {
+            $this->static_acceleration_set($key, $data);
+        }
+
         if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) {
             $data = new cache_ttl_wrapper($data, $this->definition->get_ttl());
         }
         $parsedkey = $this->parse_key($key);
-        if ($this->use_static_acceleration()) {
-            $this->static_acceleration_set($parsedkey, $data);
-        }
+
         return $this->store->set($parsedkey, $data);
     }
 
@@ -626,16 +643,20 @@ class cache implements cache_loader {
         $data = array();
         $simulatettl = $this->has_a_ttl() && !$this->store_supports_native_ttl();
         $usestaticaccelerationarray = $this->use_static_acceleration();
+        $needsdereferencing = !$this->store->supports_dereferencing_objects();
         foreach ($keyvaluearray as $key => $value) {
             if (is_object($value) && $value instanceof cacheable_object) {
                 $value = new cache_cached_object($value);
-            } else if (!is_scalar($value)) {
+            } else if ($needsdereferencing && !is_scalar($value)) {
                 // If data is an object it will be a reference.
                 // If data is an array if may contain references.
                 // We want to break references so that the cache cannot be modified outside of itself.
                 // Call the function to unreference it (in the best way possible).
                 $value = $this->unref($value);
             }
+            if ($usestaticaccelerationarray) {
+                $this->static_acceleration_set($key, $value);
+            }
             if ($simulatettl) {
                 $value = new cache_ttl_wrapper($value, $this->definition->get_ttl());
             }
@@ -643,9 +664,6 @@ class cache implements cache_loader {
                 'key' => $this->parse_key($key),
                 'value' => $value
             );
-            if ($usestaticaccelerationarray) {
-                $this->static_acceleration_set($data[$key]['key'], $value);
-            }
         }
         $successfullyset = $this->store->set_many($data);
         if ($this->perfdebug && $successfullyset) {
@@ -676,11 +694,12 @@ class cache implements cache_loader {
      * @return bool True if the cache has the requested key, false otherwise.
      */
     public function has($key, $tryloadifpossible = false) {
-        $parsedkey = $this->parse_key($key);
-        if ($this->static_acceleration_has($parsedkey)) {
+        if ($this->static_acceleration_has($key)) {
             // Hoorah, that was easy. It exists in the static acceleration array so we definitely have it.
             return true;
         }
+        $parsedkey = $this->parse_key($key);
+
         if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) {
             // The data has a TTL and the store doesn't support it natively.
             // We must fetch the data and expect a ttl wrapper.
@@ -760,17 +779,13 @@ class cache implements cache_loader {
         }
 
         if ($this->use_static_acceleration()) {
-            $parsedkeys = array();
             foreach ($keys as $id => $key) {
-                $parsedkey = $this->parse_key($key);
-                if ($this->static_acceleration_has($parsedkey)) {
+                if ($this->static_acceleration_has($key)) {
                     return true;
                 }
-                $parsedkeys[] = $parsedkey;
             }
-        } else {
-            $parsedkeys = array_map(array($this, 'parse_key'), $keys);
         }
+        $parsedkeys = array_map(array($this, 'parse_key'), $keys);
         return $this->store->has_any($parsedkeys);
     }
 
@@ -783,12 +798,12 @@ class cache implements cache_loader {
      * @return bool True of success, false otherwise.
      */
     public function delete($key, $recurse = true) {
-        $parsedkey = $this->parse_key($key);
-        $this->static_acceleration_delete($parsedkey);
+        $this->static_acceleration_delete($key);
         if ($recurse && $this->loader !== false) {
             // Delete from the bottom of the stack first.
             $this->loader->delete($key, $recurse);
         }
+        $parsedkey = $this->parse_key($key);
         return $this->store->delete($parsedkey);
     }
 
@@ -801,16 +816,16 @@ class cache implements cache_loader {
      * @return int The number of items successfully deleted.
      */
     public function delete_many(array $keys, $recurse = true) {
-        $parsedkeys = array_map(array($this, 'parse_key'), $keys);
         if ($this->use_static_acceleration()) {
-            foreach ($parsedkeys as $parsedkey) {
-                $this->static_acceleration_delete($parsedkey);
+            foreach ($keys as $key) {
+                $this->static_acceleration_delete($key);
             }
         }
         if ($recurse && $this->loader !== false) {
             // Delete from the bottom of the stack first.
             $this->loader->delete_many($keys, $recurse);
         }
+        $parsedkeys = array_map(array($this, 'parse_key'), $keys);
         return $this->store->delete_many($parsedkeys);
     }
 
@@ -974,19 +989,11 @@ class cache implements cache_loader {
      * @return bool
      */
     protected function static_acceleration_has($key) {
-        // This method of checking if an array was supplied is faster than is_array.
-        if ($key === (array)$key) {
-            $key = $key['key'];
-        }
         // This could be written as a single line, however it has been split because the ttl check is faster than the instanceof
         // and has_expired calls.
-        if (!$this->staticacceleration || !array_key_exists($key, $this->staticaccelerationarray)) {
+        if (!$this->staticacceleration || !isset($this->staticaccelerationarray[$key])) {
             return false;
         }
-        if ($this->has_a_ttl() && $this->store_supports_native_ttl()) {
-             return !($this->staticaccelerationarray[$key] instanceof cache_ttl_wrapper &&
-                      $this->staticaccelerationarray[$key]->has_expired());
-        }
         return true;
     }
 
@@ -1007,34 +1014,20 @@ class cache implements cache_loader {
      * Returns the item from the static acceleration array if it exists there.
      *
      * @param string $key The parsed key
-     * @return mixed|false The data from the static acceleration array or false if it wasn't there.
+     * @return mixed|false Dereferenced data from the static acceleration array or false if it wasn't there.
      */
     protected function static_acceleration_get($key) {
-        // This method of checking if an array was supplied is faster than is_array.
-        if ($key === (array)$key) {
-            $key = $key['key'];
-        }
-        // This isset check is faster than array_key_exists but will return false
-        // for null values, meaning null values will come from backing store not
-        // the static acceleration array. We think this okay because null usage should be
-        // very rare (see comment in MDL-39472).
         if (!$this->staticacceleration || !isset($this->staticaccelerationarray[$key])) {
             $result = false;
         } else {
-            $data = $this->staticaccelerationarray[$key];
-            if (!$this->has_a_ttl() || !$data instanceof cache_ttl_wrapper) {
-                if ($data instanceof cache_cached_object) {
-                    $data = $data->restore_object();
-                }
-                $result = $data;
-            } else if ($data->has_expired()) {
-                $this->static_acceleration_delete($key);
-                $result = false;
+            $data = $this->staticaccelerationarray[$key]['data'];
+
+            if ($data instanceof cache_cached_object) {
+                $result = $data->restore_object();
+            } else if ($this->staticaccelerationarray[$key]['serialized']) {
+                $result = unserialize($data);
             } else {
-                if ($data instanceof cache_cached_object) {
-                    $data = $data->restore_object();
-                }
-                $result = $data->data;
+                $result = $data;
             }
         }
         if ($result) {
@@ -1081,15 +1074,23 @@ class cache implements cache_loader {
      * @return bool
      */
     protected function static_acceleration_set($key, $data) {
-        // This method of checking if an array was supplied is faster than is_array.
-        if ($key === (array)$key) {
-            $key = $key['key'];
-        }
         if ($this->staticaccelerationsize !== false && isset($this->staticaccelerationkeys[$key])) {
             $this->staticaccelerationcount--;
             unset($this->staticaccelerationkeys[$key]);
         }
-        $this->staticaccelerationarray[$key] = $data;
+
+        // We serialize anything that's not;
+        // 1. A known scalar safe value.
+        // 2. A definition that says it's simpledata.  We trust it that it doesn't contain dangerous references.
+        // 3. An object that handles dereferencing by itself.
+        if (is_scalar($data) || $this->definition->uses_simple_data()
+                || $data instanceof cache_cached_object) {
+            $this->staticaccelerationarray[$key]['data'] = $data;
+            $this->staticaccelerationarray[$key]['serialized'] = false;
+        } else {
+            $this->staticaccelerationarray[$key]['data'] = serialize($data);
+            $this->staticaccelerationarray[$key]['serialized'] = true;
+        }
         if ($this->staticaccelerationsize !== false) {
             $this->staticaccelerationcount++;
             $this->staticaccelerationkeys[$key] = $key;
@@ -1123,12 +1124,9 @@ class cache implements cache_loader {
      */
     protected function static_acceleration_delete($key) {
         unset($this->staticaccelerationarray[$key]);
-        if ($this->staticaccelerationsize !== false) {
-            $dropkey = array_search($key, $this->staticaccelerationkeys);
-            if ($dropkey) {
-                unset($this->staticaccelerationkeys[$dropkey]);
-                $this->staticaccelerationcount--;
-            }
+        if ($this->staticaccelerationsize !== false && isset($this->staticaccelerationkeys[$key])) {
+            unset($this->staticaccelerationkeys[$key]);
+            $this->staticaccelerationcount--;
         }
         return true;
     }
@@ -1806,7 +1804,7 @@ class cache_session extends cache {
         }
         // 6. Make sure we don't pass back anything that could be a reference.
         //    We don't want people modifying the data in the cache.
-        if (!is_scalar($result)) {
+        if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($result)) {
             // If data is an object it will be a reference.
             // If data is an array if may contain references.
             // We want to break references so that the cache cannot be modified outside of itself.
@@ -1846,7 +1844,7 @@ class cache_session extends cache {
         }
         if (is_object($data) && $data instanceof cacheable_object) {
             $data = new cache_cached_object($data);
-        } else if (!is_scalar($data)) {
+        } else if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($data)) {
             // If data is an object it will be a reference.
             // If data is an array if may contain references.
             // We want to break references so that the cache cannot be modified outside of itself.
@@ -1922,6 +1920,12 @@ class cache_session extends cache {
             if ($value instanceof cache_cached_object) {
                 /* @var cache_cached_object $value */
                 $value = $value->restore_object();
+            } else if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($value)) {
+                // If data is an object it will be a reference.
+                // If data is an array if may contain references.
+                // We want to break references so that the cache cannot be modified outside of itself.
+                // Call the function to unreference it (in the best way possible).
+                $value = $this->unref($value);
             }
             $return[$key] = $value;
             if ($value === false) {
@@ -2027,7 +2031,7 @@ class cache_session extends cache {
         foreach ($keyvaluearray as $key => $value) {
             if (is_object($value) && $value instanceof cacheable_object) {
                 $value = new cache_cached_object($value);
-            } else if (!is_scalar($value)) {
+            } else if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($value)) {
                 // If data is an object it will be a reference.
                 // If data is an array if may contain references.
                 // We want to break references so that the cache cannot be modified outside of itself.
index 2c4b099..c415ba9 100644 (file)
@@ -126,6 +126,14 @@ abstract class cache_store implements cache_store_interface {
      */
     const IS_SEARCHABLE = 8;
 
+    /**
+     * The cache store dereferences objects.
+     *
+     * When set, loaders will assume that all data coming from this store has already had all references
+     * resolved.  So even for complex object structures it will not try to remove references again.
+     */
+    const DEREFERENCES_OBJECTS = 16;
+
     // Constants for the modes of a cache store
 
     /**
@@ -334,6 +342,15 @@ abstract class cache_store implements cache_store_interface {
         return in_array('cache_is_searchable', class_implements($this));
     }
 
+    /**
+     * Returns true if the store automatically dereferences objects.
+     *
+     * @return bool
+     */
+    public function supports_dereferencing_objects() {
+        return $this::get_supported_features() & self::DEREFERENCES_OBJECTS;
+    }
+
     /**
      * Creates a clone of this store instance ready to be initialised.
      *
index 5a2ef3b..e482702 100644 (file)
@@ -132,6 +132,8 @@ class cache_definition_mappings_form extends moodleform {
      * The definition of the form
      */
     protected final function definition() {
+        global $OUTPUT;
+
         $definition = $this->_customdata['definition'];
         $form = $this->_form;
 
@@ -139,6 +141,14 @@ class cache_definition_mappings_form extends moodleform {
         list($currentstores, $storeoptions, $defaults) =
                 cache_administration_helper::get_definition_store_options($component, $area);
 
+        $storedata = cache_administration_helper::get_definition_summaries();
+        if ($storedata[$definition]['mode'] != cache_store::MODE_REQUEST) {
+            if (isset($storedata[$definition]['canuselocalstore']) && $storedata[$definition]['canuselocalstore']) {
+                $form->addElement('html', $OUTPUT->notification(get_string('localstorenotification', 'cache'), 'notifymessage'));
+            } else {
+                $form->addElement('html', $OUTPUT->notification(get_string('sharedstorenotification', 'cache'), 'notifymessage'));
+            }
+        }
         $form->addElement('hidden', 'definition', $definition);
         $form->setType('definition', PARAM_SAFEPATH);
         $form->addElement('hidden', 'action', 'editdefinitionmapping');
index 58dcdd8..2e6813e 100644 (file)
@@ -806,6 +806,7 @@ abstract class cache_administration_helper extends cache_helper {
                 'component' => $definition->get_component(),
                 'area' => $definition->get_area(),
                 'mappings' => $mappings,
+                'canuselocalstore' => $definition->can_use_localstore(),
                 'sharingoptions' => self::get_definition_sharing_options($definition->get_sharing_options(), false),
                 'selectedsharingoption' => self::get_definition_sharing_options($definition->get_selected_sharing_option(), true),
                 'userinputsharingkey' => $definition->get_user_input_sharing_key()
index e044c91..67a6642 100644 (file)
@@ -211,7 +211,8 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
     public static function get_supported_features(array $configuration = array()) {
         $supported = self::SUPPORTS_DATA_GUARANTEE +
                      self::SUPPORTS_NATIVE_TTL +
-                     self::IS_SEARCHABLE;
+                     self::IS_SEARCHABLE +
+                     self::DEREFERENCES_OBJECTS;
         return $supported;
     }
 
index 77efd21..e4ead7f 100644 (file)
@@ -271,7 +271,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return int
      */
     public static function get_supported_features(array $configuration = array()) {
-        return self::SUPPORTS_NATIVE_TTL;
+        return self::SUPPORTS_NATIVE_TTL + self::DEREFERENCES_OBJECTS;
     }
 
     /**
index 0ed6623..5e8250d 100644 (file)
@@ -258,7 +258,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
      * @return int
      */
     public static function get_supported_features(array $configuration = array()) {
-        return self::SUPPORTS_NATIVE_TTL;
+        return self::SUPPORTS_NATIVE_TTL + self::DEREFERENCES_OBJECTS;
     }
 
     /**
index 7ea787d..641b051 100644 (file)
@@ -175,7 +175,7 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
      * @return int
      */
     public static function get_supported_features(array $configuration = array()) {
-        $supports = self::SUPPORTS_DATA_GUARANTEE;
+        $supports = self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS;
         if (array_key_exists('extendedmode', $configuration) && $configuration['extendedmode']) {
             $supports += self::SUPPORTS_MULTIPLE_IDENTIFIERS;
         }
index 6c9f14c..26ab145 100644 (file)
@@ -407,7 +407,7 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertEquals('pork', $var->subobj->subobj->key);
         $this->assertTrue($cache->delete('obj'));
 
-        // Death reference test... basicaly we don't want this to die.
+        // Death reference test... basically we don't want this to die.
         $obj = new stdClass;
         $obj->key = 'value';
         $obj->self =& $obj;
@@ -433,6 +433,32 @@ class core_cache_testcase extends advanced_testcase {
 
         $this->assertTrue($cache->delete('obj'));
 
+        // Death reference test on get_many... basically we don't want this to die.
+        $obj = new stdClass;
+        $obj->key = 'value';
+        $obj->self =& $obj;
+        $this->assertEquals(1, $cache->set_many(array('obj' => $obj)));
+        $var = $cache->get_many(array('obj'));
+        $this->assertInstanceOf('stdClass', $var['obj']);
+        $this->assertEquals('value', $var['obj']->key);
+
+        // Reference test after retrieve.
+        $obj = new stdClass;
+        $obj->key = 'value';
+        $this->assertEquals(1, $cache->set_many(array('obj' => $obj)));
+
+        $var1 = $cache->get_many(array('obj'));
+        $this->assertInstanceOf('stdClass', $var1['obj']);
+        $this->assertEquals('value', $var1['obj']->key);
+        $var1['obj']->key = 'eulav';
+        $this->assertEquals('eulav', $var1['obj']->key);
+
+        $var2 = $cache->get_many(array('obj'));
+        $this->assertInstanceOf('stdClass', $var2['obj']);
+        $this->assertEquals('value', $var2['obj']->key);
+
+        $this->assertTrue($cache->delete('obj'));
+
         // Test strictness exceptions.
         try {
             $cache->get('exception', MUST_EXIST);
index 8b3ef55..bae500d 100644 (file)
@@ -423,7 +423,6 @@ class cache_phpunit_application extends cache_application {
      * @return false|mixed
      */
     public function phpunit_static_acceleration_get($key) {
-        $key = $this->parse_key($key);
         return $this->static_acceleration_get($key);
     }
 }
index 1c1b133..0b4cbb9 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in /cache/stores/* - cache store plugins.
 Information provided here is intended especially for developers.
 
+=== 3.1 ===
+* Cache stores has a new feature DEREFERENCES_OBJECTS.
+  This allows the cache loader to decide if it needs to handle dereferencing or whether the data
+  coming directly to it has already had references resolved.
+  - see supports_dereferencing_objects in store.php.
+
 === 2.9 ===
 * Cache data source aggregation functionality has been removed. This functionality was found to be broken and unused.
   It was decided that rather than fixing it it should be removed.
index 1ddc12d..95bb480 100644 (file)
@@ -61,6 +61,8 @@ if ($delete === md5($course->timemodified)) {
 
     echo $OUTPUT->header();
     echo $OUTPUT->heading($strdeletingcourse);
+    // This might take a while. Raise the execution time limit.
+    core_php_time_limit::raise();
     // We do this here because it spits out feedback as it goes.
     delete_course($course);
     echo $OUTPUT->heading( get_string("deletedcourse", "", $courseshortname) );
index 6f6cfd8..6c34d47 100644 (file)
@@ -102,6 +102,7 @@ $string['inadequatestoreformapping'] = 'This store doesn\'t meet the requirement
 $string['invalidlock'] = 'Invalid lock';
 $string['invalidplugin'] = 'Invalid plugin';
 $string['invalidstore'] = 'Invalid cache store provided';
+$string['localstorenotification'] = 'This cache can be safely mapped to a store that is local to each webserver';
 $string['lockdefault'] = 'Default';
 $string['locking'] = 'Locking';
 $string['locking_help'] = 'Locking is a mechanism that restricts access to cached data to one process at a time to prevent the data from being overwritten. The locking method determines how the lock is acquired and checked.';
@@ -131,6 +132,7 @@ $string['requestcount'] = 'Test with {$a} requests';
 $string['rescandefinitions'] = 'Rescan definitions';
 $string['result'] = 'Result';
 $string['set'] = 'Set';
+$string['sharedstorenotification'] = 'This cache must be mapped to a store that is shared to all webservers';
 $string['sharing'] = 'Sharing';
 $string['sharing_all'] = 'Everyone.';
 $string['sharing_input'] = 'Custom key (entered below)';
index 37952ec..7df55c2 100644 (file)
Binary files a/lib/amd/build/notification.min.js and b/lib/amd/build/notification.min.js differ
index 1880b80..845a2e6 100644 (file)
@@ -14,6 +14,8 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * A system for displaying notifications to users from the session.
+ *
  * Wrapper for the YUI M.core.notification class. Allows us to
  * use the YUI version in AMD code until it is replaced.
  *
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since      2.9
  */
-define(['core/yui'], function(Y) {
+define(['core/yui', 'jquery', 'theme_bootstrapbase/bootstrap', 'core/templates', 'core/ajax', 'core/log'],
+function(Y, $, bootstrap, templates, ajax, log) {
+    var notificationModule = {
+        types: {
+            'success':  'core/notification_success',
+            'info':     'core/notification_info',
+            'warning':  'core/notification_warning',
+            'error':    'core/notification_error',
+        },
 
-    // Private variables and functions.
+        fieldName: 'user-notifications',
+
+        fetchNotifications: function() {
+            var promises = ajax.call([{
+                methodname: 'core_fetch_notifications',
+                args: {
+                    contextid: notificationModule.contextid
+                }
+            }]);
+
+            promises[0]
+                .done(notificationModule.addNotifications)
+                ;
+
+        },
+
+        addNotifications: function(notifications) {
+            if (!notifications) {
+                notifications = [];
+            }
+
+            $.each(notifications, function(i, notification) {
+                notificationModule.renderNotification(notification.template, notification.variables);
+            });
+        },
+
+        setupTargetRegion: function() {
+            var targetRegion = $('#' + notificationModule.fieldName);
+            if (targetRegion.length) {
+                return;
+            }
+
+            var newRegion = $('<span>').attr('id', notificationModule.fieldName);
+
+            targetRegion = $('#region-main');
+            if (targetRegion.length) {
+                return targetRegion.prepend(newRegion);
+            }
+
+            targetRegion = $('[role="main"]');
+            if (targetRegion.length) {
+                return targetRegion.prepend(newRegion);
+            }
+
+            targetRegion = $('body');
+            return targetRegion.prepend(newRegion);
+        },
+
+        addNotification: function(notification) {
+            var template = notificationModule.types.error;
+
+            notification = $.extend({
+                    closebutton:    true,
+                    announce:       true,
+                    type:           'error'
+                }, notification);
+
+            if (notification.template) {
+                template = notification.template;
+                delete notification.template;
+            } else if (notification.type){
+                if (typeof notificationModule.types[notification.type] !== 'undefined') {
+                    template = notificationModule.types[notification.type];
+                }
+                delete notification.type;
+            }
+
+            return notificationModule.renderNotification(template, notification);
+        },
+
+        renderNotification: function(template, variables) {
+            if (typeof variables.message === 'undefined' || !variables.message) {
+                log.debug('Notification received without content. Skipping.');
+                return;
+            }
+            templates.render(template, variables)
+                .done(function(html) {
+                    $('#' + notificationModule.fieldName).prepend(html);
+                })
+                .fail(notificationModule.exception)
+                ;
+        },
 
-    return /** @alias module:core/notification */ {
-        // Public variables and functions.
-        /**
-         * Wrap M.core.alert.
-         *
-         * @method alert
-         * @param {string} title
-         * @param {string} message
-         * @param {string} yesLabel
-         */
         alert: function(title, message, yesLabel) {
             // Here we are wrapping YUI. This allows us to start transitioning, but
             // wait for a good alternative without having inconsistent dialogues.
@@ -52,16 +133,6 @@ define(['core/yui'], function(Y) {
             });
         },
 
-        /**
-         * Wrap M.core.confirm.
-         *
-         * @method confirm
-         * @param {string} title
-         * @param {string} question
-         * @param {string} yesLabel
-         * @param {string} noLabel
-         * @param {function} callback
-         */
         confirm: function(title, question, yesLabel, noLabel, callback) {
             // Here we are wrapping YUI. This allows us to start transitioning, but
             // wait for a good alternative without having inconsistent dialogues.
@@ -80,12 +151,6 @@ define(['core/yui'], function(Y) {
             });
         },
 
-        /**
-         * Wrap M.core.exception.
-         *
-         * @method exception
-         * @param {Error} ex
-         */
         exception: function(ex) {
             // Fudge some parameters.
             if (ex.backtrace) {
@@ -102,4 +167,73 @@ define(['core/yui'], function(Y) {
             });
         }
     };
+
+    return /** @alias module:core/notification */{
+        init: function(contextid, notifications) {
+            notificationModule.contextid = contextid;
+
+            // Setup the message target region if it isn't setup already
+            notificationModule.setupTargetRegion();
+
+            // Setup closing of bootstrap alerts.
+            $().alert();
+
+            // Add provided notifications.
+            notificationModule.addNotifications(notifications);
+
+            // Poll for any new notifications.
+            notificationModule.fetchNotifications();
+        },
+
+        /**
+         * Poll the server for any new notifications.
+         *
+         * @method fetchNotifications
+         */
+        fetchNotifications: notificationModule.fetchNotifications,
+
+        /**
+         * Add a notification to the page.
+         *
+         * Note: This does not cause the notification to be added to the session.
+         *
+         * @method addNotification
+         * @param {Object}  notification                The notification to add.
+         * @param {string}  notification.message        The body of the notification
+         * @param {string}  notification.type           The type of notification to add (error, warning, info, success).
+         * @param {Boolean} notification.closebutton    Whether to show the close button.
+         * @param {Boolean} notification.announce       Whether to announce to screen readers.
+         */
+        addNotification: notificationModule.addNotification,
+
+        /**
+         * Wrap M.core.alert.
+         *
+         * @method alert
+         * @param {string} title
+         * @param {string} message
+         * @param {string} yesLabel
+         */
+        alert: notificationModule.alert,
+
+        /**
+         * Wrap M.core.confirm.
+         *
+         * @method confirm
+         * @param {string} title
+         * @param {string} question
+         * @param {string} yesLabel
+         * @param {string} noLabel
+         * @param {function} callback
+         */
+        confirm: notificationModule.confirm,
+
+        /**
+         * Wrap M.core.exception.
+         *
+         * @method exception
+         * @param {Error} ex
+         */
+        exception: notificationModule.exception
+    };
 });
diff --git a/lib/classes/notification.php b/lib/classes/notification.php
new file mode 100644 (file)
index 0000000..aeb7e03
--- /dev/null
@@ -0,0 +1,165 @@
+<?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/>.
+
+namespace core;
+
+/**
+ * User Alert notifications.
+ *
+ * @package    core
+ * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+class notification {
+    /**
+     * A notification of level 'success'.
+     */
+    const SUCCESS = 'success';
+
+    /**
+     * A notification of level 'warning'.
+     */
+    const WARNING = 'warning';
+
+    /**
+     * A notification of level 'info'.
+     */
+    const INFO = 'info';
+
+    /**
+     * A notification of level 'error'.
+     */
+    const ERROR = 'error';
+
+    /**
+     * Add a message to the session notification stack.
+     *
+     * @param string $message The message to add to the stack
+     * @param string $level   The type of message to add to the stack
+     */
+    public static function add($message, $level = null) {
+        global $PAGE, $SESSION;
+
+        if ($PAGE && $PAGE->state === \moodle_page::STATE_IN_BODY) {
+            // Currently in the page body - just render and exit immediately.
+            // We insert some code to immediately insert this into the user-notifications created by the header.
+            $id = uniqid();
+            echo \html_writer::span(
+                $PAGE->get_renderer('core')->render(new \core\output\notification($message, $level)),
+                '', array('id' => $id));
+
+            // Insert this JS here using a script directly rather than waiting for the page footer to load to avoid
+            // ensure that the message is added to the user-notifications section as soon as possible after it is created.
+            echo \html_writer::script(
+                    "(function() {" .
+                        "var notificationHolder = document.getElementById('user-notifications');" .
+                        "if (!notificationHolder) { return; }" .
+                        "var thisNotification = document.getElementById('{$id}');" .
+                        "if (!thisNotification) { return; }" .
+                        "notificationHolder.appendChild(thisNotification.firstChild);" .
+                        "thisNotification.remove();" .
+                    "})();"
+                );
+            return;
+        }
+
+        // Add the notification directly to the session.
+        // This will either be fetched in the header, or by JS in the footer.
+        $SESSION->notifications[] = (object) array(
+            'message'   => $message,
+            'type'      => $level,
+        );
+    }
+
+    /**
+     * Fetch all of the notifications in the stack and clear the stack.
+     *
+     * @return array All of the notifications in the stack
+     */
+    public static function fetch() {
+        global $SESSION;
+
+        if (!isset($SESSION) || !isset($SESSION->notifications)) {
+            return [];
+        }
+
+        $notifications = $SESSION->notifications;
+        $SESSION->notifications = [];
+
+        $renderables = [];
+        foreach ($notifications as $notification) {
+            $renderable = new \core\output\notification($notification->message, $notification->type);
+            $renderables[] = $renderable;
+        }
+
+        return $renderables;
+    }
+
+    /**
+     * Fetch all of the notifications in the stack and clear the stack.
+     *
+     * @return array All of the notifications in the stack
+     */
+    public static function fetch_as_array(\renderer_base $renderer) {
+        $notifications = [];
+        foreach (self::fetch() as $notification) {
+            $notifications[] = [
+                'template'  => $notification->get_template_name(),
+                'variables' => $notification->export_for_template($renderer),
+            ];
+        }
+        return $notifications;
+    }
+
+    /**
+     * Add a success message to the notification stack.
+     *
+     * @param string $message The message to add to the stack
+     */
+    public static function success($message) {
+        return self::add($message, self::SUCCESS);
+    }
+
+    /**
+     * Add a info message to the notification stack.
+     *
+     * @param string $message The message to add to the stack
+     */
+    public static function info($message) {
+        return self::add($message, self::INFO);
+    }
+
+    /**
+     * Add a warning message to the notification stack.
+     *
+     * @param string $message The message to add to the stack
+     */
+    public static function warning($message) {
+        return self::add($message, self::WARNING);
+    }
+
+    /**
+     * Add a error message to the notification stack.
+     *
+     * @param string $message The message to add to the stack
+     */
+    public static function error($message) {
+        return self::add($message, self::ERROR);
+    }
+}
index 6cf19ed..12c9aaf 100644 (file)
@@ -23,7 +23,6 @@
  */
 
 namespace core\output;
-use stdClass;
 
 /**
  * Data structure representing a notification.
@@ -37,31 +36,67 @@ use stdClass;
 class notification implements \renderable, \templatable {
 
     /**
-     * A generic message.
+     * A notification of level 'success'.
      */
-    const NOTIFY_MESSAGE = 'message';
+    const NOTIFY_SUCCESS = 'success';
+
     /**
-     * A message notifying the user of a successful operation.
+     * A notification of level 'warning'.
      */
-    const NOTIFY_SUCCESS = 'success';
+    const NOTIFY_WARNING = 'warning';
+
+    /**
+     * A notification of level 'info'.
+     */
+    const NOTIFY_INFO = 'info';
+
+    /**
+     * A notification of level 'error'.
+     */
+    const NOTIFY_ERROR = 'error';
+
     /**
+     * @deprecated
+     * A generic message.
+     */
+    const NOTIFY_MESSAGE = 'message';
+
+    /**
+     * @deprecated
      * A message notifying the user that a problem occurred.
      */
     const NOTIFY_PROBLEM = 'problem';
+
     /**
-     * A message to display during a redirect..
+     * @deprecated
+     * A notification of level 'redirect'.
      */
     const NOTIFY_REDIRECT = 'redirect';
 
     /**
      * @var string Message payload.
      */
-    private $message = '';
+    protected $message = '';
 
     /**
      * @var string Message type.
      */
-    private $messagetype = self::NOTIFY_PROBLEM;
+    protected $messagetype = self::NOTIFY_WARNING;
+
+    /**
+     * @var bool $announce Whether this notification should be announced assertively to screen readers.
+     */
+    protected $announce = true;
+
+    /**
+     * @var bool $closebutton Whether this notification should inlcude a button to dismiss itself.
+     */
+    protected $closebutton = true;
+
+    /**
+     * @var array $extraclasses A list of any extra classes that may be required.
+     */
+    protected $extraclasses = array();
 
     /**
      * Notification constructor.
@@ -69,11 +104,57 @@ class notification implements \renderable, \templatable {
      * @param string $message the message to print out
      * @param string $messagetype normally NOTIFY_PROBLEM or NOTIFY_SUCCESS.
      */
-    public function __construct($message, $messagetype = self::NOTIFY_PROBLEM) {
+    public function __construct($message, $messagetype = null) {
+        $this->message = $message;
+
+        if (empty($messagetype)) {
+            $messagetype = self::NOTIFY_ERROR;
+        }
 
-        $this->message = clean_text($message);
         $this->messagetype = $messagetype;
 
+        switch ($messagetype) {
+            case self::NOTIFY_PROBLEM:
+            case self::NOTIFY_REDIRECT:
+            case self::NOTIFY_MESSAGE:
+                debugging('Use of ' . $messagetype . ' has been deprecated. Please switch to an alternative type.');
+        }
+    }
+
+    /**
+     * Set whether this notification should be announced assertively to screen readers.
+     *
+     * @param bool $announce
+     * @return $this
+     */
+    public function set_announce($announce = false) {
+        $this->announce = (bool) $announce;
+
+        return $this;
+    }
+
+    /**
+     * Set whether this notification should include a button to disiss itself.
+     *
+     * @param bool $button
+     * @return $this
+     */
+    public function set_show_closebutton($button = false) {
+        $this->closebutton = (bool) $button;
+
+        return $this;
+    }
+
+    /**
+     * Add any extra classes that this notification requires.
+     *
+     * @param array $classes
+     * @return $this
+     */
+    public function set_extra_classes($classes = array()) {
+        $this->extraclasses = $classes;
+
+        return $this;
     }
 
     /**
@@ -83,12 +164,26 @@ class notification implements \renderable, \templatable {
      * @return stdClass data context for a mustache template
      */
     public function export_for_template(\renderer_base $output) {
+        return array(
+            'message'       => clean_text($this->message),
+            'extraclasses'  => implode(' ', $this->extraclasses),
+            'announce'      => $this->announce,
+            'closebutton'   => $this->closebutton,
+        );
+    }
 
-        $data = new stdClass();
-
-        $data->type = $this->messagetype;
-        $data->message = $this->message;
+    public function get_template_name() {
+        $templatemappings = [
+            // Current types mapped to template names.
+            'success'           => 'core/notification_success',
+            'info'              => 'core/notification_info',
+            'warning'           => 'core/notification_warning',
+            'error'             => 'core/notification_error',
+        ];
 
-        return $data;
+        if (isset($templatemappings[$this->messagetype])) {
+            return $templatemappings[$this->messagetype];
+        }
+        return $templatemappings['error'];
     }
 }
index d565a22..4dd205e 100644 (file)
@@ -157,10 +157,18 @@ class manager {
     public static function init_empty_session() {
         global $CFG;
 
+        // Backup notifications. These should be preserved across session changes until the user fetches and clears them.
+        $notifications = [];
+        if (isset($GLOBALS['SESSION']->notifications)) {
+            $notifications = $GLOBALS['SESSION']->notifications;
+        }
         $GLOBALS['SESSION'] = new \stdClass();
 
         $GLOBALS['USER'] = new \stdClass();
         $GLOBALS['USER']->id = 0;
+
+        // Restore notifications.
+        $GLOBALS['SESSION']->notifications = $notifications;
         if (isset($CFG->mnet_localhost_id)) {
             $GLOBALS['USER']->mnethostid = $CFG->mnet_localhost_id;
         } else {
index 2c3835f..0793271 100644 (file)
@@ -31,22 +31,22 @@ $definitions = array(
     // Used to store processed lang files.
     // The keys used are the revision, lang and component of the string file.
     // The static acceleration size has been based upon student access of the site.
-    // NOTE: this data may be safely stored in local caches on cluster nodes.
     'string' => array(
         'mode' => cache_store::MODE_APPLICATION,
         'simplekeys' => true,
         'simpledata' => true,
         'staticacceleration' => true,
-        'staticaccelerationsize' => 30
+        'staticaccelerationsize' => 30,
+        'canuselocalstore' => true,
     ),
 
     // Used to store cache of all available translations.
-    // NOTE: this data may be safely stored in local caches on cluster nodes.
     'langmenu' => array(
         'mode' => cache_store::MODE_APPLICATION,
         'simplekeys' => true,
         'simpledata' => true,
         'staticacceleration' => true,
+        'canuselocalstore' => true,
     ),
 
     // Used to store database meta information.
@@ -91,9 +91,9 @@ $definitions = array(
     // This caches the html purifier cleaned text. This is done because the text is usually cleaned once for every user
     // and context combo. Text caching handles caching for the combination, this cache is responsible for caching the
     // cleaned text which is shareable.
-    // NOTE: this data may be safely stored in local caches on cluster nodes.
     'htmlpurifier' => array(
         'mode' => cache_store::MODE_APPLICATION,
+        'canuselocalstore' => true,
     ),
 
     // Used to store data from the config + config_plugins table in the database.
@@ -206,6 +206,7 @@ $definitions = array(
     'coursemodinfo' => array(
         'mode' => cache_store::MODE_APPLICATION,
         'simplekeys' => true,
+        'canuselocalstore' => true,
     ),
     // This is the session user selections cache.
     // It's a special cache that is used to record user selections that should persist for the lifetime of the session.
index 25c193e..fd42f35 100644 (file)
@@ -1067,7 +1067,17 @@ $functions = array(
         'description' => 'Generic service to update title',
         'type'        => 'write',
         'loginrequired' => true,
-        'ajax'        => true
+        'ajax'        => true,
+    ),
+
+    'core_fetch_notifications' => array(
+        'classname'   => 'core_external',
+        'methodname'  => 'fetch_notifications',
+        'classpath'   => 'lib/external/externallib.php',
+        'description' => 'Return a list of notifications for the current session',
+        'type'        => 'read',
+        'loginrequired' => false,
+        'ajax'        => true,
     ),
 
     // === Calendar related functions ===
index 6222822..f71453c 100644 (file)
@@ -912,14 +912,14 @@ function print_container_end($return=false) {
  * @param bool $return whether to return an output string or echo now
  * @return string|bool Depending on $result
  */
-function notify($message, $classes = 'notifyproblem', $align = 'center', $return = false) {
+function notify($message, $classes = 'error', $align = 'center', $return = false) {
     global $OUTPUT;
 
     debugging('notify() is deprecated, please use $OUTPUT->notification() instead', DEBUG_DEVELOPER);
 
     if ($classes == 'green') {
-        debugging('Use of deprecated class name "green" in notify. Please change to "notifysuccess".', DEBUG_DEVELOPER);
-        $classes = 'notifysuccess'; // Backward compatible with old color system
+        debugging('Use of deprecated class name "green" in notify. Please change to "success".', DEBUG_DEVELOPER);
+        $classes = 'success'; // Backward compatible with old color system.
     }
 
     $output = $OUTPUT->notification($message, $classes);
index 09ab53f..d5ed2a6 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index b6ff702..6d77779 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index bbc68c7..d15b0a7 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index e90b30a..3f6c460 100644 (file)
@@ -118,7 +118,10 @@ EditorSelection.prototype = {
                 return;
             }
             Y.soon(Y.bind(this._hasSelectionChanged, this, e));
-        }, null, this);
+        }, {
+            // Standalone will make sure all editors receive the end event.
+            standAlone: true
+        }, this);
 
         return this;
     },
index 7c38b77..44b75e1 100644 (file)
@@ -407,4 +407,58 @@ class core_external extends external_api {
             )
         );
     }
+
+    /**
+     * Returns description of fetch_notifications() parameters.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.1
+     */
+    public static function fetch_notifications_parameters() {
+        return new external_function_parameters(
+            array(
+                'contextid' => new external_value(PARAM_INT, 'Context ID', VALUE_REQUIRED),
+            ));
+    }
+
+    /**
+     * Returns description of fetch_notifications() result value.
+     *
+     * @return external_description
+     * @since Moodle 3.1
+     */
+    public static function fetch_notifications_returns() {
+        return new external_multiple_structure(
+            new external_single_structure(
+                array(
+                    'template'      => new external_value(PARAM_RAW, 'Name of the template'),
+                    'variables'     => new external_single_structure(array(
+                        'message'       => new external_value(PARAM_RAW, 'HTML content of the Notification'),
+                        'extraclasses'  => new external_value(PARAM_RAW, 'Extra classes to provide to the tmeplate'),
+                        'announce'      => new external_value(PARAM_RAW, 'Whether to announce'),
+                        'closebutton'   => new external_value(PARAM_RAW, 'Whether to close'),
+                    )),
+                )
+            )
+        );
+    }
+
+    /**
+     * Returns the list of notifications against the current session.
+     *
+     * @return array
+     * @since Moodle 3.1
+     */
+    public static function fetch_notifications($contextid) {
+        global $PAGE;
+
+        self::validate_parameters(self::fetch_notifications_parameters(), [
+                'contextid' => $contextid,
+            ]);
+
+        $context = \context::instance_by_id($contextid);
+        $PAGE->set_context($context);
+
+        return \core\notification::fetch_as_array($PAGE->get_renderer('core'));
+    }
 }
index 066f82e..b8a655e 100644 (file)
@@ -871,10 +871,13 @@ class core_renderer extends renderer_base {
      * @param boolean $debugdisableredirect this redirect has been disabled for
      *         debugging purposes. Display a message that explains, and don't
      *         trigger the redirect.
+     * @param string $messagetype The type of notification to show the message in.
+     *         See constants on \core\output\notification.
      * @return string The HTML to display to the user before dying, may contain
      *         meta refresh, javascript refresh, and may have set header redirects
      */
-    public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect) {
+    public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect,
+                                     $messagetype = \core\output\notification::NOTIFY_INFO) {
         global $CFG;
         $url = str_replace('&amp;', '&', $encodedurl);
 
@@ -905,7 +908,7 @@ class core_renderer extends renderer_base {
                 throw new coding_exception('You cannot redirect after the entire page has been generated');
                 break;
         }
-        $output .= $this->notification($message, 'redirectmessage');
+        $output .= $this->notification($message, $messagetype);
         $output .= '<div class="continuebutton">(<a href="'. $encodedurl .'">'. get_string('continue') .'</a>)</div>';
         if ($debugdisableredirect) {
             $output .= '<p><strong>'.get_string('erroroutput', 'error').'</strong></p>';
@@ -1032,7 +1035,7 @@ class core_renderer extends renderer_base {
      * @return string HTML fragment
      */
     public function footer() {
-        global $CFG, $DB;
+        global $CFG, $DB, $PAGE;
 
         $output = $this->container_end_all(true);
 
@@ -1057,6 +1060,7 @@ class core_renderer extends renderer_base {
         }
         $footer = str_replace($this->unique_performance_info_token, $performanceinfo, $footer);
 
+        $this->page->requires->js_call_amd('core/notification', 'init', array($PAGE->context->id, \core\notification::fetch_as_array($this)));
         $footer = str_replace($this->unique_end_html_token, $this->page->requires->get_end_code(), $footer);
 
         $this->page->set_state(moodle_page::STATE_DONE);
@@ -1086,22 +1090,37 @@ class core_renderer extends renderer_base {
      */
     public function course_content_header($onlyifnotcalledbefore = false) {
         global $CFG;
-        if ($this->page->course->id == SITEID) {
-            // return immediately and do not include /course/lib.php if not necessary
-            return '';
-        }
         static $functioncalled = false;
         if ($functioncalled && $onlyifnotcalledbefore) {
             // we have already output the content header
             return '';
         }
+
+        // Output any session notification.
+        $notifications = \core\notification::fetch();
+
+        $bodynotifications = '';
+        foreach ($notifications as $notification) {
+            $bodynotifications .= $this->render_from_template(
+                    $notification->get_template_name(),
+                    $notification->export_for_template($this)
+                );
+        }
+
+        $output = html_writer::span($bodynotifications, 'notifications', array('id' => 'user-notifications'));
+
+        if ($this->page->course->id == SITEID) {
+            // return immediately and do not include /course/lib.php if not necessary
+            return $output;
+        }
+
         require_once($CFG->dirroot.'/course/lib.php');
         $functioncalled = true;
         $courseformat = course_get_format($this->page->course);
         if (($obj = $courseformat->course_content_header()) !== null) {
-            return html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-header');
+            $output .= html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-header');
         }
-        return '';
+        return $output;
     }
 
     /**
@@ -2778,38 +2797,65 @@ EOD;
     }
 
     /**
-     * Output a notification (that is, a status message about something that has
-     * just happened).
+     * Output a notification (that is, a status message about something that has just happened).
      *
-     * @param string $message the message to print out
-     * @param string $classes normally 'notifyproblem' or 'notifysuccess'.
+     * Note: \core\notification::add() may be more suitable for your usage.
+     *
+     * @param string $message The message to print out.
+     * @param string $type    The type of notification. See constants on \core\output\notification.
      * @return string the HTML to output.
      */
-    public function notification($message, $classes = 'notifyproblem') {
-
-        $classmappings = array(
-            'notifyproblem' => \core\output\notification::NOTIFY_PROBLEM,
-            'notifytiny' => \core\output\notification::NOTIFY_PROBLEM,
-            'notifysuccess' => \core\output\notification::NOTIFY_SUCCESS,
-            'notifymessage' => \core\output\notification::NOTIFY_MESSAGE,
-            'redirectmessage' => \core\output\notification::NOTIFY_REDIRECT
-        );
-
-        // Identify what type of notification this is.
-        $type = \core\output\notification::NOTIFY_PROBLEM;
-        $classarray = explode(' ', self::prepare_classes($classes));
-        if (count($classarray) > 0) {
-            foreach ($classarray as $class) {
-                if (isset($classmappings[$class])) {
-                    $type = $classmappings[$class];
-                    break;
+    public function notification($message, $type = null) {
+        $typemappings = [
+            // Valid types.
+            'success'           => \core\output\notification::NOTIFY_SUCCESS,
+            'info'              => \core\output\notification::NOTIFY_INFO,
+            'warning'           => \core\output\notification::NOTIFY_WARNING,
+            'error'             => \core\output\notification::NOTIFY_ERROR,
+
+            // Legacy types mapped to current types.
+            'notifyproblem'     => \core\output\notification::NOTIFY_ERROR,
+            'notifytiny'        => \core\output\notification::NOTIFY_ERROR,
+            'notifyerror'       => \core\output\notification::NOTIFY_ERROR,
+            'notifysuccess'     => \core\output\notification::NOTIFY_SUCCESS,
+            'notifymessage'     => \core\output\notification::NOTIFY_INFO,
+            'notifyredirect'    => \core\output\notification::NOTIFY_INFO,
+            'redirectmessage'   => \core\output\notification::NOTIFY_INFO,
+        ];
+
+        $extraclasses = [];
+
+        if ($type) {
+            if (strpos($type, ' ') === false) {
+                // No spaces in the list of classes, therefore no need to loop over and determine the class.
+                if (isset($typemappings[$type])) {
+                    $type = $typemappings[$type];
+                } else {
+                    // The value provided did not match a known type. It must be an extra class.
+                    $extraclasses = [$type];
+                }
+            } else {
+                // Identify what type of notification this is.
+                $classarray = explode(' ', self::prepare_classes($type));
+
+                // Separate out the type of notification from the extra classes.
+                foreach ($classarray as $class) {
+                    if (isset($typemappings[$class])) {
+                        $type = $typemappings[$class];
+                    } else {
+                        $extraclasses[] = $class;
+                    }
                 }
             }
         }
 
-        $n = new \core\output\notification($message, $type);
-        return $this->render($n);
+        $notification = new \core\output\notification($message, $type);
+        if (count($extraclasses)) {
+            $notification->set_extra_classes($extraclasses);
+        }
 
+        // Return the rendered template.
+        return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
     }
 
     /**
@@ -2817,9 +2863,15 @@ EOD;
      *
      * @param string $message the message to print out
      * @return string HTML fragment.
+     * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
+     * @todo MDL-53113 This will be removed in Moodle 3.5.
+     * @see \core\output\notification
      */
     public function notify_problem($message) {
-        $n = new \core\output\notification($message, \core\output\notification::NOTIFY_PROBLEM);
+        debugging(__FUNCTION__ . ' is deprecated.' .
+            'Please use \core\notification::add, or \core\output\notification as required',
+            DEBUG_DEVELOPER);
+        $n = new \core\output\notification($message, \core\output\notification::NOTIFY_ERROR);
         return $this->render($n);
     }
 
@@ -2828,8 +2880,14 @@ EOD;
      *
      * @param string $message the message to print out
      * @return string HTML fragment.
+     * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
+     * @todo MDL-53113 This will be removed in Moodle 3.5.
+     * @see \core\output\notification
      */
     public function notify_success($message) {
+        debugging(__FUNCTION__ . ' is deprecated.' .
+            'Please use \core\notification::add, or \core\output\notification as required',
+            DEBUG_DEVELOPER);
         $n = new \core\output\notification($message, \core\output\notification::NOTIFY_SUCCESS);
         return $this->render($n);
     }
@@ -2839,9 +2897,15 @@ EOD;
      *
      * @param string $message the message to print out
      * @return string HTML fragment.
+     * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
+     * @todo MDL-53113 This will be removed in Moodle 3.5.
+     * @see \core\output\notification
      */
     public function notify_message($message) {
-        $n = new \core\output\notification($message, \core\output\notification::NOTIFY_MESSAGE);
+        debugging(__FUNCTION__ . ' is deprecated.' .
+            'Please use \core\notification::add, or \core\output\notification as required',
+            DEBUG_DEVELOPER);
+        $n = new \core\output\notification($message, \core\output\notification::NOTIFY_INFO);
         return $this->render($n);
     }
 
@@ -2850,9 +2914,15 @@ EOD;
      *
      * @param string $message the message to print out
      * @return string HTML fragment.
+     * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
+     * @todo MDL-53113 This will be removed in Moodle 3.5.
+     * @see \core\output\notification
      */
     public function notify_redirect($message) {
-        $n = new \core\output\notification($message, \core\output\notification::NOTIFY_REDIRECT);
+        debugging(__FUNCTION__ . ' is deprecated.' .
+            'Please use \core\notification::add, or \core\output\notification as required',
+            DEBUG_DEVELOPER);
+        $n = new \core\output\notification($message, \core\output\notification::NOTIFY_INFO);
         return $this->render($n);
     }
 
@@ -2864,30 +2934,7 @@ EOD;
      * @return string the HTML to output.
      */
     protected function render_notification(\core\output\notification $notification) {
-
-        $data = $notification->export_for_template($this);
-
-        $templatename = '';
-        switch($data->type) {
-            case \core\output\notification::NOTIFY_MESSAGE:
-                $templatename = 'core/notification_message';
-                break;
-            case \core\output\notification::NOTIFY_SUCCESS:
-                $templatename = 'core/notification_success';
-                break;
-            case \core\output\notification::NOTIFY_PROBLEM:
-                $templatename = 'core/notification_problem';
-                break;
-            case \core\output\notification::NOTIFY_REDIRECT:
-                $templatename = 'core/notification_redirect';
-                break;
-            default:
-                $templatename = 'core/notification_message';
-                break;
-        }
-
-        return self::render_from_template($templatename, $data);
-
+        return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
     }
 
     /**
@@ -4251,13 +4298,13 @@ class core_renderer_cli extends core_renderer {
     /**
      * Returns a template fragment representing a notification.
      *
-     * @param string $message The message to include
-     * @param string $classes A space-separated list of CSS classes
+     * @param string $message The message to print out.
+     * @param string $type    The type of notification. See constants on \core\output\notification.
      * @return string A template fragment for a notification
      */
-    public function notification($message, $classes = 'notifyproblem') {
+    public function notification($message, $type = null) {
         $message = clean_text($message);
-        if ($classes === 'notifysuccess') {
+        if ($type === 'notifysuccess' || $type === 'success') {
             return "++ $message ++\n";
         }
         return "!! $message !!\n";
@@ -4325,10 +4372,10 @@ class core_renderer_ajax extends core_renderer {
      * Used to display a notification.
      * For the AJAX notifications are discarded.
      *
-     * @param string $message
-     * @param string $classes
+     * @param string $message The message to print out.
+     * @param string $type    The type of notification. See constants on \core\output\notification.
      */
-    public function notification($message, $classes = 'notifyproblem') {}
+    public function notification($message, $type = null) {}
 
     /**
      * Used to display a redirection message.
@@ -4339,8 +4386,11 @@ class core_renderer_ajax extends core_renderer {
      * @param string $message
      * @param int $delay
      * @param bool $debugdisableredirect
+     * @param string $messagetype The type of notification to show the message in.
+     *         See constants on \core\output\notification.
      */
-    public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect) {}
+    public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect,
+                                     $messagetype = \core\output\notification::NOTIFY_INFO) {}
 
     /**
      * Prepares the start of an AJAX output.
similarity index 57%
rename from lib/templates/notification_redirect.mustache
rename to lib/templates/notification_error.mustache
index 4181bd0..0e7a8c8 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core/notification_redirect
+    @template core/notification_error
 
     Moodle notification template.
 
-    The purpose of this template is to render a message notification.
+    The purpose of this template is to render an error notification.
 
     Classes required for JS:
     * none
 
     Context variables required for this template:
     * message A cleaned string (use clean_text()) to display.
+    * extraclasses Additional classes to apply to the notification.
+    * closebutton Whether a close button should be displayed to dismiss the message.
+    * announce Whether the notification should be announced to screen readers.
 
     Example context (json):
-    { "message": "Your pants are on fire!" }
+    { "message": "Your pants are on fire!", "closebutton": 1, "announce": 1, "extraclasses": "foo bar"}
 }}
-<div class="alert alert-block alert-info">{{{message}}}</div>
+<div class="alert alert-error alert-block fade in {{ extraclasses }}" {{!
+    }}{{# announce }} aria-live="assertive"{{/ announce }}{{!
+    }}>
+    {{# closebutton }}<button type="button" class="close" data-dismiss="alert">&times;</button>{{/ closebutton }}
+    {{{ message }}}
+</div>
similarity index 57%
rename from lib/templates/notification_problem.mustache
rename to lib/templates/notification_info.mustache
index 67b6516..39cd151 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core/notification_problem
+    @template core/notification_info
 
     Moodle notification template.
 
-    The purpose of this template is to render a problem notification.
+    The purpose of this template is to render an info notification.
 
     Classes required for JS:
     * none
 
     Context variables required for this template:
     * message A cleaned string (use clean_text()) to display.
+    * extraclasses Additional classes to apply to the notification.
+    * closebutton Whether a close button should be displayed to dismiss the message.
+    * announce Whether the notification should be announced to screen readers.
 
     Example context (json):
-    { "message": "Your pants are on fire!" }
+    { "message": "Your pants are on fire!", "closebutton": 1, "announce": 1, "extraclasses": "foo bar"}
 }}
-<div class="alert alert-error">{{{message}}}</div>
+<div class="alert alert-info alert-block fade in {{ extraclasses }}" {{!
+    }}{{# announce }} aria-live="assertive"{{/ announce }}{{!
+    }}>
+    {{# closebutton }}<button type="button" class="close" data-dismiss="alert">&times;</button>{{/ closebutton }}
+    {{{ message }}}
+</div>
index 1f806f0..65b7e48 100644 (file)
 
     Context variables required for this template:
     * message A cleaned string (use clean_text()) to display.
+    * extraclasses Additional classes to apply to the notification.
+    * closebutton Whether a close button should be displayed to dismiss the message.
+    * announce Whether the notification should be announced to screen readers.
 
     Example context (json):
-    { "message": "Your pants are on fire!" }
+    { "message": "Your pants are on fire!", "closebutton": 1, "announce": 1, "extraclasses": "foo bar"}
 }}
-<div class="alert alert-success">{{{message}}}</div>
+<div class="alert alert-success alert-block fade in {{ extraclasses }}" {{!
+    }}{{# announce }} aria-live="assertive"{{/ announce }}{{!
+    }}>
+    {{# closebutton }}<button type="button" class="close" data-dismiss="alert">&times;</button>{{/ closebutton }}
+    {{{ message }}}
+</div>
similarity index 57%
rename from lib/templates/notification_message.mustache
rename to lib/templates/notification_warning.mustache
index 16d87b7..b359d83 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core/notification_message
+    @template core/notification_warning
 
     Moodle notification template.
 
-    The purpose of this template is to render a message notification.
+    The purpose of this template is to render a warning notification.
 
     Classes required for JS:
     * none
 
     Context variables required for this template:
     * message A cleaned string (use clean_text()) to display.
+    * extraclasses Additional classes to apply to the notification.
+    * closebutton Whether a close button should be displayed to dismiss the message.
+    * announce Whether the notification should be announced to screen readers.
 
     Example context (json):
-    { "message": "Your pants are on fire!" }
+    { "message": "Your pants are on fire!", "closebutton": 1, "announce": 1, "extraclasses": "foo bar"}
 }}
-<div class="alert alert-info">{{{message}}}</div>
+<div class="alert alert-warning alert-block fade in {{ extraclasses }}" {{!
+    }}{{# announce }} aria-live="assertive"{{/ announce }}{{!
+    }}>
+    {{# closebutton }}<button type="button" class="close" data-dismiss="alert">&times;</button>{{/ closebutton }}
+    {{{ message }}}
+</div>
diff --git a/lib/tests/notification_test.php b/lib/tests/notification_test.php
new file mode 100644 (file)
index 0000000..2cbc131
--- /dev/null
@@ -0,0 +1,122 @@
+<?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/>.
+
+/**
+ * Unit tests for core\notification.
+ *
+ * @package   core
+ * @category  phpunit
+ * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Unit tests for core\notification.
+ *
+ * @package   core
+ * @category  phpunit
+ * @category  phpunit
+ * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_notification_testcase extends advanced_testcase {
+
+    /**
+     * Setup required for all notification tests.
+     *
+     * This includes emptying the list of notifications on the session, resetting any session which exists, and setting
+     * up a new moodle_page object.
+     */
+    public function setUp() {
+        global $PAGE, $SESSION;
+
+        parent::setUp();
+        $PAGE = new moodle_page();
+        \core\session\manager::init_empty_session();
+        $SESSION->notifications = [];
+    }
+
+    /**
+     * Tear down required for all notification tests.
+     *
+     * This includes emptying the list of notifications on the session, resetting any session which exists, and setting
+     * up a new moodle_page object.
+     */
+    public function tearDown() {
+        global $PAGE, $SESSION;
+
+        $PAGE = null;
+        \core\session\manager::init_empty_session();
+        $SESSION->notifications = [];
+        parent::tearDown();
+    }
+
+    /**
+     * Test the way in which notifications are added to the session in different stages of the page load.
+     */
+    public function test_add_during_output_stages() {
+        global $PAGE, $SESSION;
+
+        \core\notification::add('Example before header', \core\notification::INFO);
+        $this->assertCount(1, $SESSION->notifications);
+
+        $PAGE->set_state(\moodle_page::STATE_PRINTING_HEADER);
+        \core\notification::add('Example during header', \core\notification::INFO);
+        $this->assertCount(2, $SESSION->notifications);
+
+        $PAGE->set_state(\moodle_page::STATE_IN_BODY);
+        \core\notification::add('Example in body', \core\notification::INFO);
+        $this->expectOutputRegex('/Example in body/');
+        $this->assertCount(2, $SESSION->notifications);
+
+        $PAGE->set_state(\moodle_page::STATE_DONE);
+        \core\notification::add('Example after page', \core\notification::INFO);
+        $this->assertCount(3, $SESSION->notifications);
+    }
+
+    /**
+     * Test fetching of notifications from the session.
+     */
+    public function test_fetch() {
+        // Initially there won't be any notifications.
+        $this->assertCount(0, \core\notification::fetch());
+
+        // Adding a notification should make one available to fetch.
+        \core\notification::success('Notification created');
+        $this->assertCount(1, \core\notification::fetch());
+        $this->assertCount(0, \core\notification::fetch());
+    }
+
+    /**
+     * Test that session notifications are persisted across session clears.
+     */
+    public function test_session_persistance() {
+        global $PAGE, $SESSION;
+
+        // Initially there won't be any notifications.
+        $this->assertCount(0, $SESSION->notifications);
+
+        // Adding a notification should make one available to fetch.
+        \core\notification::success('Notification created');
+        $this->assertCount(1, $SESSION->notifications);
+
+        // Re-creating the session will not empty the notification bag.
+        \core\session\manager::init_empty_session();
+        $this->assertCount(1, $SESSION->notifications);
+    }
+}
index a5a15d9..66a9af1 100644 (file)
@@ -59,7 +59,7 @@ class core_session_manager_testcase extends advanced_testcase {
         \core\session\manager::init_empty_session();
 
         $this->assertInstanceOf('stdClass', $SESSION);
-        $this->assertEmpty((array)$SESSION);
+        $this->assertCount(1, (array)$SESSION);
         $this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
         $this->assertSame($GLOBALS['SESSION'], $SESSION);
 
@@ -149,7 +149,7 @@ class core_session_manager_testcase extends advanced_testcase {
         $this->assertEquals(0, $USER->id);
 
         $this->assertInstanceOf('stdClass', $SESSION);
-        $this->assertEmpty((array)$SESSION);
+        $this->assertCount(1, (array)$SESSION);
         $this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
         $this->assertSame($GLOBALS['SESSION'], $SESSION);
 
index 6879ae5..891f61b 100644 (file)
@@ -76,7 +76,7 @@ class core_sessionlib_testcase extends advanced_testcase {
         $this->assertSame($PAGE->context, context_course::instance($SITE->id));
         $this->assertNotSame($adminsession, $SESSION);
         $this->assertObjectNotHasAttribute('test1', $SESSION);
-        $this->assertEmpty((array)$SESSION);
+        $this->assertCount(1, (array)$SESSION);
         $usersession1 = $SESSION;
         $SESSION->test2 = true;
         $this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
@@ -99,7 +99,7 @@ class core_sessionlib_testcase extends advanced_testcase {
         $this->assertSame($PAGE->context, context_course::instance($SITE->id));
         $this->assertNotSame($adminsession, $SESSION);
         $this->assertNotSame($usersession1, $SESSION);
-        $this->assertEmpty((array)$SESSION);
+        $this->assertCount(1, (array)$SESSION);
         $usersession2 = $SESSION;
         $usersession2->test3 = true;
         $this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
@@ -123,7 +123,7 @@ class core_sessionlib_testcase extends advanced_testcase {
         $this->assertSame($PAGE->context, context_course::instance($SITE->id));
         $this->assertNotSame($adminsession, $SESSION);
         $this->assertNotSame($usersession1, $SESSION);
-        $this->assertEmpty((array)$SESSION);
+        $this->assertCount(1, (array)$SESSION);
         $this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
         $this->assertSame($GLOBALS['SESSION'], $SESSION);
         $this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
index 68ac67a..c6796a4 100644 (file)
@@ -3,6 +3,17 @@ information provided here is intended especially for developers.
 
 === 3.1 ===
 
+* The redirect() function will now redirect immediately if output has not
+  already started. Messages will be displayed on the subsequent page using
+  session notifications. The type of message output can be configured using the
+  fourth parameter to redirect().
+* The specification of extra classes in the $OUTPUT->notification()
+  function, and \core\output\notification renderable have been deprecated
+  and will be removed in a future version.
+  Notifications should use the levels found in \core\output\notification.
+* The constants for NOTIFY_PROBLEM, NOTIFY_REDIRECT, and NOTIFY_MESSAGE in
+  \core\output\notification have been deprecated in favour of NOTIFY_ERROR,
+  NOTIFY_WARNING, and NOTIFY_INFO respectively.
 * The following functions, previously used (exclusively) by upgrade steps are not available
   anymore because of the upgrade cleanup performed for this version. See MDL-51580 for more info:
     - upgrade_mysql_fix_unsigned_and_lob_columns()
index f3ed1ef..18ce243 100644 (file)
@@ -2596,9 +2596,10 @@ function notice ($message, $link='', $course=null) {
  * @param moodle_url|string $url A moodle_url to redirect to. Strings are not to be trusted!
  * @param string $message The message to display to the user
  * @param int $delay The delay before redirecting
+ * @param string $messagetype The type of notification to show the message in. See constants on \core\output\notification.
  * @throws moodle_exception
  */
-function redirect($url, $message='', $delay=-1) {
+function redirect($url, $message='', $delay=null, $messagetype = \core\output\notification::NOTIFY_INFO) {
     global $OUTPUT, $PAGE, $CFG;
 
     if (CLI_SCRIPT or AJAX_SCRIPT) {
@@ -2606,6 +2607,10 @@ function redirect($url, $message='', $delay=-1) {
         throw new moodle_exception('redirecterrordetected', 'error');
     }
 
+    if ($delay === null) {
+        $delay = -1;
+    }
+
     // Prevent debug errors - make sure context is properly initialised.
     if ($PAGE) {
         $PAGE->set_context(null);
@@ -2696,10 +2701,18 @@ function redirect($url, $message='', $delay=-1) {
     $url = str_replace('&amp;', '&', $encodedurl);
 
     if (!empty($message)) {
-        if ($delay === -1 || !is_numeric($delay)) {
-            $delay = 3;
+        if (!$debugdisableredirect && !headers_sent()) {
+            // A message has been provided, and the headers have not yet been sent.
+            // Display the message as a notification on the subsequent page.
+            \core\notification::add($message, $messagetype);
+            $message = null;
+            $delay = 0;
+        } else {
+            if ($delay === -1 || !is_numeric($delay)) {
+                $delay = 3;
+            }
+            $message = clean_text($message);
         }
-        $message = clean_text($message);
     } else {
         $message = get_string('pageshouldredirect');
         $delay = 0;
@@ -2720,7 +2733,7 @@ function redirect($url, $message='', $delay=-1) {
     // Include a redirect message, even with a HTTP redirect, because that is recommended practice.
     if ($PAGE) {
         $CFG->docroot = false; // To prevent the link to moodle docs from being displayed on redirect page.
-        echo $OUTPUT->redirect_message($encodedurl, $message, $delay, $debugdisableredirect);
+        echo $OUTPUT->redirect_message($encodedurl, $message, $delay, $debugdisableredirect, $messagetype);
         exit;
     } else {
         echo bootstrap_renderer::early_redirect_message($encodedurl, $message, $delay);
index bfe019c..6ba2770 100644 (file)
@@ -100,6 +100,29 @@ class assign_feedback_comments extends assign_feedback_plugin {
         return ($newvalue !== false) && ($newvalue != $commenttext);
     }
 
+    /**
+     * Has the comment feedback been modified?
+     *
+     * @param stdClass $grade The grade object.
+     * @param stdClass $data Data from the form submission.
+     * @return boolean True if the comment feedback has been modified, else false.
+     */
+    public function is_feedback_modified(stdClass $grade, stdClass $data) {
+        $commenttext = '';
+        if ($grade) {
+            $feedbackcomments = $this->get_feedback_comments($grade->id);
+            if ($feedbackcomments) {
+                $commenttext = $feedbackcomments->commenttext;
+            }
+        }
+
+        if ($commenttext == $data->assignfeedbackcomments_editor['text']) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
 
     /**
      * Override to indicate a plugin supports quickgrading.
diff --git a/mod/assign/feedback/comments/tests/comments_test.php b/mod/assign/feedback/comments/tests/comments_test.php
new file mode 100644 (file)
index 0000000..8050ca4
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Unit tests for assignfeedback_comments
+ *
+ * @package    assignfeedback_comments
+ * @copyright  2016 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
+
+/**
+ * Unit tests for assignfeedback_comments
+ *
+ * @copyright  2016 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignfeedback_comments_testcase extends mod_assign_base_testcase {
+
+    /**
+     * Create an assign object and submit an online text submission.
+     */
+    protected function create_assign_and_submit_text() {
+        $assign = $this->create_instance(array('assignsubmission_onlinetext_enabled' => 1,
+                                               'assignfeedback_comments_enabled' => 1));
+
+        $user = $this->students[0];
+        $this->setUser($user);
+
+        // Create an online text submission.
+        $submission = $assign->get_user_submission($user->id, true);
+
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+                'text' => '<p>This is some text.</p>',
+                'format' => 1,
+                'itemid' => file_get_unused_draft_itemid());
+        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
+        $plugin->save($submission, $data);
+
+        return $assign;
+    }
+
+    /**
+     * Test the is_feedback_modified() method for the comments feedback.
+     */
+    public function test_is_feedback_modified() {
+        $assign = $this->create_assign_and_submit_text();
+
+        $this->setUser($this->teachers[0]);
+
+        // Create formdata.
+        $data = new stdClass();
+        $data->assignfeedbackcomments_editor = array(
+                'text' => '<p>first comment for this test</p>',
+                'format' => 1
+            );
+        $grade = $assign->get_user_grade($this->students[0]->id, true);
+
+        // This is the first time that we are submitting feedback, so it is modified.
+        $plugin = $assign->get_feedback_plugin_by_type('comments');
+        $this->assertTrue($plugin->is_feedback_modified($grade, $data));
+        // Save the feedback.
+        $plugin->save($grade, $data);
+        // Try again with the same data.
+        $this->assertFalse($plugin->is_feedback_modified($grade, $data));
+        // Change the data.
+        $data->assignfeedbackcomments_editor = array(
+                'text' => '<p>Altered comment for this test</p>',
+                'format' => 1
+            );
+        $this->assertTrue($plugin->is_feedback_modified($grade, $data));
+    }
+}
index 0bde754..57e0e71 100644 (file)
@@ -185,6 +185,53 @@ class assign_feedback_editpdf extends assign_feedback_plugin {
         }
     }
 
+    /**
+     * Check to see if the grade feedback for the pdf has been modified.
+     *
+     * @param stdClass $grade Grade object.
+     * @param stdClass $data Data from the form submission (not used).
+     * @return boolean True if the pdf has been modified, else false.
+     */
+    public function is_feedback_modified(stdClass $grade, stdClass $data) {
+        global $USER;
+        $pagenumbercount = document_services::page_number_for_attempt($this->assignment, $grade->userid, $grade->attemptnumber);
+        for ($i = 0; $i < $pagenumbercount; $i++) {
+            // Select all annotations.
+            $draftannotations = page_editor::get_annotations($grade->id, $i, true);
+            $nondraftannotations = page_editor::get_annotations($grade->id, $i, false);
+            // Check to see if the count is the same.
+            if (count($draftannotations) != count($nondraftannotations)) {
+                // The count is different so we have a modification.
+                return true;
+            } else {
+                // Have a closer look and see if the draft files match the non draft files.
+                foreach ($nondraftannotations as $index => $ndannotation) {
+                    foreach ($ndannotation as $key => $value) {
+                        if ($key != 'id' && $value != $draftannotations[$index]->$key) {
+                            return true;
+                        }
+                    }
+                }
+            }
+            // Select all comments.
+            $draftcomments = page_editor::get_comments($grade->id, $i, true);
+            $nondraftcomments = page_editor::get_comments($grade->id, $i, false);
+            if (count($draftcomments) != count($nondraftcomments)) {
+                return true;
+            } else {
+                // Go for a closer inspection.
+                foreach ($nondraftcomments as $index => $ndcomment) {
+                    foreach ($ndcomment as $key => $value) {
+                        if ($key != 'id' && $value != $draftcomments[$index]->$key) {
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * Generate the pdf.
      *
index 6afe93f..3a0ad84 100644 (file)
@@ -300,4 +300,124 @@ class assignfeedback_editpdf_testcase extends mod_assign_base_testcase {
 
         $this->assertEmpty($file3);
     }
+
+    /**
+     * Test that modifying the annotated pdf form return true when modified
+     * and false when not modified.
+     */
+    public function test_is_feedback_modified() {
+        global $DB;
+        $assign = $this->create_assign_and_submit_pdf();
+        $this->setUser($this->teachers[0]);
+
+        $grade = $assign->get_user_grade($this->students[0]->id, true);
+
+        $notempty = page_editor::has_annotations_or_comments($grade->id, false);
+        $this->assertFalse($notempty);
+
+        $comment = new comment();
+
+        $comment->rawtext = 'Comment text';
+        $comment->width = 100;
+        $comment->x = 100;
+        $comment->y = 100;
+        $comment->colour = 'red';
+
+        page_editor::set_comments($grade->id, 0, array($comment));
+
+        $annotations = array();
+
+        $annotation = new annotation();
+        $annotation->path = '';
+        $annotation->x = 100;
+        $annotation->y = 100;
+        $annotation->endx = 200;
+        $annotation->endy = 200;
+        $annotation->type = 'line';
+        $annotation->colour = 'red';
+        array_push($annotations, $annotation);
+
+        page_editor::set_annotations($grade->id, 0, $annotations);
+
+        $plugin = $assign->get_feedback_plugin_by_type('editpdf');
+        $data = new stdClass();
+        $data->editpdf_source_userid = $this->students[0]->id;
+        $this->assertTrue($plugin->is_feedback_modified($grade, $data));
+        $plugin->save($grade, $data);
+
+        $annotation = new annotation();
+        $annotation->gradeid = $grade->id;
+        $annotation->pageno = 0;
+        $annotation->path = '';
+        $annotation->x = 100;
+        $annotation->y = 100;
+        $annotation->endx = 200;
+        $annotation->endy = 200;
+        $annotation->type = 'rectangle';
+        $annotation->colour = 'yellow';
+
+        page_editor::add_annotation($annotation);
+
+        // Add a comment as well.
+        $comment = new comment();
+        $comment->gradeid = $grade->id;
+        $comment->pageno = 0;
+        $comment->rawtext = 'Second Comment text';
+        $comment->width = 100;
+        $comment->x = 100;
+        $comment->y = 100;
+        $comment->colour = 'red';
+        page_editor::add_comment($comment);
+
+        $this->assertTrue($plugin->is_feedback_modified($grade, $data));
+        $plugin->save($grade, $data);
+
+        // We should have two annotations.
+        $this->assertCount(2, page_editor::get_annotations($grade->id, 0, false));
+        // And two comments.
+        $this->assertCount(2, page_editor::get_comments($grade->id, 0, false));
+
+        // Add one annotation and delete another.
+        $annotation = new annotation();
+        $annotation->gradeid = $grade->id;
+        $annotation->pageno = 0;
+        $annotation->path = '100,100:105,105:110,100';
+        $annotation->x = 100;
+        $annotation->y = 100;
+        $annotation->endx = 110;
+        $annotation->endy = 105;
+        $annotation->type = 'pen';
+        $annotation->colour = 'black';
+        page_editor::add_annotation($annotation);
+
+        $annotations = page_editor::get_annotations($grade->id, 0, true);
+        page_editor::remove_annotation($annotations[1]->id);
+        $this->assertTrue($plugin->is_feedback_modified($grade, $data));
+        $plugin->save($grade, $data);
+
+        // We should have two annotations.
+        $this->assertCount(2, page_editor::get_annotations($grade->id, 0, false));
+        // And two comments.
+        $this->assertCount(2, page_editor::get_comments($grade->id, 0, false));
+
+        // Add a comment and then remove it. Should not be considered as modified.
+        $comment = new comment();
+        $comment->gradeid = $grade->id;
+        $comment->pageno = 0;
+        $comment->rawtext = 'Third Comment text';
+        $comment->width = 400;
+        $comment->x = 57;
+        $comment->y = 205;
+        $comment->colour = 'black';
+        $comment->id = page_editor::add_comment($comment);
+
+        // We should now have three comments.
+        $this->assertCount(3, page_editor::get_comments($grade->id, 0, true));
+        // Now delete the newest record.
+        page_editor::remove_comment($comment->id);
+        // Back to two comments.
+        $this->assertCount(2, page_editor::get_comments($grade->id, 0, true));
+        // No modification.
+        $this->assertFalse($plugin->is_feedback_modified($grade, $data));
+    }
 }
index c4d95bc..27d1b34 100644 (file)
@@ -77,6 +77,70 @@ class assign_feedback_file extends assign_feedback_plugin {
         return $fileoptions;
     }
 
+    /**
+     * Has the feedback file been modified?
+     *
+     * @param stdClass $grade Grade object.
+     * @param stdClass $data Form data.
+     * @return boolean True if the file area has been modified, else false.
+     */
+    public function is_feedback_modified(stdClass $grade, stdClass $data) {
+        global $USER;
+
+        $filekey = null;
+        $draftareainfo = null;
+        foreach ($data as $key => $value) {
+            if (strpos($key, 'files_') === 0) {
+                $filekey = $key;
+            }
+        }
+        if (isset($filekey)) {
+            $draftareainfo = file_get_draft_area_info($data->$filekey);
+            $filecount = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA);
+            if ($filecount != $draftareainfo['filecount']) {
+                return true;
+            } else {
+                // We need to check that the files in the draft area are the same as in the file area.
+                $usercontext = context_user::instance($USER->id);
+                $fs = get_file_storage();
+                $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $data->$filekey, 'id', true);
+                $files = $fs->get_area_files($this->assignment->get_context()->id,
+                                     'assignfeedback_file',
+                                     ASSIGNFEEDBACK_FILE_FILEAREA,
+                                     $grade->id,
+                                     'id',
+                                     false);
+                foreach ($files as $key => $file) {
+                    // Flag for recording if we have a matching file.
+                    $matchflag = false;
+                    foreach ($draftfiles as $draftkey => $draftfile) {
+                        if (!$file->is_directory()) {
+                            // File name is the same, but it could be a different file with the same name.
+                            if ($draftfile->get_filename() == $file->get_filename()) {
+                                // If the file name is the same but the content hash is different, or
+                                // The file path for the file has changed, then we have a modification.
+                                if ($draftfile->get_contenthash() != $file->get_contenthash() ||
+                                        $draftfile->get_filepath() != $file->get_filepath()) {
+                                    return true;
+                                }
+                                // These files match. Check the next file.
+                                $matchflag = true;
+                                // We have a match on the file name so we can move to the next file and not
+                                // proceed through the other draftfiles.
+                                break;
+                            }
+                        }
+                    }
+                    // If the file does not match then there has been a modification.
+                    if (!$matchflag) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * Copy all the files from one file area to another.
      *
diff --git a/mod/assign/feedback/file/tests/file_test.php b/mod/assign/feedback/file/tests/file_test.php
new file mode 100644 (file)
index 0000000..6063dc1
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for assignfeedback_file
+ *
+ * @package    assignfeedback_file
+ * @copyright  2016 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
+
+/**
+ * Unit tests for assignfeedback_file
+ *
+ * @copyright  2016 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignfeedback_file_testcase extends mod_assign_base_testcase {
+
+    /**
+     * Create an assign object and submit an online text submission.
+     */
+    protected function create_assign_and_submit_text() {
+        $assign = $this->create_instance(array('assignsubmission_onlinetext_enabled' => 1,
+                                               'assignfeedback_comments_enabled' => 1));
+
+        $user = $this->students[0];
+        $this->setUser($user);
+
+        // Create an online text submission.
+        $submission = $assign->get_user_submission($user->id, true);
+
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+                'text' => '<p>This is some text.</p>',
+                'format' => 1,
+                'itemid' => file_get_unused_draft_itemid());
+        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
+        $plugin->save($submission, $data);
+
+        return $assign;
+    }
+
+    /**
+     * Test the is_feedback_modified() method for the file feedback.
+     */
+    public function test_is_feedback_modified() {
+        $assign = $this->create_assign_and_submit_text();
+
+        $this->setUser($this->teachers[0]);
+
+        $fs = get_file_storage();
+        $context = context_user::instance($this->teachers[0]->id);
+        $draftitemid = file_get_unused_draft_itemid();
+        file_prepare_draft_area($draftitemid, $context->id, 'assignfeedback_file', 'feedback_files', 1);
+
+        $dummy = array(
+            'contextid' => $context->id,
+            'component' => 'user',
+            'filearea' => 'draft',
+            'itemid' => $draftitemid,
+            'filepath' => '/',
+            'filename' => 'feedback1.txt'
+        );
+
+        $file = $fs->create_file_from_string($dummy, 'This is the first feedback file');
+
+        // Create formdata.
+        $data = new stdClass();
+        $data->{'files_' . $this->students[0]->id . '_filemanager'} = $draftitemid;
+
+        $grade = $assign->get_user_grade($this->students[0]->id, true);
+
+        // This is the first time that we are submitting feedback, so it is modified.
+        $plugin = $assign->get_feedback_plugin_by_type('file');
+        $this->assertTrue($plugin->is_feedback_modified($grade, $data));
+        // Save the feedback.
+        $plugin->save($grade, $data);
+        // Try again with the same data.
+        $draftitemid = file_get_unused_draft_itemid();
+        file_prepare_draft_area($draftitemid, $context->id, 'assignfeedback_file', 'feedback_files', 1);
+
+        $dummy['itemid'] = $draftitemid;
+
+        $file = $fs->create_file_from_string($dummy, 'This is the first feedback file');
+
+        // Create formdata.
+        $data = new stdClass();
+        $data->{'files_' . $this->students[0]->id . '_filemanager'} = $draftitemid;
+
+        $this->assertFalse($plugin->is_feedback_modified($grade, $data));
+
+        // Same name for the file but different content.
+        $draftitemid = file_get_unused_draft_itemid();
+        file_prepare_draft_area($draftitemid, $context->id, 'assignfeedback_file', 'feedback_files', 1);
+
+        $dummy['itemid'] = $draftitemid;
+
+        $file = $fs->create_file_from_string($dummy, 'This is different feedback');
+
+        // Create formdata.
+        $data = new stdClass();
+        $data->{'files_' . $this->students[0]->id . '_filemanager'} = $draftitemid;
+
+        $this->assertTrue($plugin->is_feedback_modified($grade, $data));
+        $plugin->save($grade, $data);
+
+        // Add another file.
+        $draftitemid = file_get_unused_draft_itemid();
+        file_prepare_draft_area($draftitemid, $context->id, 'assignfeedback_file', 'feedback_files', 1);
+
+        $dummy['itemid'] = $draftitemid;
+
+        $file = $fs->create_file_from_string($dummy, 'This is different feedback');
+        $dummy['filename'] = 'feedback2.txt';
+        $file = $fs->create_file_from_string($dummy, 'A second feedback file');
+
+        // Create formdata.
+        $data = new stdClass();
+        $data->{'files_' . $this->students[0]->id . '_filemanager'} = $draftitemid;
+
+        $this->assertTrue($plugin->is_feedback_modified($grade, $data));
+        $plugin->save($grade, $data);
+
+        // Deleting a file.
+        $draftitemid = file_get_unused_draft_itemid();
+        file_prepare_draft_area($draftitemid, $context->id, 'assignfeedback_file', 'feedback_files', 1);
+
+        $dummy['itemid'] = $draftitemid;
+
+        $file = $fs->create_file_from_string($dummy, 'This is different feedback');
+
+        // Create formdata.
+        $data = new stdClass();
+        $data->{'files_' . $this->students[0]->id . '_filemanager'} = $draftitemid;
+
+        $this->assertTrue($plugin->is_feedback_modified($grade, $data));
+        $plugin->save($grade, $data);
+
+        // The file was moved to a folder.
+        $draftitemid = file_get_unused_draft_itemid();
+        file_prepare_draft_area($draftitemid, $context->id, 'assignfeedback_file', 'feedback_files', 1);
+
+        $dummy['itemid'] = $draftitemid;
+        $dummy['filepath'] = '/testdir/';
+
+        $file = $fs->create_file_from_string($dummy, 'This is different feedback');
+
+        // Create formdata.
+        $data = new stdClass();
+        $data->{'files_' . $this->students[0]->id . '_filemanager'} = $draftitemid;
+
+        $this->assertTrue($plugin->is_feedback_modified($grade, $data));
+        $plugin->save($grade, $data);
+
+        // No modification to the file in the folder.
+        $draftitemid = file_get_unused_draft_itemid();
+        file_prepare_draft_area($draftitemid, $context->id, 'assignfeedback_file', 'feedback_files', 1);
+
+        $dummy['itemid'] = $draftitemid;
+        $dummy['filepath'] = '/testdir/';
+
+        $file = $fs->create_file_from_string($dummy, 'This is different feedback');
+
+        // Create formdata.
+        $data = new stdClass();
+        $data->{'files_' . $this->students[0]->id . '_filemanager'} = $draftitemid;
+
+        $this->assertFalse($plugin->is_feedback_modified($grade, $data));
+    }
+}
index 6d793f4..8398a49 100644 (file)
@@ -112,6 +112,19 @@ abstract class assign_feedback_plugin extends assign_plugin {
         return false;
     }
 
+    /**
+     * Has the plugin form element been modified in the current submission?
+     *
+     * @param stdClass $grade The grade.
+     * @param stdClass $data Form data from the feedback form.
+     * @return boolean - True if the form element has been modified.
+     */
+    public function is_feedback_modified(stdClass $grade, stdClass $data) {
+        debugging('This plugin has not overwritten the is_feedback_modified() method. Please add this method to your plugin',
+                DEBUG_DEVELOPER);
+        return true;
+    }
+
     /**
      * Save quickgrading changes.
      *
index 5d44ce8..aa6d887 100644 (file)
@@ -6838,12 +6838,19 @@ class assign {
         $adminconfig = $this->get_admin_config();
         $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
 
+        $feedbackmodified = false;
+
         // Call save in plugins.
         foreach ($this->feedbackplugins as $plugin) {
             if ($plugin->is_enabled() && $plugin->is_visible()) {
-                if (!$plugin->save($grade, $formdata)) {
-                    $result = false;
-                    print_error($plugin->get_error());
+                $gradingmodified = $plugin->is_feedback_modified($grade, $formdata);
+                if ($gradingmodified) {
+                    if (!$plugin->save($grade, $formdata)) {
+                        $result = false;
+                        print_error($plugin->get_error());
+                    }
+                    // If $feedbackmodified is true, keep it true.
+                    $feedbackmodified = $feedbackmodified || $gradingmodified;
                 }
                 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
                     // This is the feedback plugin chose to push comments to the gradebook.
@@ -6852,10 +6859,12 @@ class assign {
                 }
             }
         }
+
         // We do not want to update the timemodified if no grade was added.
         if (!empty($formdata->addattempt) ||
                 ($originalgrade !== null && $originalgrade != -1) ||
-                ($grade->grade !== null && $grade->grade != -1)) {
+                ($grade->grade !== null && $grade->grade != -1) ||
+                $feedbackmodified) {
             $this->update_grade($grade, !empty($formdata->addattempt));
         }
         // Note the default if not provided for this option is true (e.g. webservices).
index 547ac86..f80ec78 100644 (file)
@@ -781,7 +781,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         // Wait 1 second so the submission and grade do not have the same timemodified.
         sleep(1);
         // Simulate adding a grade.
-        $this->setUser($this->teachers[0]);
+        $this->setUser($this->editingteachers[0]);
         $data = new stdClass();
         $data->grade = '50.0';
         $assign1->testable_apply_grade_to_user($data, $this->extrastudents[3]->id, 0);
@@ -898,7 +898,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         $plugin->save($submission, $data);
 
         // Simulate adding a grade.
-        $this->setUser($this->teachers[0]);
+        $this->setUser($this->editingteachers[0]);
         $data = new stdClass();
         $data->grade = '50.0';
         $assign->testable_apply_grade_to_user($data, $this->extrastudents[3]->id, 0);
index d8d0282..7cadc8d 100644 (file)
@@ -1,5 +1,9 @@
 This files describes API changes in the assign code.
 
+=== 3.1 ===
+* The feedback plugins now need to implement the is_feedback_modified() method. The default is to return true
+  for backwards compatibiltiy.
+
 === 3.0 ===
 * assign_submission_status renderable now requires $usergroups in its constructor
 
index 6eb49a0..1e9d1fd 100644 (file)
@@ -174,8 +174,13 @@ foreach ($modinfo->get_instances_of('forum') as $forumid=>$cm) {
 // Do course wide subscribe/unsubscribe if requested
 if (!is_null($subscribe)) {
     if (isguestuser() or !$can_subscribe) {
-        // there should not be any links leading to this place, just redirect
-        redirect(new moodle_url('/mod/forum/index.php', array('id' => $id)), get_string('subscribeenrolledonly', 'forum'));
+        // There should not be any links leading to this place, just redirect.
+        redirect(
+                new moodle_url('/mod/forum/index.php', array('id' => $id)),
+                get_string('subscribeenrolledonly', 'forum'),
+                null,
+                \core\output\notification::NOTIFY_ERROR
+            );
     }
     // Can proceed now, the user is not guest and is enrolled
     foreach ($modinfo->get_instances_of('forum') as $forumid=>$cm) {
@@ -204,9 +209,19 @@ if (!is_null($subscribe)) {
     $returnto = forum_go_back_to(new moodle_url('/mod/forum/index.php', array('id' => $course->id)));
     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
     if ($subscribe) {
-        redirect($returnto, get_string('nowallsubscribed', 'forum', $shortname), 1);
+        redirect(
+                $returnto,
+                get_string('nowallsubscribed', 'forum', $shortname),
+                null,
+                \core\output\notification::NOTIFY_SUCCESS
+            );
     } else {
-        redirect($returnto, get_string('nowallunsubscribed', 'forum', $shortname), 1);
+        redirect(
+                $returnto,
+                get_string('nowallunsubscribed', 'forum', $shortname),
+                null,
+                \core\output\notification::NOTIFY_SUCCESS
+            );
     }
 }
 
index ae8d72d..966442d 100644 (file)
@@ -75,4 +75,4 @@ if ($backtoindex) {
     $returnto = "view.php?f={$id}";
 }
 
-redirect($returnto, $updatemessage, 1);
+redirect($returnto, $updatemessage, null, \core\output\notification::NOTIFY_SUCCESS);
index fa6ed46..653fe36 100644 (file)
@@ -740,11 +740,6 @@ if ($mform_post->is_cancelled()) {
             $DB->update_record("forum", $forum);
         }
 
-        $timemessage = 2;
-        if (!empty($message)) { // if we're printing stuff about the file upload
-            $timemessage = 4;
-        }
-
         if ($realpost->userid == $USER->id) {
             $message .= '<br />'.get_string("postupdated", "forum");
         } else {
@@ -752,9 +747,7 @@ if ($mform_post->is_cancelled()) {
             $message .= '<br />'.get_string("editedpostupdated", "forum", fullname($realuser));
         }
 
-        if ($subscribemessage = forum_post_subscription($fromform, $forum, $discussion)) {
-            $timemessage = 4;
-        }
+        $subscribemessage = forum_post_subscription($fromform, $forum, $discussion);
         if ($forum->type == 'single') {
             // Single discussion forums are an exception. We show
             // the forum itself since it only has one discussion
@@ -782,10 +775,12 @@ if ($mform_post->is_cancelled()) {
         $event->add_record_snapshot('forum_discussions', $discussion);
         $event->trigger();
 
-        redirect(forum_go_back_to($discussionurl), $message.$subscribemessage, $timemessage);
-
-        exit;
-
+        redirect(
+                forum_go_back_to($discussionurl),
+                $message . $subscribemessage,
+                null,
+                \core\output\notification::NOTIFY_SUCCESS
+            );
 
     } else if ($fromform->discussion) { // Adding a new post to an existing discussion
         // Before we add this we must check that the user will not exceed the blocking threshold.
@@ -796,18 +791,10 @@ if ($mform_post->is_cancelled()) {
         $addpost = $fromform;
         $addpost->forum=$forum->id;
         if ($fromform->id = forum_add_new_post($addpost, $mform_post, $message)) {
-            $timemessage = 2;
-            if (!empty($message)) { // if we're printing stuff about the file upload
-                $timemessage = 4;
-            }
-
-            if ($subscribemessage = forum_post_subscription($fromform, $forum, $discussion)) {
-                $timemessage = 4;
-            }
+            $subscribemessage = forum_post_subscription($fromform, $forum, $discussion);
 
             if (!empty($fromform->mailnow)) {
                 $message .= get_string("postmailnow", "forum");
-                $timemessage = 4;
             } else {
                 $message .= '<p>'.get_string("postaddedsuccess", "forum") . '</p>';
                 $message .= '<p>'.get_string("postaddedtimeleft", "forum", format_time($CFG->maxeditingtime)) . '</p>';
@@ -843,7 +830,12 @@ if ($mform_post->is_cancelled()) {
                 $completion->update_state($cm,COMPLETION_COMPLETE);
             }
 
-            redirect(forum_go_back_to($discussionurl), $message.$subscribemessage, $timemessage);
+            redirect(
+                    forum_go_back_to($discussionurl),
+                    $message . $subscribemessage,
+                    null,
+                    \core\output\notification::NOTIFY_SUCCESS
+                );
 
         } else {
             print_error("couldnotadd", "forum", $errordestination);
@@ -924,22 +916,14 @@ if ($mform_post->is_cancelled()) {
                 $event->add_record_snapshot('forum_discussions', $discussion);
                 $event->trigger();
 
-                $timemessage = 2;
-                if (!empty($message)) { // If we're printing stuff about the file upload.
-                    $timemessage = 4;
-                }
-
                 if ($fromform->mailnow) {
                     $message .= get_string("postmailnow", "forum");
-                    $timemessage = 4;
                 } else {
                     $message .= '<p>'.get_string("postaddedsuccess", "forum") . '</p>';
                     $message .= '<p>'.get_string("postaddedtimeleft", "forum", format_time($CFG->maxeditingtime)) . '</p>';
                 }
 
-                if ($subscribemessage = forum_post_subscription($fromform, $forum, $discussion)) {
-                    $timemessage = 6;
-                }
+                $subscribemessage = forum_post_subscription($fromform, $forum, $discussion);
             } else {
                 print_error("couldnotadd", "forum", $errordestination);
             }
@@ -953,7 +937,12 @@ if ($mform_post->is_cancelled()) {
         }
 
         // Redirect back to the discussion.
-        redirect(forum_go_back_to($redirectto->out()), $message . $subscribemessage, $timemessage);
+        redirect(
+                forum_go_back_to($redirectto->out()),
+                $message . $subscribemessage,
+                null,
+                \core\output\notification::NOTIFY_SUCCESS
+            );
     }
 }
 
index 7bd8414..ab209b0 100644 (file)
@@ -99,8 +99,13 @@ if (is_null($mode) and !is_enrolled($context, $USER, '', true)) {   // Guests an
         echo $OUTPUT->footer();
         exit;
     } else {
-        // there should not be any links leading to this place, just redirect
-        redirect(new moodle_url('/mod/forum/view.php', array('f'=>$id)), get_string('subscribeenrolledonly', 'forum'));
+        // There should not be any links leading to this place, just redirect.
+        redirect(
+                new moodle_url('/mod/forum/view.php', array('f'=>$id)),
+                get_string('subscribeenrolledonly', 'forum'),
+                null,
+                \core\output\notification::NOTIFY_ERROR
+            );
     }
 }
 
@@ -117,11 +122,21 @@ if (!is_null($mode) and has_capability('mod/forum:managesubscriptions', $context
     switch ($mode) {
         case FORUM_CHOOSESUBSCRIBE : // 0
             \mod_forum\subscriptions::set_subscription_mode($forum->id, FORUM_CHOOSESUBSCRIBE);
-            redirect($returnto, get_string("everyonecannowchoose", "forum"), 1);
+            redirect(
+                    $returnto,
+                    get_string('everyonecannowchoose', 'forum'),
+                    null,
+                    \core\output\notification::NOTIFY_SUCCESS
+                );
             break;
         case FORUM_FORCESUBSCRIBE : // 1
             \mod_forum\subscriptions::set_subscription_mode($forum->id, FORUM_FORCESUBSCRIBE);
-            redirect($returnto, get_string("everyoneisnowsubscribed", "forum"), 1);
+            redirect(
+                    $returnto,
+                    get_string('everyoneisnowsubscribed', 'forum'),
+                    null,
+                    \core\output\notification::NOTIFY_SUCCESS
+                );
             break;
         case FORUM_INITIALSUBSCRIBE : // 2
             if ($forum->forcesubscribe <> FORUM_INITIALSUBSCRIBE) {
@@ -131,11 +146,21 @@ if (!is_null($mode) and has_capability('mod/forum:managesubscriptions', $context
                 }
             }
             \mod_forum\subscriptions::set_subscription_mode($forum->id, FORUM_INITIALSUBSCRIBE);
-            redirect($returnto, get_string("everyoneisnowsubscribed", "forum"), 1);
+            redirect(
+                    $returnto,
+                    get_string('everyoneisnowsubscribed', 'forum'),
+                    null,
+                    \core\output\notification::NOTIFY_SUCCESS
+                );
             break;
         case FORUM_DISALLOWSUBSCRIBE : // 3
             \mod_forum\subscriptions::set_subscription_mode($forum->id, FORUM_DISALLOWSUBSCRIBE);
-            redirect($returnto, get_string("noonecansubscribenow", "forum"), 1);
+            redirect(
+                    $returnto,
+                    get_string('noonecansubscribenow', 'forum'),
+                    null,
+                    \core\output\notification::NOTIFY_SUCCESS
+                );
             break;
         default:
             print_error(get_string('invalidforcesubscribe', 'forum'));
@@ -143,7 +168,12 @@ if (!is_null($mode) and has_capability('mod/forum:managesubscriptions', $context
 }
 
 if (\mod_forum\subscriptions::is_forcesubscribed($forum)) {
-    redirect($returnto, get_string("everyoneisnowsubscribed", "forum"), 1);
+    redirect(
+            $returnto,
+            get_string('everyoneisnowsubscribed', 'forum'),
+            null,
+            \core\output\notification::NOTIFY_SUCCESS
+        );
 }
 
 $info = new stdClass();
@@ -174,14 +204,24 @@ if ($issubscribed) {
     require_sesskey();
     if ($discussionid === null) {
         if (\mod_forum\subscriptions::unsubscribe_user($user->id, $forum, $context, true)) {
-            redirect($returnto, get_string("nownotsubscribed", "forum", $info), 1);
+            redirect(
+                    $returnto,
+                    get_string('nownotsubscribed', 'forum', $info),
+                    null,
+                    \core\output\notification::NOTIFY_SUCCESS
+                );
         } else {
             print_error('cannotunsubscribe', 'forum', get_local_referer(false));
         }
     } else {
         if (\mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $discussion, $context)) {
             $info->discussion = $discussion->name;
-            redirect($returnto, get_string("discussionnownotsubscribed", "forum", $info), 1);
+            redirect(
+                    $returnto,
+                    get_string('discussionnownotsubscribed', 'forum', $info),
+                    null,
+                    \core\output\notification::NOTIFY_SUCCESS
+                );
         } else {
             print_error('cannotunsubscribe', 'forum', get_local_referer(false));
         }
@@ -217,10 +257,20 @@ if ($issubscribed) {
     require_sesskey();
     if ($discussionid == null) {
         \mod_forum\subscriptions::subscribe_user($user->id, $forum, $context, true);
-        redirect($returnto, get_string("nowsubscribed", "forum", $info), 1);
+        redirect(
+                $returnto,
+                get_string('nowsubscribed', 'forum', $info),
+                null,
+                \core\output\notification::NOTIFY_SUCCESS
+            );
     } else {
         $info->discussion = $discussion->name;
         \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion, $context);
-        redirect($returnto, get_string("discussionnowsubscribed", "forum", $info), 1);
+        redirect(
+                $returnto,
+                get_string('discussionnowsubscribed', 'forum', $info),
+                null,
+                \core\output\notification::NOTIFY_SUCCESS
+            );
     }
 }
index 760bfcf..e3cb931 100644 (file)
@@ -39,27 +39,27 @@ Feature: A user can control their own subscription preferences for a discussion
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I click on "You are not subscribed to this discussion. Click to subscribe." "link" in the "Test post subject one" "table_row"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test post subject one' of 'Test forum name'"
     And I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I click on "You are subscribed to this discussion. Click to unsubscribe." "link" in the "Test post subject one" "table_row"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test post subject one' of 'Test forum name'"
     And I should see "Subscribe to this forum"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I click on "You are not subscribed to this discussion. Click to subscribe." "link" in the "Test post subject one" "table_row"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test post subject one' of 'Test forum name'"
     And I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I follow "Subscribe to this forum"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test forum name'"
     And I should see "Unsubscribe from this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject two" "table_row"
     And I follow "Unsubscribe from this forum"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test forum name'"
     And I should see "Subscribe to this forum"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
@@ -84,27 +84,27 @@ Feature: A user can control their own subscription preferences for a discussion
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject two" "table_row"
     And I click on "You are subscribed to this discussion. Click to unsubscribe." "link" in the "Test post subject one" "table_row"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test post subject one' of 'Test forum name'"
     And I should see "Unsubscribe from this forum"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject two" "table_row"
     And I click on "You are not subscribed to this discussion. Click to subscribe." "link" in the "Test post subject one" "table_row"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test post subject one' of 'Test forum name'"
     And I should see "Unsubscribe from this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject two" "table_row"
     And I click on "You are subscribed to this discussion. Click to unsubscribe." "link" in the "Test post subject one" "table_row"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test post subject one' of 'Test forum name'"
     And I should see "Unsubscribe from this forum"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject two" "table_row"
     And I follow "Unsubscribe from this forum"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test forum name'"
     And I should see "Subscribe to this forum"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I follow "Subscribe to this forum"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test forum name'"
     And I should see "Unsubscribe from this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject two" "table_row"
@@ -129,7 +129,7 @@ Feature: A user can control their own subscription preferences for a discussion
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I click on "You are not subscribed to this discussion. Click to subscribe." "link" in the "Test post subject one" "table_row"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test post subject one' of 'Test forum name'"
     And I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
@@ -150,8 +150,8 @@ Feature: A user can control their own subscription preferences for a discussion
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject two" "table_row"
     When I follow "Unsubscribe from this forum"
-    And I follow "Continue"
-    Then I should see "Subscribe to this forum"
+    Then I should see "Student One will NOT be notified of new posts in 'Test forum name'"
+    And I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
 
@@ -175,7 +175,7 @@ Feature: A user can control their own subscription preferences for a discussion
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I click on "You are not subscribed to this discussion. Click to subscribe." "link" in the "Test post subject one" "table_row"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test post subject one' of 'Test forum name'"
     And I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
@@ -196,7 +196,7 @@ Feature: A user can control their own subscription preferences for a discussion
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject two" "table_row"
     When I follow "Unsubscribe from this forum"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test forum name'"
     Then I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
@@ -325,31 +325,31 @@ Feature: A user can control their own subscription preferences for a discussion
     Then I should see "Subscribe to this forum"
     And I should see "Subscribe to this discussion"
     And I follow "Subscribe to this forum"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test forum name'"
     And I follow "Test post subject one"
     And I should see "Unsubscribe from this forum"
     And I should see "Unsubscribe from this discussion"
     And I follow "Unsubscribe from this discussion"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test post subject one' of 'Test forum name'"
     And I follow "Test post subject one"
     And I should see "Unsubscribe from this forum"
     And I should see "Subscribe to this discussion"
     And I follow "Unsubscribe from this forum"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test forum name'"
     And I follow "Test post subject one"
     And I should see "Subscribe to this forum"
     And I should see "Subscribe to this discussion"
     And I follow "Subscribe to this discussion"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test post subject one' of 'Test forum name'"
     And I should see "Subscribe to this forum"
     And I should see "Unsubscribe from this discussion"
     And I follow "Subscribe to this forum"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test forum name'"
     And I follow "Test post subject one"
     And I should see "Unsubscribe from this forum"
     And I should see "Unsubscribe from this discussion"
     And I follow "Unsubscribe from this forum"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test forum name'"
     And I follow "Test post subject one"
     And I should see "Subscribe to this forum"
     And I should see "Subscribe to this discussion"
index acab5cd..f155118 100644 (file)
@@ -71,7 +71,7 @@ Feature: A user can control their own subscription preferences for a forum
     Then I should see "Subscribe to this forum"
     And I should not see "Unsubscribe from this forum"
     And I follow "Subscribe to this forum"
-    And I follow "Continue"
+    And I should see "Student One will be notified of new posts in 'Test forum name'"
     And I should see "Unsubscribe from this forum"
     And I should not see "Subscribe to this forum"
 
@@ -91,6 +91,6 @@ Feature: A user can control their own subscription preferences for a forum
     Then I should see "Unsubscribe from this forum"
     And I should not see "Subscribe to this forum"
     And I follow "Unsubscribe from this forum"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test forum name'"
     And I should see "Subscribe to this forum"
     And I should not see "Unsubscribe from this forum"
index d5da6b6..e40e7b2 100644 (file)
@@ -121,7 +121,7 @@ Feature: A user can control their default discussion subscription settings
     And I follow "Course 1"
     And I follow "Test forum name"
     And I click on "You are subscribed to this discussion. Click to unsubscribe." "link" in the "Test post subject" "table_row"
-    And I follow "Continue"
+    And I should see "Student One will NOT be notified of new posts in 'Test post subject' of 'Test forum name'"
     And I follow "Test post subject"
     When I follow "Reply"
     And "input[name=discussionsubscribe][checked=checked]" "css_element" should exist
@@ -130,7 +130,7 @@ Feature: A user can control their default discussion subscription settings
     And I follow "Course 1"
     And I follow "Test forum name"
     And I click on "You are subscribed to this discussion. Click to unsubscribe." "link" in the "Test post subject" "table_row"
-    And I follow "Continue"
+    And I should see "Student Two will NOT be notified of new posts in 'Test post subject' of 'Test forum name'"
     And I follow "Test post subject"
     And I follow "Reply"
     And "input[name=discussionsubscribe]:not([checked=checked])" "css_element" should exist
index e3c2867..f47f6cc 100644 (file)
@@ -62,17 +62,15 @@ $PAGE->requires->string_for_js('show', 'moodle');
 $PAGE->set_title($course->shortname.': '.$imscp->name);
 $PAGE->set_heading($course->fullname);
 $PAGE->set_activity_record($imscp);
-echo $OUTPUT->header();
-echo $OUTPUT->heading(format_string($imscp->name));
 
 // Verify imsmanifest was parsed properly.
 if (!$imscp->structure) {
-    echo $OUTPUT->notification(get_string('deploymenterror', 'imscp'), 'notifyproblem');
-    echo $OUTPUT->continue_button(course_get_url($course->id, $cm->section));
-    echo $OUTPUT->footer();
-    die;
+    redirect(course_get_url($course->id, $cm->section), get_string('deploymenterror', 'imscp'));
 }
 
+echo $OUTPUT->header();
+echo $OUTPUT->heading(format_string($imscp->name));
+
 imscp_print_content($imscp, $cm, $course);
 
 echo $OUTPUT->footer();
index 5cb1145..646073a 100644 (file)
@@ -302,11 +302,7 @@ class quiz_overview_report extends quiz_attempts_report {
      * @uses exit. This method never returns.
      */
     protected function finish_regrade($nexturl) {
-        global $OUTPUT, $PAGE;
-        echo $OUTPUT->heading(get_string('regradecomplete', 'quiz_overview'), 3);
-        echo $OUTPUT->continue_button($nexturl);
-        echo $OUTPUT->footer();
-        die();
+        redirect($nexturl, get_string('regradecomplete', 'quiz_overview'), null, \core\output\notification::NOTIFY_SUCCESS);
     }
 
     /**
index 3e9664e..d995f51 100644 (file)
@@ -66,7 +66,7 @@ Feature: Reset all personalised pages to default
     And I log in as "admin"
     And I navigate to "Default Dashboard page" node in "Site administration > Appearance"
     When I press "Reset Dashboard for all users"
-    And I follow "Continue"
+    And I should see "All Dashboard pages have been reset to default."
     And I log out
 
     And I log in as "student1"
@@ -108,7 +108,7 @@ Feature: Reset all personalised pages to default
     And I log in as "admin"
     And I navigate to "Default profile page" node in "Site administration > Appearance"
     When I press "Reset profile for all users"
-    And I follow "Continue"
+    And I should see "All profile pages have been reset to default."
     And I log out
 
     And I log in as "student2"
index 966abee..74231be 100644 (file)
@@ -29,13 +29,11 @@ Feature: Users can flag tags and manager can reset flags
     And I follow "Badtag"
     And I follow "Flag as inappropriate"
     And I should see "The person responsible will be notified"
-    And I follow "Continue"
     And I navigate to "Participants" node in "Site pages"
     And I follow "User 1"
     And I follow "Sweartag"
     And I follow "Flag as inappropriate"
     And I should see "The person responsible will be notified"
-    And I follow "Continue"
     And I log out
     And I log in as "user3"
     And I navigate to "Participants" node in "Site pages"
@@ -43,7 +41,6 @@ Feature: Users can flag tags and manager can reset flags
     And I follow "Sweartag"
     And I follow "Flag as inappropriate"
     And I should see "The person responsible will be notified"
-    And I follow "Continue"
     And I log out
 
   Scenario: Managing tag flags with javascript disabled
index b86f04c..05587d5 100644 (file)
@@ -6,6 +6,11 @@ information provided here is intended especially for theme designer.
 * A new search box for global search has been added to bootstrap and clean layout files, if
   your theme is overwriting columns1.php, columns2.php or columns3.php you will need to add a
   call to core_renderer::search_box to display it.
+* Notification templates have been renamed to better suit types of alert
+  rather than uses. The following changes have been made:
+  * notification_problem.mustache => notification_error.mustache
+  * notification_message          => notification_info
+  * notification_redirect         => notification_warning
 
 === 3.0 ===
 
index 7adb835..bfc3945 100644 (file)
@@ -45,16 +45,15 @@ $stremailupdate = get_string('emailupdate', 'auth', $a);
 $PAGE->set_title(format_string($SITE->fullname) . ": $stremailupdate");
 $PAGE->set_heading(format_string($SITE->fullname) . ": $stremailupdate");
 
-echo $OUTPUT->header();
-
 if (empty($preferences['newemailattemptsleft'])) {
     redirect("$CFG->wwwroot/user/view.php?id=$user->id");
 
 } else if ($preferences['newemailattemptsleft'] < 1) {
     cancel_email_update($user->id);
-    $stroutofattempts = get_string('auth_outofnewemailupdateattempts', 'auth');
-    echo $OUTPUT->box($stroutofattempts, 'center');
 
+    echo $OUTPUT->header();
+    echo $OUTPUT->box(get_string('auth_outofnewemailupdateattempts', 'auth'), 'center');
+    echo $OUTPUT->footer();
 } else if ($key == $preferences['newemailkey']) {
     $olduser = clone($user);
     cancel_email_update($user->id);
@@ -62,25 +61,25 @@ if (empty($preferences['newemailattemptsleft'])) {
 
     // Detect duplicate before saving.
     if ($DB->get_record('user', array('email' => $user->email))) {
-        $stremailnowexists = get_string('emailnowexists', 'auth');
-        echo $OUTPUT->box($stremailnowexists, 'center');
-        echo $OUTPUT->continue_button("$CFG->wwwroot/user/view.php?id=$user->id");
+        redirect(new moodle_url('/user/view.php', ['id' => $user->id]), get_string('emailnowexists', 'auth'));
     } else {
         // Update user email.
         $authplugin = get_auth_plugin($user->auth);
         $authplugin->user_update($olduser, $user);
         user_update_user($user, false);
         $a->email = $user->email;
-        $stremailupdatesuccess = get_string('emailupdatesuccess', 'auth', $a);
-        echo $OUTPUT->box($stremailupdatesuccess, 'center');
-        echo $OUTPUT->continue_button("$CFG->wwwroot/user/view.php?id=$user->id");
+        redirect(
+                new moodle_url('/user/view.php', ['id' => $user->id]),
+                get_string('emailupdatesuccess', 'auth', $a),
+                null,
+                \core\output\notification::NOTIFY_SUCCESS
+            );
     }
 
 } else {
     $preferences['newemailattemptsleft']--;
     set_user_preference('newemailattemptsleft', $preferences['newemailattemptsleft'], $user->id);
-    $strinvalidkey = get_string('auth_invalidnewemailkey', 'auth');
-    echo $OUTPUT->box($strinvalidkey, 'center');
+    echo $OUTPUT->header();
+    echo $OUTPUT->box(get_string('auth_invalidnewemailkey', 'auth'), 'center');
+    echo $OUTPUT->footer();
 }
-
-echo $OUTPUT->footer();
index 8fbca14..0829ca8 100644 (file)
@@ -110,12 +110,12 @@ $fullname = fullname($user, has_capability('moodle/site:viewfullnames', $coursec
 if ($currentuser) {
     if (!is_viewing($coursecontext) && !is_enrolled($coursecontext)) {
         // Need to have full access to a course to see the rest of own info.
-        echo $OUTPUT->header();
-        echo $OUTPUT->heading(get_string('notenrolled', '', $fullname));
         $referer = get_local_referer(false);
         if (!empty($referer)) {
-            echo $OUTPUT->continue_button($referer);
+            redirect($referer, get_string('notenrolled', '', $fullname));
         }
+        echo $OUTPUT->header();
+        echo $OUTPUT->heading(get_string('notenrolled', '', $fullname));
         echo $OUTPUT->footer();
         die;
     }
@@ -136,17 +136,17 @@ if ($currentuser) {
         //       or test for course:inspect capability.
         if (has_capability('moodle/role:assign', $coursecontext)) {
             $PAGE->navbar->add($fullname);
-            echo $OUTPUT->header();
-            echo $OUTPUT->heading(get_string('notenrolled', '', $fullname));
+            $notice = get_string('notenrolled', '', $fullname);
         } else {
-            echo $OUTPUT->header();
             $PAGE->navbar->add($struser);
-            echo $OUTPUT->heading(get_string('notenrolledprofile'));
+            $notice = get_string('notenrolledprofile', '', $fullname);
         }
         $referer = get_local_referer(false);
         if (!empty($referer)) {
-            echo $OUTPUT->continue_button($referer);
+            redirect($referer, $notice);
         }
+        echo $OUTPUT->header();
+        echo $OUTPUT->heading($notice);
         echo $OUTPUT->footer();
         exit;
     }
index 4f06df6..ab95f05 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016022500.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016030100.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.