Commit 0346323c authored by Andrew Nicols's avatar Andrew Nicols
Browse files

MDL-30811 output: Add support for session notifications

parent 24346803
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -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.
*
......@@ -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 = $('<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
};
});
<?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);
}
}
......@@ -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,
);
}
......
......@@ -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 {
......
......@@ -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 ===
......
......@@ -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'));
}
}
......@@ -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);