MDL-30811 output: Add support for session notifications
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 16 Feb 2016 00:48:39 +0000 (08:48 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 29 Feb 2016 01:05:56 +0000 (09:05 +0800)
16 files changed:
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/services.php
lib/external/externallib.php
lib/outputrenderers.php
lib/templates/notification_error.mustache
lib/templates/notification_info.mustache
lib/templates/notification_success.mustache
lib/templates/notification_warning.mustache
lib/tests/notification_test.php [new file with mode: 0644]
lib/tests/session_manager_test.php
lib/tests/sessionlib_test.php
version.php

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 f97af74..12c9aaf 100644 (file)
@@ -83,6 +83,16 @@ class notification implements \renderable, \templatable {
      */
     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.
      */
@@ -111,6 +121,30 @@ class notification implements \renderable, \templatable {
         }
     }
 
+    /**
+     * 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.
      *
@@ -133,6 +167,8 @@ class notification implements \renderable, \templatable {
         return array(
             'message'       => clean_text($this->message),
             'extraclasses'  => implode(' ', $this->extraclasses),
+            'announce'      => $this->announce,
+            'closebutton'   => $this->closebutton,
         );
     }
 
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 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 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 99997c8..b2aca16 100644 (file)
@@ -1032,7 +1032,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 +1057,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 +1087,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;
     }
 
     /**
@@ -2780,6 +2796,8 @@ EOD;
     /**
      * Output a notification (that is, a status message about something that has just happened).
      *
+     * 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.
@@ -2848,7 +2866,7 @@ EOD;
      */
     public function notify_problem($message) {
         debugging(__FUNCTION__ . ' is deprecated.' .
-            'Please use notification() or \core\output\notification as required',
+            '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);
@@ -2865,7 +2883,7 @@ EOD;
      */
     public function notify_success($message) {
         debugging(__FUNCTION__ . ' is deprecated.' .
-            'Please use notification() or \core\output\notification as required',
+            '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);
@@ -2882,7 +2900,7 @@ EOD;
      */
     public function notify_message($message) {
         debugging(__FUNCTION__ . ' is deprecated.' .
-            'Please use notification() or \core\output\notification as required',
+            '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);
@@ -2899,7 +2917,7 @@ EOD;
      */
     public function notify_redirect($message) {
         debugging(__FUNCTION__ . ' is deprecated.' .
-            'Please use notification() or \core\output\notification as required',
+            '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);
index 3929ca0..0e7a8c8 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!", "extraclasses": "foo bar"}
+    { "message": "Your pants are on fire!", "closebutton": 1, "announce": 1, "extraclasses": "foo bar"}
 }}
-<div class="alert alert-error alert-block fade in {{ extraclasses }}">
+<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>
index dc42ca2..39cd151 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!", "extraclasses": "foo bar"}
+    { "message": "Your pants are on fire!", "closebutton": 1, "announce": 1, "extraclasses": "foo bar"}
 }}
-<div class="alert alert-info alert-block fade in {{ extraclasses }}">
+<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 ef7aeb0..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!", "extraclasses": "foo bar"}
+    { "message": "Your pants are on fire!", "closebutton": 1, "announce": 1, "extraclasses": "foo bar"}
 }}
-<div class="alert alert-success alert-block fade in {{ extraclasses }}">
+<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>
index 1fe001d..b359d83 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!", "extraclasses": "foo bar"}
+    { "message": "Your pants are on fire!", "closebutton": 1, "announce": 1, "extraclasses": "foo bar"}
 }}
-<div class="alert alert-warning alert-block fade in {{ extraclasses }}">
+<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 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.