From 0346323cecb7c69fc9e5341f2e2af9d628ed366d Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 16 Feb 2016 08:48:39 +0800 Subject: [PATCH] MDL-30811 output: Add support for session notifications --- lib/amd/build/notification.min.js | Bin 671 -> 2167 bytes lib/amd/src/notification.js | 190 +++++++++++++++++--- lib/classes/notification.php | 165 +++++++++++++++++ lib/classes/output/notification.php | 36 ++++ lib/classes/session/manager.php | 8 + lib/db/services.php | 12 +- lib/external/externallib.php | 54 ++++++ lib/outputrenderers.php | 40 +++-- lib/templates/notification_error.mustache | 9 +- lib/templates/notification_info.mustache | 9 +- lib/templates/notification_success.mustache | 9 +- lib/templates/notification_warning.mustache | 9 +- lib/tests/notification_test.php | 122 +++++++++++++ lib/tests/session_manager_test.php | 4 +- lib/tests/sessionlib_test.php | 6 +- version.php | 2 +- 16 files changed, 621 insertions(+), 54 deletions(-) create mode 100644 lib/classes/notification.php create mode 100644 lib/tests/notification_test.php diff --git a/lib/amd/build/notification.min.js b/lib/amd/build/notification.min.js index 37952ecce657876371431e889b0b613295202bfb..7df55c238aeae53272bd79c94e87b24ef4654a8f 100644 GIT binary patch literal 2167 zcmah~+mhTU4E+@ksY;+ilc|05tapAOI}f}2*3~tIL3We4!NGVg<@CR=pndC0C2w3L zaU>lbG*rd{-4f+Ie0lE;;pE$o9{f8l!X6IrTsjxJ;ALCN4!+DgyiYy>4s9a?bkl+S zCf}xY<7%?wRc}==&Qi%suDIsFD>l4HpVh?>-dpJMq3e}`uFFqBtqZ0yN=C2e@rrO` zD|gEvo)i8my*0MJWiVb54&FO|%OYKnBd-iJ`bi!jCw&Kgvj&10RuI(wX$7v!=j2Wq z$pVCuO+&ZC0m9yCn}WqsJtyzyi;&XSn7(p0z*{gnuf@DR9(U|PZtr(OJ8V)dq}FGT zvm?JeTJT6V$e%x{EVetwO94{tu>mt&>;)TY;lXO~>!`F8Q%}(iyJTe9K*x^wS_h$T z|3kLyI|3#d6HGq@T9lHBgXbx%>1>TT(jxp296r7&~57 zb;%No8hNXVS|s9%M4X2Os=zKrID=|j2W1}u7U}MuOKTmD0mOCm%*jMK86i#;f268d zc3eXP0kY+U<+w)oj~^-Nv2$^HHMuL|nX9r9)%IkxL#r9-bjDUe(TQQrZ*V|yiS53= zSXF{6Hk|Ulg+z%1jAZY=aYko)d=JT}*ooN-DcNw%pSi^diHS;@21fzj6tqzu zSe~L*LSwaSO4d})JSZ?PptDzlv)G3$&6W)!`}ezPTa*hy;t)09!;)GODV!dG5CZxAd(3kf(Z@eB_B(YEB+4MS6M=n>m!c1==ScF zrU^rUmGOrQFekl)^3`4h)(Y6F(0;i!zNzX(*M`I2XUG_MGHdF`_!iVnu=3jFxAe zK8~U>7;9GCi$j63oOl}lgF@nfUdb-SG$;yZEg2)=LXLs4n9#gp^qfybMELnuaC&6{{bi34wwJ{ delta 53 zcmew^FrQUDB{eNGFI6L2DLKC=Rll+{Qz=#_tu!yWBr`uxBT=)uD7B=tD6e`m8*3>O IbFHQ|00?Rlga7~l diff --git a/lib/amd/src/notification.js b/lib/amd/src/notification.js index 1880b80db06..845a2e63290 100644 --- a/lib/amd/src/notification.js +++ b/lib/amd/src/notification.js @@ -14,6 +14,8 @@ // along with Moodle. If not, see . /** + * 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. * @@ -24,20 +26,99 @@ * @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 = $('').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 index 00000000000..aeb7e0365b2 --- /dev/null +++ b/lib/classes/notification.php @@ -0,0 +1,165 @@ +. + +namespace core; + +/** + * User Alert notifications. + * + * @package core + * @copyright 2016 Andrew Nicols + * @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); + } +} diff --git a/lib/classes/output/notification.php b/lib/classes/output/notification.php index f97af740486..12c9aaf075f 100644 --- a/lib/classes/output/notification.php +++ b/lib/classes/output/notification.php @@ -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, ); } diff --git a/lib/classes/session/manager.php b/lib/classes/session/manager.php index d565a22cfa4..4dd205ecc2f 100644 --- a/lib/classes/session/manager.php +++ b/lib/classes/session/manager.php @@ -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 { diff --git a/lib/db/services.php b/lib/db/services.php index 25c193e9586..fd42f3531e5 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -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 === diff --git a/lib/external/externallib.php b/lib/external/externallib.php index 7c38b776cb7..44b75e12c36 100644 --- a/lib/external/externallib.php +++ b/lib/external/externallib.php @@ -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')); + } } diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 99997c8075b..b2aca169f81 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -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); diff --git a/lib/templates/notification_error.mustache b/lib/templates/notification_error.mustache index 3929ca013c2..0e7a8c8448b 100644 --- a/lib/templates/notification_error.mustache +++ b/lib/templates/notification_error.mustache @@ -30,10 +30,15 @@ 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"} }} -
+
+ {{# closebutton }}{{/ closebutton }} {{{ message }}}
diff --git a/lib/templates/notification_info.mustache b/lib/templates/notification_info.mustache index dc42ca26484..39cd1511c94 100644 --- a/lib/templates/notification_info.mustache +++ b/lib/templates/notification_info.mustache @@ -30,10 +30,15 @@ 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"} }} -
+
+ {{# closebutton }}{{/ closebutton }} {{{ message }}}
diff --git a/lib/templates/notification_success.mustache b/lib/templates/notification_success.mustache index ef7aeb09d0f..65b7e48321f 100644 --- a/lib/templates/notification_success.mustache +++ b/lib/templates/notification_success.mustache @@ -30,10 +30,15 @@ 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"} }} -
+
+ {{# closebutton }}{{/ closebutton }} {{{ message }}}
diff --git a/lib/templates/notification_warning.mustache b/lib/templates/notification_warning.mustache index 1fe001d3f4f..b359d830ae5 100644 --- a/lib/templates/notification_warning.mustache +++ b/lib/templates/notification_warning.mustache @@ -30,10 +30,15 @@ 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"} }} -
+
+ {{# closebutton }}{{/ closebutton }} {{{ message }}}
diff --git a/lib/tests/notification_test.php b/lib/tests/notification_test.php new file mode 100644 index 00000000000..2cbc131f1bd --- /dev/null +++ b/lib/tests/notification_test.php @@ -0,0 +1,122 @@ +. + +/** + * Unit tests for core\notification. + * + * @package core + * @category phpunit + * @copyright 2016 Andrew Nicols + * @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 + * @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); + } +} diff --git a/lib/tests/session_manager_test.php b/lib/tests/session_manager_test.php index a5a15d904e2..66a9af13d1c 100644 --- a/lib/tests/session_manager_test.php +++ b/lib/tests/session_manager_test.php @@ -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); diff --git a/lib/tests/sessionlib_test.php b/lib/tests/sessionlib_test.php index 6879ae58aaa..891f61b5e3c 100644 --- a/lib/tests/sessionlib_test.php +++ b/lib/tests/sessionlib_test.php @@ -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']); diff --git a/version.php b/version.php index 4f06df6ce28..ab95f0598cc 100644 --- a/version.php +++ b/version.php @@ -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. -- 2.43.0