}
// Get HTML from logger.
- $loghtml = $logger->get_html();
+ if ($CFG->debugdisplay) {
+ $loghtml = $logger->get_html();
+ }
// Hide the progress display and first backup step bar (the 'finished' step will show next).
echo html_writer::end_div();
// Do actual restore.
$restore->execute();
// Get HTML from logger.
- $loghtml = $logger->get_html();
+ if ($CFG->debugdisplay) {
+ $loghtml = $logger->get_html();
+ }
// Hide this section because we are now going to make the page show 'finished'.
echo html_writer::end_div();
echo html_writer::script('document.getElementById("executionprogress").style.display = "none";');
--- /dev/null
+// 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/>.
+
+/**
+ * Parse the response from the navblock ajax page and render the correct DOM
+ * structure for the tree from it.
+ *
+ * @module block_navigation/ajax_response_renderer
+ * @package core
+ * @copyright 2015 John Okely <john@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery'], function($) {
+
+ // Mappings for the different types of nodes coming from the navigation.
+ // Copied from lib/navigationlib.php navigation_node constants.
+ var NODETYPE = {
+ // @type int Activity (course module) = 40.
+ ACTIVITY : 40,
+ // @type int Resource (course module = 50.
+ RESOURCE : 50,
+ };
+
+ /**
+ * Build DOM.
+ *
+ * @method buildDOM
+ * @param {Object} rootElement the root element of DOM.
+ * @param {object} nodes jquery object representing the nodes to be build.
+ * @return
+ */
+ function buildDOM(rootElement, nodes) {
+ var ul = $('<ul></ul>');
+ ul.attr('role', 'group');
+
+ $.each(nodes, function(index, node) {
+ if (typeof node !== 'object') {
+ return;
+ }
+
+ var li = $('<li></li>');
+ var p = $('<p></p>');
+ var icon = null;
+ var isBranch = (node.expandable || node.haschildren) ? true : false;
+
+ p.addClass('tree_item');
+ p.attr('id', node.id);
+ li.attr('role', 'treeitem');
+
+ if (node.requiresajaxloading) {
+ li.attr('data-requires-ajax', true);
+ li.attr('data-node-id', node.id);
+ li.attr('data-node-key', node.key);
+ li.attr('data-node-type', node.type);
+ }
+
+ if (isBranch) {
+ li.addClass('collapsed contains_branch');
+ li.attr('aria-expanded', false);
+ p.addClass('branch');
+ }
+
+ if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) {
+ li.addClass('item_with_icon');
+ p.addClass('hasicon');
+
+ icon = $('<img/>');
+ icon.attr('alt', node.icon.alt);
+ icon.attr('title', node.icon.title);
+ icon.attr('src', M.util.image_url(node.icon.pix, node.icon.component));
+ $.each(node.icon.classes, function(index, className) {
+ icon.addClass(className);
+ });
+ }
+
+ if (node.link) {
+ var link = $('<a title="' + node.title + '" href="' + node.link + '"></a>');
+
+ if (icon) {
+ link.append(icon);
+ link.append('<span class="item-content-wrap">'+node.name+'</span>');
+ } else {
+ link.text(node.name);
+ }
+
+ if (node.hidden) {
+ link.addClass('dimmed');
+ }
+
+ p.append(link);
+ } else {
+ var span = $('<span></span>');
+
+ if (icon) {
+ span.append(icon);
+ span.append('<span class="item-content-wrap">'+node.name+'</span>');
+ } else {
+ span.text(node.name);
+ }
+
+ if (node.hidden) {
+ span.addClass('dimmed');
+ }
+
+ p.append(span);
+ }
+
+ li.append(p);
+ ul.append(li);
+
+ if (node.children && node.children.length) {
+ buildDOM(li, node.children);
+ } else if (isBranch && !node.requiresajaxloading) {
+ li.removeClass('contains_branch');
+ li.addClass('emptybranch');
+ }
+ });
+
+ rootElement.append(ul);
+ }
+
+ return {
+ render: function(element, nodes) {
+ // The first element of the response is the existing node so we start with processing the children.
+ if (nodes.children && nodes.children.length) {
+ buildDOM(element, nodes.children);
+ } else {
+ if (element.hasClass('contains_branch')) {
+ element.removeClass('contains_branch').addClass('emptybranch');
+ }
+ }
+ }
+ };
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Load the nav tree items via ajax and render the response.
+ *
+ * @module block_navigation/nav_loader
+ * @package core
+ * @copyright 2015 John Okely <john@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/ajax', 'core/config', 'block_navigation/ajax_response_renderer'],
+ function($, ajax, config, renderer) {
+ var URL = config.wwwroot + '/lib/ajax/getnavbranch.php';
+
+ /**
+ * Get the block instance id.
+ *
+ * @function getBlockInstanceId
+ * @param element
+ * @returns {*}
+ */
+ function getBlockInstanceId(element) {
+ return element.closest('[data-block]').attr('data-instanceid');
+ }
+
+ return {
+ load: function(element) {
+ element = $(element);
+ var promise = $.Deferred();
+ var data = {
+ elementid: element.attr('data-node-id'),
+ id: element.attr('data-node-key'),
+ type: element.attr('data-node-type'),
+ sesskey: config.sesskey,
+ instance: getBlockInstanceId(element)
+ };
+ var settings = {
+ type: 'POST',
+ dataType: 'json',
+ data: data
+ };
+
+ $.ajax(URL, settings).done(function(nodes) {
+ renderer.render(element, nodes);
+ promise.resolve();
+ });
+
+ return promise;
+ }
+ };
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Load the navigation tree javascript.
+ *
+ * @module block_navigation/navblock
+ * @package core
+ * @copyright 2015 John Okely <john@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/tree'], function($, Tree) {
+ return {
+ init: function() {
+ new Tree(".block_navigation .block_tree");
+ }
+ };
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Load the site admin nav tree via ajax and render the response.
+ *
+ * @module block_navigation/site_admin_loader
+ * @package core
+ * @copyright 2015 John Okely <john@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/ajax', 'core/config', 'block_navigation/ajax_response_renderer'],
+ function($, ajax, config, renderer) {
+
+ var SITE_ADMIN_NODE_TYPE = 71;
+ var URL = config.wwwroot + '/lib/ajax/getsiteadminbranch.php';
+
+ return {
+ load: function(element) {
+ element = $(element);
+ var promise = $.Deferred();
+ var data = {
+ type: SITE_ADMIN_NODE_TYPE,
+ sesskey: config.sesskey
+ };
+ var settings = {
+ type: 'POST',
+ dataType: 'json',
+ data: data
+ };
+
+ $.ajax(URL, settings).done(function(nodes) {
+ renderer.render(element, nodes);
+ promise.resolve();
+ });
+
+ return promise;
+ }
+ };
+});
* Gets Javascript that may be required for navigation
*/
function get_required_javascript() {
- global $CFG;
parent::get_required_javascript();
- $limit = 20;
- if (!empty($CFG->navcourselimit)) {
- $limit = $CFG->navcourselimit;
- }
- $expansionlimit = 0;
- if (!empty($this->config->expansionlimit)) {
- $expansionlimit = $this->config->expansionlimit;
- }
- $arguments = array(
- 'id' => $this->instance->id,
- 'instance' => $this->instance->id,
- 'candock' => $this->instance_can_be_docked(),
- 'courselimit' => $limit,
- 'expansionlimit' => $expansionlimit
- );
$this->page->requires->string_for_js('viewallcourses', 'moodle');
- $this->page->requires->yui_module('moodle-block_navigation-navigation', 'M.block_navigation.init_add_tree', array($arguments));
+ $this->page->requires->js_call_amd('block_navigation/navblock', 'init', array());
}
/**
* @return object $this->content
*/
function get_content() {
+ global $CFG;
// First check if we have already generated, don't waste cycles
if ($this->contentgenerated === true) {
return $this->content;
}
}
- $this->page->requires->data_for_js('navtreeexpansions'.$this->instance->id, $expandable);
+ $limit = 20;
+ if (!empty($CFG->navcourselimit)) {
+ $limit = $CFG->navcourselimit;
+ }
+ $expansionlimit = 0;
+ if (!empty($this->config->expansionlimit)) {
+ $expansionlimit = $this->config->expansionlimit;
+ }
+ $arguments = array(
+ 'id' => $this->instance->id,
+ 'instance' => $this->instance->id,
+ 'candock' => $this->instance_can_be_docked(),
+ 'courselimit' => $limit,
+ 'expansionlimit' => $expansionlimit
+ );
$options = array();
$options['linkcategories'] = (!empty($this->config->linkcategories) && $this->config->linkcategories == 'yes');
*/
public function navigation_tree(global_navigation $navigation, $expansionlimit, array $options = array()) {
$navigation->add_class('navigation_node');
- $content = $this->navigation_node(array($navigation), array('class'=>'block_tree list'), $expansionlimit, $options);
+ $navigationattrs = array(
+ 'class' => 'block_tree list',
+ 'role' => 'tree',
+ 'data-ajax-loader' => 'block_navigation/nav_loader');
+ $content = $this->navigation_node(array($navigation), $navigationattrs, $expansionlimit, $options);
if (isset($navigation->id) && !is_numeric($navigation->id) && !empty($content)) {
$content = $this->output->box($content, 'block_tree_box', $navigation->id);
}
// Turn our navigation items into list items.
$lis = array();
+ $number = 0;
foreach ($items as $item) {
+ $number++;
if (!$item->display && !$item->contains_active_node()) {
continue;
}
continue;
}
- $attributes = array();
+ $nodetextid = 'label_' . $depth . '_' . $number;
+ $attributes = array('tabindex' => '-1', 'id' => $nodetextid);
if ($title !== '') {
$attributes['title'] = $title;
}
if (is_string($item->action) || empty($item->action) ||
(($item->type === navigation_node::TYPE_CATEGORY || $item->type === navigation_node::TYPE_MY_CATEGORY) &&
empty($options['linkcategories']))) {
- $attributes['tabindex'] = '0'; //add tab support to span but still maintain character stream sequence.
$content = html_writer::tag('span', $content, $attributes);
} else if ($item->action instanceof action_link) {
//TODO: to be replaced with something else
$divclasses = array('tree_item');
$liexpandable = array();
- if ($item->has_children() && (!$item->forceopen || $item->collapse)) {
- $liclasses[] = 'collapsed';
- }
+ $lirole = array('role' => 'treeitem');
if ($isbranch) {
$liclasses[] = 'contains_branch';
- $liexpandable = array('aria-expanded' => in_array('collapsed', $liclasses) ? "false" : "true");
+ if ($depth == 1) {
+ $liexpandable = array(
+ 'data-expandable' => 'false',
+ 'data-collapsible' => 'false'
+ );
+ } else {
+ $liexpandable = array(
+ 'aria-expanded' => ($item->has_children() &&
+ (!$item->forceopen || $item->collapse)) ? "false" : "true");
+ }
+
+ if ($item->requiresajaxloading) {
+ $liexpandable['data-requires-ajax'] = 'true';
+ $liexpandable['data-loaded'] = 'false';
+ $liexpandable['data-node-id'] = $item->id;
+ $liexpandable['data-node-key'] = $item->key;
+ $liexpandable['data-node-type'] = $item->type;
+ }
+
$divclasses[] = 'branch';
} else {
$divclasses[] = 'leaf';
}
// Now build attribute arrays.
- $liattr = array('class' => join(' ', $liclasses)) + $liexpandable;
+ $liattr = array('class' => join(' ', $liclasses)) + $liexpandable + $lirole;
$divattr = array('class'=>join(' ', $divclasses));
if (!empty($item->id)) {
$divattr['id'] = $item->id;
// Create the structure.
$content = html_writer::tag('p', $content, $divattr);
if ($isexpandable) {
- $content .= $this->navigation_node($item->children, array(), $expansionlimit, $options, $depth+1);
+ $content .= $this->navigation_node($item->children, array('role' => 'group'), $expansionlimit,
+ $options, $depth + 1);
}
if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
$content = html_writer::empty_tag('hr') . $content;
}
+ if ($depth == 1) {
+ $liattr['tabindex'] = '0';
+ }
+ $liattr['aria-labelledby'] = $nodetextid;
$content = html_writer::tag('li', $content, $liattr);
$lis[] = $content;
}
background-image: url('[[pix:i/loading_small]]');
}
+.block_navigation .block_tree .loading .tree_item.branch {
+ background-image: url('[[pix:i/loading_small]]');
+}
+
+.block_navigation .block_tree .emptybranch .tree_item,
+.block_navigation .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
+ padding-left: 21px;
+ background-image: url('[[pix:t/collapsed_empty]]');
+}
+
.block_navigation .block_tree .tree_item img {
width: 16px;
height: 16px;
list-style: none;
}
-.jsenabled .block_navigation .block_tree li.collapsed ul {
+.jsenabled .block_navigation .block_tree [aria-expanded="false"] ul {
display: none;
}
-.jsenabled .block_navigation .block_tree li.collapsed .tree_item.branch {
+.jsenabled .block_navigation .block_tree [aria-expanded="false"] .tree_item.branch {
background-image: url('[[pix:t/collapsed]]');
}
+.jsenabled .block_navigation .block_tree [aria-expanded="false"].loading .tree_item.branch {
+ background-image: url('[[pix:i/loading_small]]');
+}
+
+.jsenabled .block_navigation .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
+ padding-left: 21px;
+ background-image: url('[[pix:t/collapsed_empty]]');
+}
+
.jsenabled .block_navigation.dock_on_load {
display: none;
}
padding-left: 0;
}
-.dir-rtl .block_navigation .block_tree .tree_item.emptybranch {
+.dir-rtl .block_navigation .block_tree .tree_item.emptybranch,
+.dir-rtl .block_navigation .block_tree .emptybranch .tree_item {
padding-right: 21px;
padding-left: 0;
background-image: url('[[pix:t/collapsed_empty_rtl]]');
margin: 0 16px 0 0;
}
-.dir-rtl.jsenabled .block_navigation .block_tree .collapsed .tree_item.branch {
+.dir-rtl.jsenabled .block_navigation .block_tree [aria-expanded="false"] .tree_item.branch {
background-image: url('[[pix:t/collapsed_rtl]]');
}
+
+.dir-rtl.jsenabled .block_navigation .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
+ background-image: url('[[pix:t/collapsed_empty_rtl]]');
+}
+++ /dev/null
-{
- "name": "moodle-block_navigation-navigation",
- "builds": {
- "moodle-block_navigation-navigation": {
- "jsfiles": [
- "navigation.js"
- ]
- }
- }
-}
+++ /dev/null
-/**
- * Navigation block JS.
- *
- * This file contains the Navigation block JS..
- *
- * @module moodle-block_navigation-navigation
- */
-
-/**
- * This namespace will contain all of the contents of the navigation blocks
- * global navigation and settings.
- * @class M.block_navigation
- * @static
- */
-M.block_navigation = M.block_navigation || {};
-/**
- * The number of expandable branches in existence.
- *
- * @property expandablebranchcount
- * @protected
- * @static
- * @type Number
- */
-M.block_navigation.expandablebranchcount = 1;
-/**
- * The maximum number of courses to show as part of a branch.
- *
- * @property courselimit
- * @protected
- * @static
- * @type Number
- */
-M.block_navigation.courselimit = 20;
-/**
- * Add new instance of navigation tree to tree collection
- *
- * @method init_add_tree
- * @static
- * @param {Object} properties
- */
-M.block_navigation.init_add_tree = function(properties) {
- if (properties.courselimit) {
- this.courselimit = properties.courselimit;
- }
- new TREE(properties);
-};
-
-/**
- * A 'actionkey' Event to help with Y.delegate().
- * The event consists of the left arrow, right arrow, enter and space keys.
- * More keys can be mapped to action meanings.
- * actions: collapse , expand, toggle, enter.
- *
- * This event is delegated to branches in the navigation tree.
- * The on() method to subscribe allows specifying the desired trigger actions as JSON.
- *
- * @namespace M.block_navigation
- * @class ActionKey
- */
-Y.Event.define("actionkey", {
- // Webkit and IE repeat keydown when you hold down arrow keys.
- // Opera links keypress to page scroll; others keydown.
- // Firefox prevents page scroll via preventDefault() on either
- // keydown or keypress.
- _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
-
- /**
- * The keys to trigger on.
- * @method _keys
- */
- _keys: {
- //arrows
- '37': 'collapse',
- '39': 'expand',
- '32': 'toggle',
- '13': 'enter'
- },
-
- /**
- * Handles key events
- * @method _keyHandler
- * @param {EventFacade} e
- * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
- * @param {Object} args
- */
- _keyHandler: function (e, notifier, args) {
- var actObj;
- if (!args.actions) {
- actObj = {collapse:true, expand:true, toggle:true, enter:true};
- } else {
- actObj = args.actions;
- }
- if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) {
- e.action = this._keys[e.keyCode];
- notifier.fire(e);
- }
- },
-
- /**
- * Subscribes to events.
- * @method on
- * @param {Node} node The node this subscription was applied to.
- * @param {Subscription} sub The object tracking this subscription.
- * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
- */
- on: function (node, sub, notifier) {
- // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
- if (sub.args === null) {
- //no actions given
- sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false});
- } else {
- sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]);
- }
- },
-
- /**
- * Detaches an event listener
- * @method detach
- */
- detach: function (node, sub) {
- //detach our _detacher handle of the subscription made in on()
- sub._detacher.detach();
- },
-
- /**
- * Creates a delegated event listener.
- * @method delegate
- * @param {Node} node The node this subscription was applied to.
- * @param {Subscription} sub The object tracking this subscription.
- * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
- * @param {String|function} filter Selector string or function that accpets an event object and returns null.
- */
- delegate: function (node, sub, notifier, filter) {
- // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
- if (sub.args === null) {
- //no actions given
- sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false});
- } else {
- sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]);
- }
- },
-
- /**
- * Detaches a delegated event listener.
- * @method detachDelegate
- * @param {Node} node The node this subscription was applied to.
- * @param {Subscription} sub The object tracking this subscription.
- * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
- * @param {String|function} filter Selector string or function that accpets an event object and returns null.
- */
- detachDelegate: function (node, sub) {
- sub._delegateDetacher.detach();
- }
-});
-
-var EXPANSIONLIMIT_EVERYTHING = 0,
- EXPANSIONLIMIT_COURSE = 20,
- EXPANSIONLIMIT_SECTION = 30,
- EXPANSIONLIMIT_ACTIVITY = 40;
-
-// Mappings for the different types of nodes coming from the navigation.
-// Copied from lib/navigationlib.php navigation_node constants.
-var NODETYPE = {
- // @type int Root node = 0
- ROOTNODE : 0,
- // @type int System context = 1
- SYSTEM : 1,
- // @type int Course category = 10
- CATEGORY : 10,
- // @type int MYCATEGORY = 11
- MYCATEGORY : 11,
- // @type int Course = 20
- COURSE : 20,
- // @type int Course section = 30
- SECTION : 30,
- // @type int Activity (course module) = 40
- ACTIVITY : 40,
- // @type int Resource (course module = 50
- RESOURCE : 50,
- // @type int Custom node (could be anything) = 60
- CUSTOM : 60,
- // @type int Setting = 70
- SETTING : 70,
- // @type int site administration = 71
- SITEADMIN : 71,
- // @type int User context = 80
- USER : 80,
- // @type int Container = 90
- CONTAINER : 90
-};
-
-/**
- * Navigation tree class.
- *
- * This class establishes the tree initially, creating expandable branches as
- * required, and delegating the expand/collapse event.
- *
- * @namespace M.block_navigation
- * @class Tree
- * @constructor
- * @extends Base
- */
-var TREE = function() {
- TREE.superclass.constructor.apply(this, arguments);
-};
-TREE.prototype = {
- /**
- * The tree's ID, normally its block instance id.
- * @property id
- * @type Number
- * @protected
- */
- id : null,
- /**
- * An array of initialised branches.
- * @property branches
- * @type Array
- * @protected
- */
- branches : [],
- /**
- * Initialise the tree object when its first created.
- * @method initializer
- * @param {Object} config
- */
- initializer : function(config) {
- Y.log('Initialising navigation block tree', 'note', 'moodle-block_navigation');
-
- this.id = parseInt(config.id, 10);
-
- var node = Y.one('#inst'+config.id);
-
- // Can't find the block instance within the page
- if (node === null) {
- return;
- }
-
- // Delegate event to toggle expansion
- Y.delegate('click', this.toggleExpansion, node.one('.block_tree'), '.tree_item.branch', this);
- Y.delegate('actionkey', this.toggleExpansion, node.one('.block_tree'), '.tree_item.branch', this);
-
- // Gather the expandable branches ready for initialisation.
- var expansions = [];
- if (config.expansions) {
- expansions = config.expansions;
- } else if (window['navtreeexpansions'+config.id]) {
- expansions = window['navtreeexpansions'+config.id];
- }
- // Establish each expandable branch as a tree branch.
- for (var i in expansions) {
- var branch = new BRANCH({
- tree:this,
- branchobj:expansions[i],
- overrides : {
- expandable : true,
- children : [],
- haschildren : true
- }
- }).wire();
- M.block_navigation.expandablebranchcount++;
- this.branches[branch.get('id')] = branch;
- }
- // Create siteadmin branch.
- if (window.siteadminexpansion) {
- var siteadminbranch = new BRANCH({
- tree: this,
- branchobj: window.siteadminexpansion,
- overrides : {
- expandable : true,
- children : [],
- haschildren : true
- }
- }).wire();
- M.block_navigation.expandablebranchcount++;
- this.branches[siteadminbranch.get('id')] = siteadminbranch;
- // Remove link on site admin with JS to keep old UI.
- if (siteadminbranch.node) {
- var siteadminlinknode = siteadminbranch.node.get('childNodes').item(0);
- if (siteadminlinknode) {
- var siteadminnode = Y.Node.create('<span tabindex="0">'+siteadminlinknode.get('innerHTML')+'</span>');
- siteadminbranch.node.replaceChild(siteadminnode, siteadminlinknode);
- }
- }
- }
- if (M.block_navigation.expandablebranchcount > 0) {
- // Delegate some events to handle AJAX loading.
- Y.delegate('click', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this);
- Y.delegate('actionkey', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this);
- }
- },
- /**
- * Fire actions for a branch when an event occurs.
- * @method fire_branch_action
- * @param {EventFacade} event
- */
- fire_branch_action : function(event) {
- var id = event.currentTarget.getAttribute('id');
- var branch = this.branches[id];
- branch.ajaxLoad(event);
- },
- /**
- * This is a callback function responsible for expanding and collapsing the
- * branches of the tree. It is delegated to rather than multiple event handles.
- * @method toggleExpansion
- * @param {EventFacade} e
- * @return Boolean
- */
- toggleExpansion : function(e) {
- // First check if they managed to click on the li iteslf, then find the closest
- // LI ancestor and use that
-
- if (e.target.test('a') && (e.keyCode === 0 || e.keyCode === 13)) {
- // A link has been clicked (or keypress is 'enter') don't fire any more events just do the default.
- e.stopPropagation();
- return;
- }
-
- // Makes sure we can get to the LI containing the branch.
- var target = e.target;
- if (!target.test('li')) {
- target = target.ancestor('li');
- }
- if (!target) {
- return;
- }
-
- // Toggle expand/collapse providing its not a root level branch.
- if (!target.hasClass('depth_1')) {
- if (e.type === 'actionkey') {
- switch (e.action) {
- case 'expand' :
- target.removeClass('collapsed');
- target.set('aria-expanded', true);
- break;
- case 'collapse' :
- target.addClass('collapsed');
- target.set('aria-expanded', false);
- break;
- default :
- target.toggleClass('collapsed');
- target.set('aria-expanded', !target.hasClass('collapsed'));
- }
- e.halt();
- } else {
- target.toggleClass('collapsed');
- target.set('aria-expanded', !target.hasClass('collapsed'));
- }
- }
-
- // If the accordian feature has been enabled collapse all siblings.
- if (this.get('accordian')) {
- target.siblings('li').each(function(){
- if (this.get('id') !== target.get('id') && !this.hasClass('collapsed')) {
- this.addClass('collapsed');
- this.set('aria-expanded', false);
- }
- });
- }
-
- // If this block can dock tell the dock to resize if required and check
- // the width on the dock panel in case it is presently in use.
- if (this.get('candock') && M.core.dock.notifyBlockChange) {
- M.core.dock.notifyBlockChange(this.id);
- }
- return true;
-
- }
-};
-// The tree extends the YUI base foundation.
-Y.extend(TREE, Y.Base, TREE.prototype, {
- NAME : 'navigation-tree',
- ATTRS : {
- /**
- * True if the block can dock.
- * @attribute candock
- * @type Boolean
- */
- candock : {
- validator : Y.Lang.isBool,
- value : false
- },
- /**
- * If set to true nodes will be opened/closed in an accordian fashion.
- * @attribute accordian
- * @type Boolean
- */
- accordian : {
- validator : Y.Lang.isBool,
- value : false
- },
- /**
- * The nodes that get shown.
- * @attribute expansionlimit
- * @type Number
- */
- expansionlimit : {
- value : 0,
- setter : function(val) {
- val = parseInt(val, 10);
- if (val !== EXPANSIONLIMIT_EVERYTHING &&
- val !== EXPANSIONLIMIT_COURSE &&
- val !== EXPANSIONLIMIT_SECTION &&
- val !== EXPANSIONLIMIT_ACTIVITY) {
- val = EXPANSIONLIMIT_EVERYTHING;
- }
- return val;
- }
- },
- /**
- * The navigation tree block instance.
- *
- * @attribute instance
- * @default false
- * @type Number
- */
- instance : {
- value : false,
- setter : function(val) {
- return parseInt(val, 10);
- }
- }
- }
-});
-
-/**
- * The Branch class.
- *
- * This class is used to manage a tree branch, in particular its ability to load
- * its contents by AJAX.
- *
- * @namespace M.block_navigation
- * @class Branch
- * @constructor
- * @extends Base
- */
-var BRANCH = function() {
- BRANCH.superclass.constructor.apply(this, arguments);
-};
-BRANCH.prototype = {
- /**
- * The node for this branch (p)
- * @property node
- * @type Node
- * @protected
- */
- node : null,
- /**
- * Initialises the branch when it is first created.
- * @method initializer
- * @param {Object} config
- */
- initializer : function(config) {
- var i,
- children;
- if (config.branchobj !== null) {
- // Construct from the provided xml
- for (i in config.branchobj) {
- this.set(i, config.branchobj[i]);
- }
- children = this.get('children');
- this.set('haschildren', (children.length > 0));
- }
- if (config.overrides !== null) {
- // Construct from the provided xml
- for (i in config.overrides) {
- this.set(i, config.overrides[i]);
- }
- }
- // Get the node for this branch
- this.node = Y.one('#'+this.get('id'));
- var expansionlimit = this.get('tree').get('expansionlimit');
- var type = this.get('type');
- if (expansionlimit !== EXPANSIONLIMIT_EVERYTHING && type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) {
- this.set('expandable', false);
- this.set('haschildren', false);
- }
- },
- /**
- * Draws the branch within the tree.
- *
- * This function creates a DOM structure for the branch and then injects
- * it into the navigation tree at the correct point.
- *
- * It is important that this is kept in check with block_navigation_renderer::navigation_node as that produces
- * the same thing as this but on the php side.
- *
- * @method draw
- * @chainable
- * @param {Node} element
- * @return Branch
- */
- draw : function(element) {
-
- var isbranch = (this.get('expandable') || this.get('haschildren'));
- var branchli = Y.Node.create('<li></li>');
- var link = this.get('link');
- var branchp = Y.Node.create('<p class="tree_item"></p>').setAttribute('id', this.get('id'));
- var name;
- if (!link) {
- //add tab focus if not link (so still one focus per menu node).
- // it was suggested to have 2 foci. one for the node and one for the link in MDL-27428.
- branchp.setAttribute('tabindex', '0');
- }
- if (isbranch) {
- branchli.addClass('collapsed').addClass('contains_branch');
- branchli.set('aria-expanded', false);
- branchp.addClass('branch');
- }
-
- // Prepare the icon, should be an object representing a pix_icon
- var branchicon = false;
- var icon = this.get('icon');
- if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY || this.get('type') === NODETYPE.RESOURCE)) {
- branchicon = Y.Node.create('<img alt="" />');
- branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component));
- branchli.addClass('item_with_icon');
- branchp.addClass('hasicon');
- if (icon.alt) {
- branchicon.setAttribute('alt', icon.alt);
- }
- if (icon.title) {
- branchicon.setAttribute('title', icon.title);
- }
- if (icon.classes) {
- for (var i in icon.classes) {
- branchicon.addClass(icon.classes[i]);
- }
- }
- }
-
- if (!link) {
- var branchspan = Y.Node.create('<span></span>');
- if (branchicon) {
- branchspan.appendChild(branchicon);
- name = '<span class="item-content-wrap">' + this.get('name') + '</span>';
- } else {
- name = this.get('name');
- }
- branchspan.append(name);
- if (this.get('hidden')) {
- branchspan.addClass('dimmed_text');
- }
- branchp.appendChild(branchspan);
- } else {
- var branchlink = Y.Node.create('<a title="'+this.get('title')+'" href="'+link+'"></a>');
- if (branchicon) {
- branchlink.appendChild(branchicon);
- name = '<span class="item-content-wrap">' + this.get('name') + '</span>';
- } else {
- name = this.get('name');
- }
- branchlink.append(name);
- if (this.get('hidden')) {
- branchlink.addClass('dimmed');
- }
- branchp.appendChild(branchlink);
- }
-
- branchli.appendChild(branchp);
- element.appendChild(branchli);
- this.node = branchp;
- return this;
- },
- /**
- * Attaches required events to the branch structure.
- *
- * @chainable
- * @method wire
- * @return {BRANCH} This function is chainable, it always returns itself.
- */
- wire : function() {
- this.node = this.node || Y.one('#'+this.get('id'));
- if (!this.node) {
- return this;
- }
- if (this.get('expandable')) {
- this.node.setAttribute('data-expandable', '1');
- this.node.setAttribute('data-loaded', '0');
- }
- return this;
- },
- /**
- * Gets the UL element that children for this branch should be inserted into.
- * @method getChildrenUL
- * @return Node
- */
- getChildrenUL : function() {
- var ul = this.node.next('ul');
- if (!ul) {
- ul = Y.Node.create('<ul></ul>');
- this.node.ancestor().append(ul);
- }
- return ul;
- },
- /**
- * Load the content of the branch via AJAX.
- *
- * This function calls ajaxProcessResponse with the result of the AJAX
- * request made here.
- *
- * @method ajaxLoad
- * @param {EventFacade} e
- * @return Bool
- */
- ajaxLoad : function(e) {
- if (e.type === 'actionkey' && e.action !== 'enter') {
- e.halt();
- } else {
- e.stopPropagation();
- }
- if ((e.type === 'actionkey' && e.action === 'enter') || e.target.test('a')) {
- // No ajaxLoad for enter.
- this.node.setAttribute('data-expandable', '0');
- this.node.setAttribute('data-loaded', '1');
- return true;
- }
-
- if (this.node.hasClass('loadingbranch')) {
- // Already loading. Just skip.
- return true;
- }
-
- if (this.node.getAttribute('data-loaded') === '1') {
- // We've already loaded this stuff.
- return true;
- }
- Y.log('Loading navigation branch via AJAX: '+this.get('key'), 'note', 'moodle-block_navigation');
- this.node.addClass('loadingbranch');
-
- var params = {
- elementid : this.get('id'),
- id : this.get('key'),
- type : this.get('type'),
- sesskey : M.cfg.sesskey,
- instance : this.get('tree').get('instance')
- };
-
- var ajaxfile = '/lib/ajax/getnavbranch.php';
- // For siteadmin navigation get tree from getsiteadminbranch.php.
- if (this.get('type') === NODETYPE.SITEADMIN) {
- ajaxfile = '/lib/ajax/getsiteadminbranch.php';
- }
-
- Y.io(M.cfg.wwwroot + ajaxfile, {
- method:'POST',
- data: params,
- on: {
- complete: this.ajaxProcessResponse
- },
- context:this
- });
- return true;
- },
- /**
- * Processes an AJAX request to load the content of this branch through
- * AJAX.
- *
- * @method ajaxProcessResponse
- * @param {Int} tid The transaction id.
- * @param {Object} outcome
- * @return Boolean
- */
- ajaxProcessResponse : function(tid, outcome) {
- this.node.removeClass('loadingbranch');
- this.node.setAttribute('data-loaded', '1');
- try {
- var object = Y.JSON.parse(outcome.responseText);
- if (object.error) {
- Y.use('moodle-core-notification-ajaxexception', function () {
- return new M.core.ajaxException(object).show();
- });
- return false;
- }
- if (object.children && object.children.length > 0) {
- var coursecount = 0;
- for (var i in object.children) {
- if (typeof(object.children[i])==='object') {
- if (object.children[i].type === NODETYPE.COURSE) {
- coursecount++;
- }
- this.addChild(object.children[i]);
- }
- }
- if ((this.get('type') === NODETYPE.CATEGORY ||
- this.get('type') === NODETYPE.ROOTNODE ||
- this.get('type') === NODETYPE.MYCATEGORY)
- && coursecount >= M.block_navigation.courselimit) {
- this.addViewAllCoursesChild(this);
- }
- Y.log('AJAX loading complete.', 'note', 'moodle-block_navigation');
- // If this block can dock tell the dock to resize if required and check
- // the width on the dock panel in case it is presently in use.
- if (this.get('tree').get('candock') && M.core.dock.notifyBlockChange) {
- M.core.dock.notifyBlockChange(this.get('tree').id);
- }
- return true;
- }
- Y.log('AJAX loading complete but there were no children.', 'note', 'moodle-block_navigation');
- } catch (error) {
- if (outcome && outcome.status && outcome.status > 0) {
- // If we got here then there was an error parsing the result.
- Y.log('Error parsing AJAX response or adding branches to the navigation tree', 'error', 'moodle-block_navigation');
- Y.use('moodle-core-notification-exception', function () {
- return new M.core.exception(error).show();
- });
- }
-
- return false;
- }
- // The branch is empty so class it accordingly
- this.node.replaceClass('branch', 'emptybranch');
- return true;
- },
- /**
- * Turns the branch object passed to the method into a proper branch object
- * and then adds it as a child of this branch.
- *
- * @method addChild
- * @param {Object} branchobj
- * @return Boolean
- */
- addChild : function(branchobj) {
- // Make the new branch into an object
- var branch = new BRANCH({tree:this.get('tree'), branchobj:branchobj});
- if (branch.draw(this.getChildrenUL())) {
- this.get('tree').branches[branch.get('id')] = branch;
- branch.wire();
- var count = 0, i, children = branch.get('children');
- for (i in children) {
- // Add each branch to the tree
- if (children[i].type === NODETYPE.COURSE) {
- count++;
- }
- if (typeof(children[i]) === 'object') {
- branch.addChild(children[i]);
- }
- }
- if ((branch.get('type') === NODETYPE.CATEGORY || branch.get('type') === NODETYPE.MYCATEGORY)
- && count >= M.block_navigation.courselimit) {
- this.addViewAllCoursesChild(branch);
- }
- }
- return true;
- },
-
- /**
- * Add a link to view all courses in a category
- *
- * @method addViewAllCoursesChild
- * @param {BRANCH} branch
- */
- addViewAllCoursesChild: function(branch) {
- var url = null;
- if (branch.get('type') === NODETYPE.ROOTNODE) {
- if (branch.get('key') === 'mycourses') {
- url = M.cfg.wwwroot + '/my';
- } else {
- url = M.cfg.wwwroot + '/course/index.php';
- }
- } else {
- url = M.cfg.wwwroot+'/course/index.php?categoryid=' + branch.get('key');
- }
- branch.addChild({
- name : M.util.get_string('viewallcourses', 'moodle'),
- title : M.util.get_string('viewallcourses', 'moodle'),
- link : url,
- haschildren : false,
- icon : {'pix':"i/navigationitem",'component':'moodle'}
- });
- }
-};
-Y.extend(BRANCH, Y.Base, BRANCH.prototype, {
- NAME : 'navigation-branch',
- ATTRS : {
- /**
- * The Tree this branch belongs to.
- * @attribute tree
- * @type TREE
- * @required
- * @writeOnce
- */
- tree : {
- writeOnce : 'initOnly',
- validator : Y.Lang.isObject
- },
- /**
- * The name of this branch.
- * @attribute name
- * @type String
- */
- name : {
- value : '',
- validator : Y.Lang.isString,
- setter : function(val) {
- return val.replace(/\n/g, '<br />');
- }
- },
- /**
- * The title to use for this branch.
- * @attribute title
- * @type String
- */
- title : {
- value : '',
- validator : Y.Lang.isString
- },
- /**
- * The ID of this branch.
- * The ID and Type should always form a unique pair.
- * @attribute id
- * @type String
- */
- id : {
- value : '',
- validator : Y.Lang.isString,
- getter : function(val) {
- if (val === '') {
- val = 'expandable_branch_'+M.block_navigation.expandablebranchcount;
- M.block_navigation.expandablebranchcount++;
- }
- return val;
- }
- },
- /**
- * The key used to identify this branch easily if there is one.
- * @attribute key
- * @type String
- */
- key : {
- value : null
- },
- /**
- * The type of this branch.
- * @attribute type
- * @type Number
- */
- type : {
- value : null,
- setter : function(value) {
- return parseInt(value, 10);
- }
- },
- /**
- * The link to use for this branch.
- * @attribute link
- * @type String
- */
- link : {
- value : false
- },
- /**
- * The Icon to add when displaying this branch.
- * @attribute icon
- * @type Object
- */
- icon : {
- value : false,
- validator : Y.Lang.isObject
- },
- /**
- * True if this branch is expandable.
- * @attribute expandable
- * @type Boolean
- */
- expandable : {
- value : false,
- validator : Y.Lang.isBool
- },
- /**
- * True if this branch is hidden and should be displayed greyed out.
- * @attribute hidden
- * @type Boolean
- */
- hidden : {
- value : false,
- validator : Y.Lang.isBool
- },
- /**
- * True if this branch has any children.
- * @attribute haschildren
- * @type Boolean
- */
- haschildren : {
- value : false,
- validator : Y.Lang.isBool
- },
- /**
- * An array of other branches that appear as children of this branch.
- * @attribute children
- * @type Array
- */
- children : {
- value : [],
- validator : Y.Lang.isArray
- }
- }
-});
+++ /dev/null
-{
- "moodle-block_navigation-navigation": {
- "requires": [
- "base",
- "io-base",
- "node",
- "event-synthetic",
- "event-delegate",
- "json-parse"
- ]
- }
-}
--- /dev/null
+// 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/>.
+
+/**
+ * Load the settings block tree javscript
+ *
+ * @module block_navigation/navblock
+ * @package core
+ * @copyright 2015 John Okely <john@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/tree'], function($, Tree) {
+ return {
+ init: function(siteAdminNodeId) {
+ var adminTree = new Tree(".block_settings .block_tree");
+ if (siteAdminNodeId) {
+ var siteAdminNode = adminTree.treeRoot.find('#' + siteAdminNodeId);
+ var siteAdminLink = siteAdminNode.children('a').first();
+ siteAdminLink.replaceWith('<span tabindex="0">' + siteAdminLink.html() + '</span>');
+ }
+ }
+ };
+});
}
function get_required_javascript() {
+ global $PAGE;
+ $adminnodeid = null;
+ $adminnode = $PAGE->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN);
+ if (!empty($adminnode)) {
+ $adminnodeid = $adminnode->id;
+ }
parent::get_required_javascript();
$arguments = array(
'id' => $this->instance->id,
'instance' => $this->instance->id,
'candock' => $this->instance_can_be_docked()
);
- $this->page->requires->yui_module('moodle-block_navigation-navigation', 'M.block_navigation.init_add_tree', array($arguments));
+ $this->page->requires->js_call_amd('block_settings/settingsblock', 'init', array($adminnodeid));
}
/**
$count++;
}
}
- $content = $this->navigation_node($navigation, array('class'=>'block_tree list'));
+ $navigationattrs = array(
+ 'class' => 'block_tree list',
+ 'role' => 'tree',
+ 'data-ajax-loader' => 'block_navigation/site_admin_loader');
+ $content = $this->navigation_node($navigation, $navigationattrs);
if (isset($navigation->id) && !is_numeric($navigation->id) && !empty($content)) {
$content = $this->output->box($content, 'block_tree_box', $navigation->id);
}
return $content;
}
- protected function navigation_node(navigation_node $node, $attrs=array()) {
+ /**
+ * Build the navigation node.
+ *
+ * @param navigation_node $node the navigation node object.
+ * @param array $attrs list of attributes.
+ * @param int $depth the depth, default to 1.
+ * @return string the navigation node code.
+ */
+ protected function navigation_node(navigation_node $node, $attrs=array(), $depth = 1) {
$items = $node->children;
// exit if empty, we don't want an empty ul element
// array of nested li elements
$lis = array();
+ $number = 0;
foreach ($items as $item) {
+ $number++;
if (!$item->display) {
continue;
}
// this applies to the li item which contains all child lists too
$liclasses = array($item->get_css_type());
$liexpandable = array();
- if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count()==0 && $item->nodetype==navigation_node::NODETYPE_BRANCH)) {
- $liclasses[] = 'collapsed';
- }
if ($isbranch) {
$liclasses[] = 'contains_branch';
- $liexpandable = array('aria-expanded' => in_array('collapsed', $liclasses) ? "false" : "true");
+ if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count() == 0
+ && $item->nodetype == navigation_node::NODETYPE_BRANCH)) {
+ $liexpandable = array('aria-expanded' => 'false');
+ } else {
+ $liexpandable = array('aria-expanded' => 'true');
+ }
+ if ($item->requiresajaxloading) {
+ $liexpandable['data-requires-ajax'] = 'true';
+ $liexpandable['data-loaded'] = 'false';
+ }
+
} else if ($hasicon) {
$liclasses[] = 'item_with_icon';
}
if ($item->isactive === true) {
$liclasses[] = 'current_branch';
}
- $liattr = array('class' => join(' ',$liclasses)) + $liexpandable;
+ $nodetextid = 'label_' . $depth . '_' . $number;
+ $liattr = array('class' => join(' ', $liclasses), 'tabindex' => '-1', 'role' => 'treeitem') + $liexpandable;
// class attribute on the div item which only contains the item content
$divclasses = array('tree_item');
if ($isbranch) {
if (!empty($item->id)) {
$divattr['id'] = $item->id;
}
- $content = html_writer::tag('p', $content, $divattr) . $this->navigation_node($item);
+ $content = html_writer::tag('p', $content, $divattr) . $this->navigation_node($item, array(), $depth + 1);
if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
$content = html_writer::empty_tag('hr') . $content;
}
+ $liattr['aria-labelledby'] = $nodetextid;
$content = html_writer::tag('li', $content, $liattr);
$lis[] = $content;
}
if (count($lis)) {
+ if (empty($attrs['role'])) {
+ $attrs['role'] = 'group';
+ }
return html_writer::tag('ul', implode("\n", $lis), $attrs);
} else {
return '';
.block_settings .block_tree .tree_item {padding-left: 21px;margin:3px 0px;text-align:left;}
.block_settings .block_tree .tree_item.branch {background-image: url([[pix:t/expanded]]);background-position: 0 10%;background-repeat: no-repeat;}
+.block_settings .block_tree .loading .tree_item.branch {background-image: url('[[pix:i/loading_small]]');}
.block_settings .block_tree .active_tree_node {font-weight:bold;}
.jsenabled .block_settings .block_tree .tree_item.branch {cursor:pointer;}
-.jsenabled .block_settings .block_tree .tree_item.emptybranch {background-image: url([[pix:t/collapsed_empty]]);background-position: 0 10%;background-repeat: no-repeat;}
-.jsenabled .block_settings .block_tree .collapsed ul {display: none;}
-.jsenabled .block_settings .block_tree .collapsed .tree_item.branch {background-image: url([[pix:t/collapsed]]);}
+.jsenabled .block_settings .block_tree .emptybranch .tree_item,
+.jsenabled .block_settings .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
+ background-image: url([[pix:t/collapsed_empty]]);background-position: 0 10%;background-repeat: no-repeat;
+}
+.jsenabled .block_settings .block_tree [aria-expanded="false"] ul {display: none;}
+.jsenabled .block_settings .block_tree [aria-expanded="false"] .tree_item.branch {background-image: url([[pix:t/collapsed]]);}
+.jsenabled .block_settings .block_tree [aria-expanded="false"].loading .tree_item.branch {background-image: url('[[pix:i/loading_small]]');}
/** Internet explorer specific rules **/
.ie6 .block_settings .block_tree .tree_item {width:100%;}
.dir-rtl .block_settings .block_tree li.item_with_icon > p img { right: 0; left: auto;}
.jsenabled .block_settings .block_tree .tree_item.branch.loadingbranch {background-image:url([[pix:i/loading_small]]);}
-.jsenabled.dir-rtl .block_settings .block_tree .tree_item.emptybranch {background-image: url([[pix:t/collapsed_empty_rtl]]);background-position: center right;}
-.jsenabled.dir-rtl .block_settings .block_tree .collapsed .tree_item.branch {background-image: url([[pix:t/collapsed_rtl]]);}
+.jsenabled.dir-rtl .block_settings .block_tree .emptybranch .tree_item,
+.jsenabled.dir-rtl .block_settings .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
+ background-image: url([[pix:t/collapsed_empty_rtl]]);background-position: center right;
+}
+.jsenabled.dir-rtl .block_settings .block_tree [aria-expanded="false"] .tree_item.branch {background-image: url([[pix:t/collapsed_rtl]]);}
# The first block matching the pattern should be top-left block
And I should see "Comments" in the "//*[@id='region-pre' or @id='block-region-side-pre']/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]" "xpath_element"
- Scenario: Blocks on the dashboard page cannot have roles assigned to them
+ Scenario: Blocks on the dashboard page can have roles assigned to them
Given I log in as "manager1"
And I click on "Dashboard" "link" in the "Navigation" "block"
When I press "Customise this page"
- Then I should not see "Assign roles in Navigation block"
+ Then I should see "Assign roles in Navigation block"
Scenario: Blocks on courses can have roles assigned to them
Given I log in as "teacher1"
This files describes API changes in /blocks/* - activity modules,
information provided here is intended especially for developers.
+=== 3.1 ===
+
+* The collapsed class was removed from the navigation block to make it compatible with aria.
+* New aria attributes were added on the navigation block [aria-expanded="false"].
+* The tree JS handling were moved from YUI to AMD module (Jquery).
+
=== 2.9 ===
* The obsolete method preferred_width() was removed (it was not doing anything)
$editicon = $OUTPUT->action_icon($editurl, new pix_icon('t/edit', get_string('editexternalblog', 'blog')));
$deletelink = new moodle_url('/blog/external_blogs.php', array('delete' => $blog->id, 'sesskey' => sesskey()));
- $deleteicon = $OUTPUT->action_icon($deletelink, new pix_icon('t/delete', get_string('deleteexternalblog', 'blog')));
+ $action = new confirm_action(get_string('externalblogdeleteconfirm', 'blog'));
+ $deleteicon = $OUTPUT->action_icon($deletelink, new pix_icon('t/delete', get_string('deleteexternalblog', 'blog')),
+ $action);
$table->data[] = new html_table_row(array($blog->name,
$blog->url,
userdate($blog->timefetched),
$validicon,
- $editicon . ' '. $deleteicon));
+ $editicon . $deleteicon));
}
echo html_writer::table($table);
}
$mform->addElement('text', 'criteria_grade_value', get_string('graderequired', 'completion'));
$mform->disabledIf('criteria_grade_value', 'criteria_grade');
$mform->setType('criteria_grade_value', PARAM_RAW); // Uses unformat_float.
- $mform->setDefault('criteria_grade_value', format_float($data));
+ // Grades are stored in Moodle with 5 decimal points, make sure we do not accidentally round them
+ // when setting the form value.
+ $mform->setDefault('criteria_grade_value', format_float($data, 5));
if ($this->id) {
$mform->setDefault('criteria_grade', 1);
- $mform->setDefault('criteria_grade_value', format_float($this->gradepass));
+ $mform->setDefault('criteria_grade_value', format_float($this->gradepass, 5));
}
}
* @return string
*/
public function get_title_detailed() {
- $graderequired = round($this->gradepass, 2).'%';
+ global $CFG;
+ require_once($CFG->libdir . '/gradelib.php');
+ $decimalpoints = grade_get_setting($this->course, 'decimalpoints', $CFG->grade_decimalpoints);
+ $graderequired = format_float($this->gradepass, $decimalpoints);
return get_string('gradexrequired', 'completion', $graderequired);
}
* @return string
*/
public function get_status($completion) {
+ global $CFG;
+ require_once($CFG->libdir . '/gradelib.php');
+ $decimalpoints = grade_get_setting($this->course, 'decimalpoints', $CFG->grade_decimalpoints);
+
$grade = $this->get_grade($completion);
$graderequired = $this->get_title_detailed();
if ($grade) {
- $grade = round($grade, 2).'%';
+ $grade = format_float($grade, $decimalpoints);
} else {
$grade = get_string('nograde');
}
* type, criteria, requirement, status
*/
public function get_details($completion) {
+ global $CFG;
+ require_once($CFG->libdir . '/gradelib.php');
+ $decimalpoints = grade_get_setting($this->course, 'decimalpoints', $CFG->grade_decimalpoints);
+
$details = array();
$details['type'] = get_string('coursegrade', 'completion');
$details['criteria'] = get_string('graderequired', 'completion');
- $details['requirement'] = round($this->gradepass, 2).'%';
+ $details['requirement'] = format_float($this->gradepass, $decimalpoints);
$details['status'] = '';
- $grade = round($this->get_grade($completion), 2);
+ $grade = format_float($this->get_grade($completion), $decimalpoints);
if ($grade) {
- $details['status'] = $grade.'%';
+ $details['status'] = $grade;
}
return $details;
* @Given /^I go to the current course activity completion report$/
*/
public function go_to_the_current_course_activity_completion_report() {
+ $completionnode = get_string('pluginname', 'report_progress');
+ $reportsnode = get_string('courseadministration') . ' > ' . get_string('reports');
- $steps = array();
-
- // Expand reports node if we can't see the link.
- try {
- $this->find('xpath', "//div[@id='settingsnav']" .
- "/descendant::li" .
- "/descendant::li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed '))]" .
- "/descendant::p[contains(., '" . get_string('pluginname', 'report_progress') . "')]");
- } catch (ElementNotFoundException $e) {
- $steps[] = new Given('I expand "' . get_string('reports') . '" node');
- }
-
- $steps[] = new Given('I follow "' . get_string('pluginname', 'report_progress') . '"');
-
- return $steps;
+ return new Given('I navigate to "' . $completionnode . '" node in "' . $reportsnode . '"');
}
/**
throw new DriverException('Section edit menu not available when Javascript is disabled');
}
+ // Wait for section to be available, before clicking on the menu.
+ $this->i_wait_until_section_is_available($sectionnumber);
+
// If it is already opened we do nothing.
$xpath = $this->section_exists($sectionnumber);
$xpath .= "/descendant::div[contains(@class, 'section-actions')]/descendant::a[contains(@class, 'textmenu')]";
*/
public function i_show_section($sectionnumber) {
$showlink = $this->show_section_icon_exists($sectionnumber);
+
+ // Ensure section edit menu is open before interacting with it.
+ if ($this->running_javascript()) {
+ $this->i_open_section_edit_menu($sectionnumber);
+ }
$showlink->click();
if ($this->running_javascript()) {
*/
public function i_hide_section($sectionnumber) {
$hidelink = $this->hide_section_icon_exists($sectionnumber);
+
+ // Ensure section edit menu is open before interacting with it.
+ if ($this->running_javascript()) {
+ $this->i_open_section_edit_menu($sectionnumber);
+ }
$hidelink->click();
if ($this->running_javascript()) {
// We need to know the course format as the text strings depends on them.
$courseformat = $this->get_course_format();
- // If javascript is on, link is inside a menu.
- if ($this->running_javascript()) {
- $this->i_open_section_edit_menu($sectionnumber);
- }
-
// Checking the show button alt text and show icon.
$showtext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('showfromothers', $courseformat));
$linkxpath = $xpath . "/descendant::a[@title=$showtext]";
// We need to know the course format as the text strings depends on them.
$courseformat = $this->get_course_format();
- // If javascript is on, link is inside a menu.
- if ($this->running_javascript()) {
- $this->i_open_section_edit_menu($sectionnumber);
- }
-
// Checking the hide button alt text and hide icon.
$hidetext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('hidefromothers', $courseformat));
$linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
$string['cannotdownloadcomponents'] = 'לא ניתן להוריד רכיבים.';
$string['cannotdownloadzipfile'] = 'לא ניתן להוריד קובץ ZIP.';
$string['cannotfindcomponent'] = 'הרכיב לא נמצא.';
-$string['cannotsavemd5file'] = 'לא ניתן לשמור קובץ md5.';
-$string['cannotsavezipfile'] = 'לא ניתן לשמור קובץ ZIP.';
+$string['cannotsavemd5file'] = 'לא ניתן לשמור קובץ md5';
+$string['cannotsavezipfile'] = 'לא ניתן לשמור קובץ ZIP';
$string['cannotunzipfile'] = 'לא ניתן לפתוח את קובץ ה-ZIP.';
$string['componentisuptodate'] = 'הרכיב מעודכן.';
$string['downloadedfilecheckfailed'] = 'נכשלה בדיקת הקובץ המורד.';
<a href="http://www.opensource.org/docs/definition_plain.html">קוד פתוח</a>
והיא מופצת תחת רשיון
<a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>';
-$string['welcomep60'] = '×\94×¢×\9e×\95×\93×\99×\9d ×\94×\91×\90×\99×\9d ×\99×\95×\91×\99×\9c×\95 ×\90×\95ת×\9a ×\91צ×\95ר×\94 פש×\95×\98×\94 ×\93ר×\9a ×\9b×\9e×\94 צע×\93×\99×\9d ×\9c×¢×\99צ×\95×\91 ×\94×\92×\93ר×\95ת <strong>Moodle</strong> ×\91×\9e×\97ש×\91×\9a.
-ת×\95×\9b×\9c ×\9c×\90שר ×\90ת ×\94×\92×\93ר×\95ת ×\91ר×\99רת ×\94×\9e×\97×\93×\9c ×\90×\95, ×\91×\90פשר×\95ת×\9a, לשנותם לפי צרכיך.';
+$string['welcomep60'] = '×\94×¢×\9e×\95×\93×\99×\9d ×\94×\91×\90×\99×\9d ×\99×\95×\91×\99×\9c×\95 ×\90×\95ת×\9a ×\91צ×\95ר×\94 פש×\95×\98×\94 ×\93ר×\9a ×\9b×\9e×\94 צע×\93×\99×\9d ×\9c×§×\91×\99עת ×\94×\92×\93ר×\95ת <strong>Moodle</strong> ×\91שרת.
+× ×\99ת×\9f ×\9c×\90שר ×\94×\92×\93ר×\95ת ×\91ררת־×\94×\9e×\97×\93×\9c ×\90×\95 לשנותם לפי צרכיך.';
$string['welcomep70'] = 'הקש על לחצן ה"המשך" למטה כדי להמשיך עם הגדרת ה-<strong>Moodle</strong>';
$string['wwwroot'] = 'כתובת האתר';
--- /dev/null
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thisdirection'] = 'ltr';
+$string['thislanguage'] = 'ଓଡ଼ିଆ';
$string['installation'] = 'Instalare';
$string['paths'] = 'Căi';
$string['pathshead'] = 'Confirmare căi';
+$string['pathssubdirroot'] = '<p>Calea completă către directorul care conține codul Moodle .</p>';
+$string['pathsunsecuredataroot'] = 'Locația dataroot nu este sigură';
+$string['pathswrongadmindir'] = 'Directorul admin nu există';
$string['phpextension'] = 'extensie PHP {$a}';
$string['phpversion'] = 'Versiune PHP';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
defined('MOODLE_INTERNAL') || die();
$string['language'] = 'ภาษาที่ใช้ในเว็บ';
+$string['moodlelogo'] = 'โลโก้ Moodle';
$string['next'] = 'ต่อไป';
$string['previous'] = 'หน้าก่อน';
+$string['reload'] = 'โหลดใหม่';
--- /dev/null
+// 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/>.
+
+/**
+ * Implement an accessible aria tree widget, from a nested unordered list.
+ * Based on http://oaa-accessibility.org/example/41/.
+ *
+ * @module tool_lp/tree
+ * @package core
+ * @copyright 2015 Damyon Wiese <damyon@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery'], function($) {
+ // Private variables and functions.
+ var SELECTORS = {
+ ITEM: '[role=treeitem]',
+ GROUP: '[role=treeitem]:has([role=group]), [role=treeitem][data-requires-ajax=true]',
+ CLOSED_GROUP: '[role=treeitem]:has([role=group])[aria-expanded=false], [role=treeitem]' +
+ '[data-requires-ajax=true][aria-expanded=false]',
+ FIRST_ITEM: '[role=treeitem]:first',
+ VISIBLE_ITEM: '[role=treeitem]:visible',
+ UNLOADED_AJAX_ITEM: '[role=treeitem][data-requires-ajax=true][data-loaded=false][aria-expanded=true]'
+ };
+
+ /**
+ * Constructor.
+ *
+ * @param {String} selector
+ * @param {function} selectCallback Called when the active node is changed.
+ */
+ var Tree = function(selector, selectCallback) {
+ this.treeRoot = $(selector);
+
+ this.treeRoot.data('activeItem', null);
+ this.selectCallback = selectCallback;
+ this.keys = {
+ tab: 9,
+ enter: 13,
+ space: 32,
+ pageup: 33,
+ pagedown: 34,
+ end: 35,
+ home: 36,
+ left: 37,
+ up: 38,
+ right: 39,
+ down: 40,
+ asterisk: 106
+ };
+
+ // Apply the standard default initialisation for all nodes, starting with the tree root.
+ this.initialiseNodes(this.treeRoot);
+ // Make the first item the active item for the tree so that it is added to the tab order.
+ this.setActiveItem(this.treeRoot.find(SELECTORS.FIRST_ITEM));
+ // Create the cache of the visible items.
+ this.refreshVisibleItemsCache();
+ // Create the event handlers for the tree.
+ this.bindEventHandlers();
+ };
+
+ /**
+ * Find all visible tree items and save a cache of them on the tree object.
+ *
+ * @method refreshVisibleItemsCache
+ */
+ Tree.prototype.refreshVisibleItemsCache = function() {
+ this.treeRoot.data('visibleItems', this.treeRoot.find(SELECTORS.VISIBLE_ITEM));
+ };
+
+ /**
+ * Get all visible tree items.
+ *
+ * @method getVisibleItems
+ */
+ Tree.prototype.getVisibleItems = function() {
+ return this.treeRoot.data('visibleItems');
+ };
+
+ /**
+ * Mark the given item as active within the tree and fire the callback for when the active item is set.
+ *
+ * @method setActiveItem
+ * @param {object} item jquery object representing an item on the tree.
+ */
+ Tree.prototype.setActiveItem = function(item) {
+ var currentActive = this.treeRoot.data('activeItem');
+ if (item === currentActive) {
+ return;
+ }
+
+ // Remove previous active from tab order.
+ if (currentActive) {
+ currentActive.attr('tabindex', '-1');
+ currentActive.attr('aria-selected', 'false');
+ }
+ item.attr('tabindex', '0');
+ item.attr('aria-selected', 'true');
+
+ // Set the new active item.
+ this.treeRoot.data('activeItem', item);
+
+ if (typeof this.selectCallback === 'function') {
+ this.selectCallback(item);
+ }
+ };
+
+ /**
+ * Determines if the given item is a group item (contains child tree items) in the tree.
+ *
+ * @method isGroupItem
+ * @param {object} item jquery object representing an item on the tree.
+ * @returns {bool}
+ */
+ Tree.prototype.isGroupItem = function(item) {
+ return item.is(SELECTORS.GROUP);
+ };
+
+ /**
+ * Determines if the given group item (contains child tree items) is collapsed.
+ *
+ * @method isGroupCollapsed
+ * @param {object} item jquery object representing a group item on the tree.
+ * @returns {bool}
+ */
+ Tree.prototype.isGroupCollapsed = function(item) {
+ return item.attr('aria-expanded') === 'false';
+ };
+
+ /**
+ * Determines if the given group item (contains child tree items) can be collapsed.
+ *
+ * @method isGroupCollapsible
+ * @param {object} item jquery object representing a group item on the tree.
+ * @returns {bool}
+ */
+ Tree.prototype.isGroupCollapsible = function(item) {
+ return item.attr('data-collapsible') !== 'false';
+ };
+
+ /**
+ * Performs the tree initialisation for all child items from the given node,
+ * such as removing everything from the tab order and setting aria selected
+ * on items.
+ *
+ * @method initialiseNodes
+ * @param {object} node jquery object representing a node.
+ */
+ Tree.prototype.initialiseNodes = function(node) {
+ this.removeAllFromTabOrder(node);
+ this.setAriaSelectedFalseOnItems(node);
+
+ // Get all ajax nodes that have been rendered as expanded but haven't loaded the child items yet.
+ var thisTree = this;
+ node.find(SELECTORS.UNLOADED_AJAX_ITEM).each(function() {
+ var unloadedNode = $(this);
+ // Collapse and then expand to trigger the ajax loading.
+ thisTree.collapseGroup(unloadedNode);
+ thisTree.expandGroup(unloadedNode);
+ });
+ };
+
+ /**
+ * Removes all child DOM elements of the given node from the tab order.
+ *
+ * @method removeAllFromTabOrder
+ * @param {object} node jquery object representing a node.
+ */
+ Tree.prototype.removeAllFromTabOrder = function(node) {
+ node.find('*').attr('tabindex', '-1');
+ };
+
+ /**
+ * Find all child tree items from the given node and set the aria selected attribute to false.
+ *
+ * @method setAriaSelectedFalseOnItems
+ * @param {object} node jquery object representing a node.
+ */
+ Tree.prototype.setAriaSelectedFalseOnItems = function(node) {
+ node.find(SELECTORS.ITEM).attr('aria-selected', 'false');
+ };
+
+ /**
+ * Expand all group nodes within the tree.
+ *
+ * @method expandAllGroups
+ */
+ Tree.prototype.expandAllGroups = function() {
+ this.expandAllChildGroups(this.treeRoot);
+ };
+
+ /**
+ * Find all child group nodes from the given node and expand them.
+ *
+ * @method expandAllChildGroups
+ * @param {object} node jquery object representing a node.
+ */
+ Tree.prototype.expandAllChildGroups = function(node) {
+ var thisTree = this;
+
+ node.find(SELECTORS.CLOSED_GROUP).each(function() {
+ var childNode = $(this);
+ thisTree.expandGroup(childNode).done(function() {
+ thisTree.expandAllChildGroups(childNode);
+ });
+ });
+ };
+
+ /**
+ * Expand a collapsed group.
+ *
+ * Handles expanding nodes that are ajax loaded (marked with a data-requires-ajax attribute).
+ *
+ * @method expandGroup
+ * @param {Object} item is the jquery id of the parent item of the group.
+ * @return {Object} a promise that is resolved when the group has been expanded.
+ */
+ Tree.prototype.expandGroup = function(item) {
+ var promise = $.Deferred();
+ // Ignore nodes that are explicitly maked as not expandable or are already expanded.
+ if (item.attr('data-expandable') !== 'false' && this.isGroupCollapsed(item)) {
+ // If this node requires ajax load and we haven't already loaded it.
+ if (item.attr('data-requires-ajax') === 'true' && item.attr('data-loaded') !== 'true') {
+ item.attr('data-loaded', false);
+ // Get the closes ajax loading module specificed in the tree.
+ var moduleName = item.closest('[data-ajax-loader]').attr('data-ajax-loader');
+ var thisTree = this;
+ // Flag this node as loading.
+ item.addClass('loading');
+ // Require the ajax module (must be AMD) and try to load the items.
+ require([moduleName], function(loader) {
+ // All ajax module must implement a "load" method.
+ loader.load(item).done(function() {
+ item.attr('data-loaded', true);
+
+ // Set defaults on the newly constructed part of the tree.
+ thisTree.initialiseNodes(item);
+ thisTree.finishExpandingGroup(item);
+ // Make sure no child elements of the item we just loaded are tabbable.
+ item.removeClass('loading');
+ promise.resolve();
+ });
+ });
+ } else {
+ this.finishExpandingGroup(item);
+ promise.resolve();
+ }
+ } else {
+ promise.resolve();
+ }
+ return promise;
+ };
+
+ /**
+ * Perform the necessary DOM changes to display a group item.
+ *
+ * @method finishExpandingGroup
+ * @param {Object} item is the jquery id of the parent item of the group.
+ */
+ Tree.prototype.finishExpandingGroup = function(item) {
+ // Find the first child node.
+ var group = item.children(SELECTORS.GROUP);
+
+ // Expand the group.
+ group.show().attr('aria-hidden', 'false');
+
+ item.attr('aria-expanded', 'true');
+
+ // Update the list of visible items.
+ this.refreshVisibleItemsCache();
+ };
+
+ /**
+ * Collapse an expanded group.
+ *
+ * @method collapseGroup
+ * @param {Object} item is the jquery id of the parent item of the group.
+ */
+ Tree.prototype.collapseGroup = function(item) {
+ // If the item is not collapsible or already collapsed then do nothing.
+ if (!this.isGroupCollapsible(item) || this.isGroupCollapsed(item)) {
+ return;
+ }
+
+ // Get and collapse the group.
+ var group = item.children(SELECTORS.GROUP);
+ group.hide().attr('aria-hidden', 'true');
+ item.attr('aria-expanded', 'false');
+
+ // Update the list of visible items.
+ this.refreshVisibleItemsCache();
+ };
+
+ /**
+ * Expand or collapse a group.
+ *
+ * @method toggleGroup
+ * @param {Object} item is the jquery id of the parent item of the group.
+ */
+ Tree.prototype.toggleGroup = function(item) {
+ if (item.attr('aria-expanded') === 'true') {
+ this.collapseGroup(item);
+ } else {
+ this.expandGroup(item);
+ }
+ };
+
+ /**
+ * Handle a key down event - ie navigate the tree.
+ *
+ * @method handleKeyDown
+ * @param {Object} item is the jquery id of the parent item of the group.
+ * @param {Event} e The event.
+ */
+ Tree.prototype.handleKeyDown = function(item, e) {
+ var currentIndex = this.getVisibleItems().index(item);
+
+ if ((e.altKey || e.ctrlKey) || (e.shiftKey && e.keyCode != this.keys.tab)) {
+ // Do nothing.
+ return true;
+ }
+
+ switch (e.keyCode) {
+ case this.keys.home: {
+ // Jump to first item in tree.
+ this.getVisibleItems().first().focus();
+
+ e.stopPropagation();
+ return false;
+ }
+ case this.keys.end: {
+ // Jump to last visible item.
+ this.getVisibleItems().last().focus();
+
+ e.stopPropagation();
+ return false;
+ }
+ case this.keys.enter: {
+ var links = item.children().not(SELECTORS.GROUP).children('a');
+ if (links.length) {
+ window.location.href = links.first().attr('href');
+ } else if (this.isGroupItem(item)) {
+ this.toggleGroup(item, true);
+ }
+
+ e.stopPropagation();
+ return false;
+ }
+ case this.keys.space: {
+ if (this.isGroupItem(item)) {
+ this.toggleGroup(item, true);
+ }
+
+ e.stopPropagation();
+ return false;
+ }
+ case this.keys.left: {
+ var focusParent = function(tree) {
+ // Get the immediate visible parent group item that contains this element.
+ var parentGroup = tree.getVisibleItems().filter(SELECTORS.GROUP).has(item).last();
+ parentGroup.focus();
+ };
+
+ // If this is a goup item then collapse it and focus the parent group
+ // in accordance with the aria spec.
+ if (this.isGroupItem(item)) {
+ if (this.isGroupCollapsed(item)) {
+ focusParent(this);
+ } else {
+ this.collapseGroup(item);
+ }
+ } else {
+ focusParent(this);
+ }
+
+ e.stopPropagation();
+ return false;
+ }
+ case this.keys.right: {
+ // If this is a group item then expand it and focus the first child item
+ // in accordance with the aria spec.
+ if (this.isGroupItem(item)) {
+ if (this.isGroupCollapsed(item)) {
+ this.expandGroup(item);
+ } else {
+ // Move to the first item in the child group.
+ item.find(SELECTORS.ITEM).first().focus();
+ }
+ }
+
+ e.stopPropagation();
+ return false;
+ }
+ case this.keys.up: {
+
+ if (currentIndex > 0) {
+ var prev = this.getVisibleItems().eq(currentIndex - 1);
+
+ prev.focus();
+ }
+
+ e.stopPropagation();
+ return false;
+ }
+ case this.keys.down: {
+
+ if (currentIndex < this.getVisibleItems().length - 1) {
+ var next = this.getVisibleItems().eq(currentIndex + 1);
+
+ next.focus();
+ }
+
+ e.stopPropagation();
+ return false;
+ }
+ case this.keys.asterisk: {
+ // Expand all groups.
+ this.expandAllGroups();
+ e.stopPropagation();
+ return false;
+ }
+ }
+ return true;
+ };
+
+ /**
+ * Handle a click (select).
+ *
+ * @method handleClick
+ * @param {Object} item The jquery id of the parent item of the group.
+ * @param {Event} e The event.
+ */
+ Tree.prototype.handleClick = function(item, e) {
+
+ if (e.altKey || e.ctrlKey || e.shiftKey) {
+ // Do nothing.
+ return true;
+ }
+
+ // Update the active item.
+ item.focus();
+
+ // If the item is a group node.
+ if (this.isGroupItem(item)) {
+ this.toggleGroup(item);
+ }
+
+ e.stopPropagation();
+ return true;
+ };
+
+ /**
+ * Handle a focus event.
+ *
+ * @method handleFocus
+ * @param {Object} item The jquery id of the parent item of the group.
+ * @param {Event} e The event.
+ */
+ Tree.prototype.handleFocus = function(item, e) {
+
+ this.setActiveItem(item);
+
+ e.stopPropagation();
+ return true;
+ };
+
+ /**
+ * Bind the event listeners we require.
+ *
+ * @method bindEventHandlers
+ */
+ Tree.prototype.bindEventHandlers = function() {
+ var thisObj = this;
+
+ // Bind event handlers to the tree items. Use event delegates to allow
+ // for dynamically loaded parts of the tree.
+ this.treeRoot.on({
+ click: function(e) { return thisObj.handleClick($(this), e); },
+ keydown: function(e) { return thisObj.handleKeyDown($(this), e); },
+ focus: function(e) { return thisObj.handleFocus($(this), e); },
+ }, SELECTORS.ITEM);
+ };
+
+ return /** @alias module:tool_lp/tree */ Tree;
+});
}
// Display either "Assign roles" or "Permissions" or "Change permissions" icon (whichever first is available).
- if ($this->page->pagetype != 'my-index') {
- $rolesurl = null;
-
- if (get_assignable_roles($block->context, ROLENAME_SHORT)) {
- $rolesurl = new moodle_url('/admin/roles/assign.php', array('contextid' => $block->context->id));
- $str = new lang_string('assignrolesinblock', 'block', $blocktitle);
- $icon = 'i/assignroles';
- } else if (has_capability('moodle/role:review', $block->context) or get_overridable_roles($block->context)) {
- $rolesurl = new moodle_url('/admin/roles/permissions.php', array('contextid' => $block->context->id));
- $str = get_string('permissions', 'role');
- $icon = 'i/permissions';
- } else if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override', 'moodle/role:assign'), $block->context)) {
- $rolesurl = new moodle_url('/admin/roles/check.php', array('contextid' => $block->context->id));
- $str = get_string('checkpermissions', 'role');
- $icon = 'i/checkpermissions';
- }
+ $rolesurl = null;
+
+ if (get_assignable_roles($block->context, ROLENAME_SHORT)) {
+ $rolesurl = new moodle_url('/admin/roles/assign.php', array('contextid' => $block->context->id));
+ $str = new lang_string('assignrolesinblock', 'block', $blocktitle);
+ $icon = 'i/assignroles';
+ } else if (has_capability('moodle/role:review', $block->context) or get_overridable_roles($block->context)) {
+ $rolesurl = new moodle_url('/admin/roles/permissions.php', array('contextid' => $block->context->id));
+ $str = get_string('permissions', 'role');
+ $icon = 'i/permissions';
+ } else if (has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override', 'moodle/role:assign'), $block->context)) {
+ $rolesurl = new moodle_url('/admin/roles/check.php', array('contextid' => $block->context->id));
+ $str = get_string('checkpermissions', 'role');
+ $icon = 'i/checkpermissions';
+ }
+
+ if ($rolesurl) {
+ // TODO: please note it is sloppy to pass urls through page parameters!!
+ // it is shortened because some web servers (e.g. IIS by default) give
+ // a 'security' error if you try to pass a full URL as a GET parameter in another URL.
+ $return = $this->page->url->out(false);
+ $return = str_replace($CFG->wwwroot . '/', '', $return);
+ $rolesurl->param('returnurl', $return);
- if ($rolesurl) {
- //TODO: please note it is sloppy to pass urls through page parameters!!
- // it is shortened because some web servers (e.g. IIS by default) give
- // a 'security' error if you try to pass a full URL as a GET parameter in another URL.
- $return = $this->page->url->out(false);
- $return = str_replace($CFG->wwwroot . '/', '', $return);
- $rolesurl->param('returnurl', $return);
-
- $controls[] = new action_menu_link_secondary(
- $rolesurl,
- new pix_icon($icon, $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
- $str,
- array('class' => 'editing_roles')
- );
- }
+ $controls[] = new action_menu_link_secondary(
+ $rolesurl,
+ new pix_icon($icon, $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+ $str,
+ array('class' => 'editing_roles')
+ );
}
if ($this->user_can_delete_block($block)) {
if (isguestuser($user) or is_siteadmin($user)) {
continue;
}
+ if ($user->lastname !== '' and $user->firstname !== '' and $user->email !== '') {
+ // This can happen on MySQL - see MDL-52831.
+ continue;
+ }
delete_user($user);
mtrace(" Deleted not fully setup user $user->username ($user->id)");
}
*/
_focusFromClick: false,
+ /**
+ * Whether if the last gesturemovestart event target was contained in this editor or not.
+ *
+ * @property _gesturestartededitor
+ * @type Boolean
+ * @default false
+ * @private
+ */
+ _gesturestartededitor: false,
+
/**
* Set up the watchers for selection save and restoration.
*
Y.soon(Y.bind(this._hasSelectionChanged, this, e));
}, this);
- // To capture both mouseup and touchend events, we need to track the gesturemoveend event in standAlone mode. Without
- // standAlone, it will only fire if we listened to a gesturemovestart too.
- this.editor.on('gesturemoveend', function(e) {
- Y.soon(Y.bind(this._hasSelectionChanged, this, e));
- }, {
- standAlone: true
- }, this);
+ Y.one(document.body).on('gesturemovestart', function(e) {
+ if (this._wrapper.contains(e.target._node)) {
+ this._gesturestartededitor = true;
+ } else {
+ this._gesturestartededitor = false;
+ }
+ }, null, this);
+
+ Y.one(document.body).on('gesturemoveend', function(e) {
+ if (!this._gesturestartededitor) {
+ // Ignore the event if movestart target was not contained in the editor.
+ return;
+ }
+ Y.soon(Y.bind(this._hasSelectionChanged, this, e));
+ }, null, this);
return this;
},
send_file_not_found();
}
+ if ($context->get_course_context(false)) {
+ // If block is in course context, then check if user has capability to access course.
+ require_course_login($course);
+ } else if ($CFG->forcelogin) {
+ // If user is logged out, bp record will not be visible, even if the user would have access if logged in.
+ require_login();
+ }
+
$bprecord = $DB->get_record('block_positions', array('contextid' => $context->id, 'blockinstanceid' => $context->instanceid));
// User can't access file, if block is hidden or doesn't have block:view capability
if (($bprecord && !$bprecord->visible) || !has_capability('moodle/block:view', $context)) {
$values = array(0, 1);
}
- if (!is_null($attributes['group'])) {
+ if (!empty($attributes['group'])) {
$this->_group = 'checkboxgroup' . $attributes['group'];
unset($attributes['group']);
*/
var $_pageparams = '';
+ /**
+ * Whether the form contains any client-side validation or not.
+ * @var bool
+ */
+ protected $clientvalidation = false;
+
/**
* Class constructor - same parameters as HTML_QuickForm_DHTMLRulesTableless
*
{
parent::addRule($element, $message, $type, $format, $validation, $reset, $force);
if ($validation == 'client') {
- $this->updateAttributes(array('onsubmit' => 'try { var myValidator = validate_' . $this->_formName . '; } catch(e) { return true; } return myValidator(this);'));
+ $this->clientvalidation = true;
}
}
foreach ($arg1 as $rules) {
foreach ($rules as $rule) {
$validation = (isset($rule[3]) && 'client' == $rule[3])? 'client': 'server';
-
- if ('client' == $validation) {
- $this->updateAttributes(array('onsubmit' => 'try { var myValidator = validate_' . $this->_formName . '; } catch(e) { return true; } return myValidator(this);'));
+ if ($validation == 'client') {
+ $this->clientvalidation = true;
}
}
}
} elseif (is_string($arg1)) {
-
if ($validation == 'client') {
- $this->updateAttributes(array('onsubmit' => 'try { var myValidator = validate_' . $this->_formName . '; } catch(e) { return true; } return myValidator(this);'));
+ $this->clientvalidation = true;
}
}
}
*/
function getValidationScript()
{
- if (empty($this->_rules) || empty($this->_attributes['onsubmit'])) {
+ if (empty($this->_rules) || $this->clientvalidation === false) {
return '';
}
var skipClientValidation = false;
-function qf_errorHandler(element, _qfMsg, escapedName) {
- div = element.parentNode;
+(function() {
- if ((div == undefined) || (element.name == undefined)) {
- //no checking can be done for undefined elements so let server handle it.
- return true;
- }
+ function qf_errorHandler(element, _qfMsg, escapedName) {
+ div = element.parentNode;
- if (_qfMsg != \'\') {
- var errorSpan = document.getElementById(\'id_error_\' + escapedName);
- if (!errorSpan) {
- errorSpan = document.createElement("span");
- errorSpan.id = \'id_error_\' + escapedName;
- errorSpan.className = "error";
- element.parentNode.insertBefore(errorSpan, element.parentNode.firstChild);
- document.getElementById(errorSpan.id).setAttribute(\'TabIndex\', \'0\');
- document.getElementById(errorSpan.id).focus();
- }
+ if ((div == undefined) || (element.name == undefined)) {
+ //no checking can be done for undefined elements so let server handle it.
+ return true;
+ }
- while (errorSpan.firstChild) {
- errorSpan.removeChild(errorSpan.firstChild);
- }
+ if (_qfMsg != \'\') {
+ var errorSpan = document.getElementById(\'id_error_\' + escapedName);
+ if (!errorSpan) {
+ errorSpan = document.createElement("span");
+ errorSpan.id = \'id_error_\' + escapedName;
+ errorSpan.className = "error";
+ element.parentNode.insertBefore(errorSpan, element.parentNode.firstChild);
+ document.getElementById(errorSpan.id).setAttribute(\'TabIndex\', \'0\');
+ document.getElementById(errorSpan.id).focus();
+ }
+
+ while (errorSpan.firstChild) {
+ errorSpan.removeChild(errorSpan.firstChild);
+ }
- errorSpan.appendChild(document.createTextNode(_qfMsg.substring(3)));
+ errorSpan.appendChild(document.createTextNode(_qfMsg.substring(3)));
- if (div.className.substr(div.className.length - 6, 6) != " error"
- && div.className != "error") {
- div.className += " error";
- linebreak = document.createElement("br");
- linebreak.className = "error";
- linebreak.id = \'id_error_break_\' + escapedName;
- errorSpan.parentNode.insertBefore(linebreak, errorSpan.nextSibling);
- }
+ if (div.className.substr(div.className.length - 6, 6) != " error"
+ && div.className != "error") {
+ div.className += " error";
+ linebreak = document.createElement("br");
+ linebreak.className = "error";
+ linebreak.id = \'id_error_break_\' + escapedName;
+ errorSpan.parentNode.insertBefore(linebreak, errorSpan.nextSibling);
+ }
- return false;
- } else {
- var errorSpan = document.getElementById(\'id_error_\' + escapedName);
- if (errorSpan) {
- errorSpan.parentNode.removeChild(errorSpan);
- }
- var linebreak = document.getElementById(\'id_error_break_\' + escapedName);
- if (linebreak) {
- linebreak.parentNode.removeChild(linebreak);
- }
+ return false;
+ } else {
+ var errorSpan = document.getElementById(\'id_error_\' + escapedName);
+ if (errorSpan) {
+ errorSpan.parentNode.removeChild(errorSpan);
+ }
+ var linebreak = document.getElementById(\'id_error_break_\' + escapedName);
+ if (linebreak) {
+ linebreak.parentNode.removeChild(linebreak);
+ }
- if (div.className.substr(div.className.length - 6, 6) == " error") {
- div.className = div.className.substr(0, div.className.length - 6);
- } else if (div.className == "error") {
- div.className = "";
- }
+ if (div.className.substr(div.className.length - 6, 6) == " error") {
+ div.className = div.className.substr(0, div.className.length - 6);
+ } else if (div.className == "error") {
+ div.className = "";
+ }
- return true;
- }
-}';
+ return true;
+ }
+ }';
$validateJS = '';
foreach ($test as $elementName => $jsandelement) {
// Fix for bug displaying errors for elements in a group
'/[_\[\]-]/',
create_function('$matches', 'return sprintf("_%2x",ord($matches[0]));'),
$elementName);
+ $valFunc = 'validate_' . $this->_formName . '_' . $escapedElementName . '(ev.target, \''.$escapedElementName.'\')';
+
$js .= '
-function validate_' . $this->_formName . '_' . $escapedElementName . '(element, escapedName) {
- if (undefined == element) {
- //required element was not found, then let form be submitted without client side validation
- return true;
- }
- var value = \'\';
- var errFlag = new Array();
- var _qfGroups = {};
- var _qfMsg = \'\';
- var frm = element.parentNode;
- if ((undefined != element.name) && (frm != undefined)) {
- while (frm && frm.nodeName.toUpperCase() != "FORM") {
- frm = frm.parentNode;
+ function validate_' . $this->_formName . '_' . $escapedElementName . '(element, escapedName) {
+ if (undefined == element) {
+ //required element was not found, then let form be submitted without client side validation
+ return true;
}
- ' . join("\n", $jsArr) . '
- return qf_errorHandler(element, _qfMsg, escapedName);
- } else {
- //element name should be defined else error msg will not be displayed.
- return true;
- }
-}
+ var value = \'\';
+ var errFlag = new Array();
+ var _qfGroups = {};
+ var _qfMsg = \'\';
+ var frm = element.parentNode;
+ if ((undefined != element.name) && (frm != undefined)) {
+ while (frm && frm.nodeName.toUpperCase() != "FORM") {
+ frm = frm.parentNode;
+ }
+ ' . join("\n", $jsArr) . '
+ return qf_errorHandler(element, _qfMsg, escapedName);
+ } else {
+ //element name should be defined else error msg will not be displayed.
+ return true;
+ }
+ }
+
+ document.getElementById(\'' . $element->_attributes['id'] . '\').addEventListener(\'blur\', function(ev) {
+ ' . $valFunc . '
+ });
+ document.getElementById(\'' . $element->_attributes['id'] . '\').addEventListener(\'change\', function(ev) {
+ ' . $valFunc . '
+ });
';
$validateJS .= '
- ret = validate_' . $this->_formName . '_' . $escapedElementName.'(frm.elements[\''.$elementName.'\'], \''.$escapedElementName.'\') && ret;
- if (!ret && !first_focus) {
- first_focus = true;
- Y.use(\'moodle-core-event\', function() {
- Y.Global.fire(M.core.globalEvents.FORM_ERROR, {formid: \'' . $this->_attributes['id'] . '\',
- elementid: \'id_error_' . $escapedElementName . '\'});
- document.getElementById(\'id_error_' . $escapedElementName . '\').focus();
- });
- }
+ ret = validate_' . $this->_formName . '_' . $escapedElementName.'(frm.elements[\''.$elementName.'\'], \''.$escapedElementName.'\') && ret;
+ if (!ret && !first_focus) {
+ first_focus = true;
+ Y.use(\'moodle-core-event\', function() {
+ Y.Global.fire(M.core.globalEvents.FORM_ERROR, {formid: \'' . $this->_attributes['id'] . '\',
+ elementid: \'id_error_' . $escapedElementName . '\'});
+ document.getElementById(\'id_error_' . $escapedElementName . '\').focus();
+ });
+ }
';
// Fix for bug displaying errors for elements in a group
//unset($element);
//$element =& $this->getElement($elementName);
//end of fix
- $valFunc = 'validate_' . $this->_formName . '_' . $escapedElementName . '(this, \''.$escapedElementName.'\')';
- $onBlur = $element->getAttribute('onBlur');
- $onChange = $element->getAttribute('onChange');
- $element->updateAttributes(array('onBlur' => $onBlur . $valFunc,
- 'onChange' => $onChange . $valFunc));
+ //$onBlur = $element->getAttribute('onBlur');
+ //$onChange = $element->getAttribute('onChange');
+ //$element->updateAttributes(array('onBlur' => $onBlur . $valFunc,
+ //'onChange' => $onChange . $valFunc));
}
// do not rely on frm function parameter, because htmlarea breaks it when overloading the onsubmit method
$js .= '
-function validate_' . $this->_formName . '(frm) {
- if (skipClientValidation) {
- return true;
- }
- var ret = true;
-
- var frm = document.getElementById(\''. $this->_attributes['id'] .'\')
- var first_focus = false;
-' . $validateJS . ';
- return ret;
-}
+
+ function validate_' . $this->_formName . '() {
+ if (skipClientValidation) {
+ return true;
+ }
+ var ret = true;
+
+ var frm = document.getElementById(\''. $this->_attributes['id'] .'\')
+ var first_focus = false;
+ ' . $validateJS . ';
+ return ret;
+ }
+
+
+ document.getElementById(\'' . $this->_attributes['id'] . '\').addEventListener(\'submit\', function(ev) {
+ try {
+ var myValidator = validate_' . $this->_formName . ';
+ } catch(e) {
+ return true;
+ }
+ if (!myValidator()) {
+ ev.preventDefault();
+ }
+ });
+})();
//]]>
</script>';
return $js;
}
}
+ // If the X-PHP-Originating-Script email header is on then also add an additional
+ // header with details of where exactly in moodle the email was triggered from,
+ // either a call to message_send() or to email_to_user().
+ if (ini_get('mail.add_x_header')) {
+
+ $stack = debug_backtrace(false);
+ $origin = $stack[0];
+
+ foreach ($stack as $depth => $call) {
+ if ($call['function'] == 'message_send') {
+ $origin = $call;
+ }
+ }
+
+ $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
+ . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
+ $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
+ }
+
if (!empty($from->priority)) {
$mail->Priority = $from->priority;
}
// Replace underscores (which are classed as word characters) with spaces.
$string = preg_replace('/_/u', ' ', $string);
// Remove any characters that shouldn't be treated as word boundaries.
- $string = preg_replace('/[\'’-]/u', '', $string);
+ $string = preg_replace('/[\'"’-]/u', '', $string);
// Remove dots and commas from within numbers only.
$string = preg_replace('/([0-9])[.,]([0-9])/u', '$1$2', $string);
protected static $loadadmintree = false;
/** @var mixed If set to an int, that section will be included even if it has no activities */
public $includesectionnum = false;
+ /** @var bool does the node need to be loaded via ajax */
+ public $requiresajaxloading = false;
/**
* Constructs a new navigation_node
if ($child->display && $child->has_children() && $child->children->count() == 0) {
$child->id = 'expandable_branch_'.$child->type.'_'.clean_param($child->key, PARAM_ALPHANUMEXT);
$this->add_class('canexpand');
+ $child->requiresajaxloading = true;
$expandable[] = array('id' => $child->id, 'key' => $child->key, 'type' => $child->type);
}
$child->find_expandable($expandable);
}
$siteadminnode = $this->add(get_string('administrationsite'), new moodle_url('/admin'), self::TYPE_SITE_ADMIN, null, 'siteadministration');
$siteadminnode->id = 'expandable_branch_'.$siteadminnode->type.'_'.clean_param($siteadminnode->key, PARAM_ALPHANUMEXT);
- $this->page->requires->data_for_js('siteadminexpansion', $siteadminnode);
+ $siteadminnode->requiresajaxloading = 'true';
}
}
$attributes['type'] = $child->type;
$attributes['key'] = $child->key;
$attributes['class'] = $child->get_css_type();
+ $attributes['requiresajaxloading'] = $child->requiresajaxloading;
if ($child->icon instanceof pix_icon) {
$attributes['icon'] = array(
if (!isset($this->_rules[$element])) {
$this->_rules[$element] = array();
}
- if ($validation == 'client') {
- $this->updateAttributes(array('onsubmit' => 'try { var myValidator = validate_' . $this->_attributes['id'] . '; } catch(e) { return true; } return myValidator(this);'));
- }
$this->_rules[$element][] = array(
'type' => $type,
'format' => $format,
$this->_required[] = $elementName;
$required++;
}
- if ('client' == $validation) {
- $this->updateAttributes(array('onsubmit' => 'try { var myValidator = validate_' . $this->_attributes['id'] . '; } catch(e) { return true; } return myValidator(this);'));
- }
}
}
if ($required > 0 && count($groupObj->getElements()) == $required) {
if ($type == 'required') {
$this->_required[] = $group;
}
- if ($validation == 'client') {
- $this->updateAttributes(array('onsubmit' => 'try { var myValidator = validate_' . $this->_attributes['id'] . '; } catch(e) { return true; } return myValidator(this);'));
- }
}
} // end func addGroupRule
} // end func getValidationScript
// }}}
-
- function display() {
- $this->getValidationScript();
- return parent::display();
- }
}
-?>
\ No newline at end of file
+?>
$this->_html = str_replace('></label>', '> </label>', $this->_html);
// add a validation script
if ('' != ($script = $form->getValidationScript())) {
- $this->_html = $script . "\n" . $this->_html;
+ $this->_html = $this->_html . "\n" . $script;
}
} // end func finishForm
MDL-40267 - Moodle core_text strlen functions used for range rule rule to be utf8 safe.
MDL-46467 - $mform->hardfreeze causes labels to loose their for HTML attribute
MDL-52081 - made all constructors PHP7 compatible
+MDL-52826 - Remove onsubmit events pointing to the global validation functions and script
+ tag moved after the HTML
Pear
}
if ($borkedmysql) {
- if (empty($records) and isset($empties[$table])) {
+ if (empty($records)) {
+ if (!isset($empties[$table])) {
+ // Table has been modified and is not empty.
+ $DB->delete_records($table, null);
+ }
continue;
}
}
if (empty($records)) {
- if (isset($empties[$table])) {
- // table was not modified and is empty
- } else {
+ if (!isset($empties[$table])) {
+ // Table has been modified and is not empty.
$DB->delete_records($table, array());
}
continue;
* @AfterStep
*/
public function after_step(StepEvent $event) {
- global $CFG;
+ global $CFG, $DB;
// Save the page content if the step failed.
if (!empty($CFG->behat_faildump_path) &&
$event->getResult() === StepEvent::FAILED) {
$this->take_contentdump($event);
}
+
+ // Abort any open transactions to prevent subsequent tests hanging.
+ // This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't
+ // want to see a message in the behat output.
+ if ($event->hasException()) {
+ if ($DB && $DB->is_transaction_started()) {
+ $DB->force_transaction_rollback();
+ }
+ }
}
/**
* @AfterScenario @_switch_window
*/
public function after_scenario_switchwindow(ScenarioEvent $event) {
- $this->getSession()->restart();
+ for ($count = 0; $count < self::EXTENDED_TIMEOUT; $count) {
+ try {
+ $this->getSession()->restart();
+ break;
+ } catch (DriverException $e) {
+ // Wait for timeout and try again.
+ sleep(self::TIMEOUT);
+ }
+ }
+ // If session is not restarted above then it will try to start session before next scenario
+ // and if that fails then exception will be thrown.
}
/**
$nodetextliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
$hasblocktree = "[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]";
$hasbranch = "[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]";
- $hascollapsed = "li[contains(concat(' ', normalize-space(@class), ' '), ' collapsed ') or @data-expandable='1']";
- $notcollapsed = "li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed '))]";
+ $hascollapsed = "li[@aria-expanded='false']";
+ $notcollapsed = "li[@aria-expanded='true']";
$match = "[normalize-space(.)={$nodetextliteral}]";
// Avoid problems with quotes.
$iscollapsed = 'li';
}
- // First check root nodes.
+ // First check root nodes, it can be a span or link.
$xpath = "//ul{$hasblocktree}/$hascollapsed/p{$isbranch}/span{$match}|";
+ $xpath .= "//ul{$hasblocktree}/$hascollapsed/p{$isbranch}/a{$match}|";
+
// Next search for the node containing the text within a link.
- $xpath .= "//ul{$hasblocktree}//{$notcollapsed}/ul/{$iscollapsed}/p{$isbranch}/a{$match}|";
+ $xpath .= "//ul{$hasblocktree}//ul/{$iscollapsed}/p{$isbranch}/a{$match}|";
+
// Finally search for the node containing the text within a span.
- $xpath .= "//ul{$hasblocktree}//{$notcollapsed}/ul/{$iscollapsed}/p{$isbranch}/span{$match}";
+ $xpath .= "//ul{$hasblocktree}//ul/{$iscollapsed}/p{$isbranch}/span{$match}";
$node = $this->find('xpath', $xpath, $exception);
$this->ensure_node_is_visible($node);
$node = $this->get_node_text_node($nodetext, true);
$node = $node->getParent();
- if ($node->hasAttribute('data-expandable') && $node->getAttribute('data-expandable')) {
- return true;
+ if ($node->hasClass('emptybranch')) {
+ throw new ExpectationException('The "' . $nodetext . '" node is not expandable', $this->getSession());
}
- throw new ExpectationException('The "' . $nodetext . '" node is not expandable', $this->getSession());
+
+ return true;
}
/**
$node = $this->get_node_text_node($nodetext);
$node = $node->getParent();
- if ($node->hasAttribute('data-expandable') && $node->getAttribute('data-expandable')) {
- throw new ExpectationException('The "' . $nodetext . '" node is expandable', $this->getSession());
+
+ if ($node->hasClass('emptybranch') || $node->hasClass('tree_item')) {
+ return true;
}
- return true;
+ throw new ExpectationException('The "' . $nodetext . '" node is expandable', $this->getSession());
}
/**
}
// Keep expanding all sub-parents if js enabled.
- if ($this->running_javascript() && $node->hasClass('collapsed')) {
+ if ($this->running_javascript() && $node->hasAttribute('aria-expanded') &&
+ ($node->getAttribute('aria-expanded') == "false")) {
+
$xpath = "/p[contains(concat(' ', normalize-space(@class), ' '), ' tree_item ')]";
$nodetoexpand = $node->find('xpath', $xpath);
$nodetoexpand->click();
// Wait for node to load, if not loaded before.
- if ($nodetoexpand->hasAttribute('data-loaded') && $nodetoexpand->getAttribute('data-loaded') == 0) {
- $jscondition = '(document.evaluate("' . $nodetoexpand->getXpath() . '", document, null, '.
- 'XPathResult.ANY_TYPE, null).iterateNext().getAttribute(\'data-loaded\') == 1)';
+ $linode = $nodetoexpand->getParent();
+ if ($linode && $linode->hasAttribute('data-loaded') && $linode->getAttribute('data-loaded') == "false") {
+ $jscondition = '(document.evaluate("' . $linode->getXpath() . '", document, null, '.
+ 'XPathResult.ANY_TYPE, null).iterateNext().getAttribute(\'data-loaded\') == "true")';
$this->getSession()->wait(self::EXTENDED_TIMEOUT * 1000, $jscondition);
}
"//div[contains(concat(' ', normalize-space(@class), ' '), ' content ')]/div" .
"/ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]" .
"/li[p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" .
- "/span[normalize-space(.)=" . $nodetextliteral ."]]";
- $node = $this->find('xpath', $xpath, $exception);
+ "/span[normalize-space(.)=" . $nodetextliteral ."]]" .
+ "|" .
+ "//div[contains(concat(' ', normalize-space(@class), ' '), ' content ')]/div" .
+ "/ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]" .
+ "/li[p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" .
+ "/a[normalize-space(.)=" . $nodetextliteral ."]]";
+
+ $node = $this->find('xpath', $xpath, $exception);
return $node;
}
$this->assertEquals(3, $count);
$count = count_words('one"two three-four');
- $this->assertEquals(3, $count);
+ $this->assertEquals(2, $count);
$count = count_words('one@two three_four');
$this->assertEquals(4, $count);
* The previous events API handlers are being deprecated in favour of events 2 API, debugging messages are being displayed if
there are 3rd party plugins using it. Switch to events 2 API please, see https://docs.moodle.org/dev/Event_2#Event_dispatching_and_observers
Note than you will need to bump the plugin version so moodle is aware that you removed the plugin's event handlers.
+* mforms validation functions are not available in the global JS namespace anymore, event listeners
+ are assigned to fields and buttons through a self-contained JS function.
=== 3.0 ===
$namefields = useredit_get_required_name_fields();
foreach ($namefields as $field) {
$mform->addElement('text', $field, get_string($field), 'maxlength="100" size="30"');
- $mform->setType($field, PARAM_TEXT);
+ $mform->setType($field, PARAM_NOTAGS);
$stringid = 'missing' . $field;
if (!get_string_manager()->string_exists($stringid, 'moodle')) {
$stringid = 'required';
function definition_after_data(){
$mform = $this->_form;
$mform->applyFilter('username', 'trim');
+
+ // Trim required name fields.
+ foreach (useredit_get_required_name_fields() as $field) {
+ $mform->applyFilter($field, 'trim');
+ }
}
function validation($data, $files) {
And I click on ".stampbutton" "css_element"
And I click on ".linebutton" "css_element"
And I click on ".commentcolourbutton" "css_element"
- And I click on "//img[@alt=\"Blue\"]" "xpath_element"
+ And I click on "//img[@alt=\"Blue\"]/parent::button" "xpath_element"
And I change window size to "medium"
And I wait until the page is ready
And I click on "Close" "button"
$string['notanswered'] = 'Not answered yet';
$string['notenrolledchoose'] = 'Sorry, only enrolled users are allowed to make choices.';
$string['notopenyet'] = 'Sorry, this activity is not available until {$a}';
-$string['numberofuser'] = 'Number of participants';
-$string['numberofuserinpercentage'] = 'Percentage of participants';
+$string['numberofuser'] = 'Number of responses';
+$string['numberofuserinpercentage'] = 'Percentage of responses';
$string['option'] = 'Option';
$string['optionno'] = 'Option {no}';
$string['options'] = 'Options';
header('Expires: ' . gmdate("D, d M Y H:i:s", time() + $lifetime) . ' GMT');
header('Cache-control: max_age = '. $lifetime);
header('Pragma: ');
- header('Content-type: text/css; charset=utf-8'); // Correct MIME type
+ header('Content-type: application/javascript; charset=utf-8'); // Correct MIME type.
echo $data->jstemplate;
}
\ No newline at end of file
require_login($course, false, $cm);
if ($returnpage == 'index.php') {
- $returnto = forum_go_back_to(new moodle_url("/mod/forum/$returnpage", array('id' => $course->id)));
+ $returnto = new moodle_url("/mod/forum/$returnpage", array('id' => $course->id));
} else {
- $returnto = forum_go_back_to(new moodle_url("/mod/forum/$returnpage", array('f' => $forum->id)));
+ $returnto = new moodle_url("/mod/forum/$returnpage", array('f' => $forum->id));
}
if (isguestuser()) { // Guests can't change forum
return false;
}
- if ($this->get_question_type_for_slot($slotnumber) == 'random') {
+ if (in_array($this->get_question_type_for_slot($slotnumber), array('random', 'missingtype'))) {
return \question_engine::can_questions_finish_during_the_attempt(
$this->quizobj->get_quiz()->preferredbehaviour);
}
return $this->slotsinorder[$slotnumber]->canfinish;
}
- $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context());
- $tempslot = $quba->add_question(\question_bank::load_question(
- $this->slotsinorder[$slotnumber]->questionid));
- $quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour);
- $quba->start_all_questions();
+ try {
+ $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context());
+ $tempslot = $quba->add_question(\question_bank::load_question(
+ $this->slotsinorder[$slotnumber]->questionid));
+ $quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour);
+ $quba->start_all_questions();
- $this->slotsinorder[$slotnumber]->canfinish = $quba->can_question_finish_during_attempt($tempslot);
- return $this->slotsinorder[$slotnumber]->canfinish;
+ $this->slotsinorder[$slotnumber]->canfinish = $quba->can_question_finish_during_attempt($tempslot);
+ return $this->slotsinorder[$slotnumber]->canfinish;
+ } catch (\Exception $e) {
+ // If the question fails to start, this should not block editing.
+ return false;
+ }
}
/**
// Sorry - localsync disabled.
return;
}
- if ($scorm->reference !== '' and (!$full or $scorm->sha1hash !== sha1($scorm->reference))) {
+ if ($scorm->reference !== '') {
$fs->delete_area_files($context->id, 'mod_scorm', 'package');
$filerecord = array('contextid' => $context->id, 'component' => 'mod_scorm', 'filearea' => 'package',
'itemid' => 0, 'filepath' => '/');
- if ($packagefile = $fs->create_file_from_url($filerecord, $scorm->reference, array('calctimeout' => true))) {
- $newhash = sha1($scorm->reference);
+ if ($packagefile = $fs->create_file_from_url($filerecord, $scorm->reference, array('calctimeout' => true), true)) {
+ $newhash = $packagefile->get_contenthash();
} else {
$newhash = null;
}
$sql = "SELECT MAX(attempt)
FROM {scorm_scoes_track}
WHERE userid = ? AND scormid = ?
- AND (value='completed' OR value='passed')";
- $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid));
+ AND (".$DB->sql_compare_text('value')." = ".$DB->sql_compare_text('?')." OR ".
+ $DB->sql_compare_text('value')." = ".$DB->sql_compare_text('?').")";
+ $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid, 'completed', 'passed'));
if (empty($lastattempt)) {
return '1';
} else {
scorm_require_available($this->scorm, false);
}
+ /**
+ * Test scorm_get_last_completed_attempt
+ *
+ * @return void
+ */
+ public function test_scorm_get_last_completed_attempt() {
+ $this->assertEquals(1, scorm_get_last_completed_attempt($this->scorm->id, $this->student->id));
+ }
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015112701; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2016011400; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2015111000; // Requires this Moodle version.
$plugin->component = 'mod_scorm'; // Full name of the plugin (used for diagnostics).
$plugin->cron = 300;
}
}
- echo "<p <p class=\"centerpara\">";
+ echo "<p class=\"centerpara\">";
echo $OUTPUT->user_picture($user, array('courseid'=>$course->id));
echo "</p>";
if ($showscales) {
// Print overall summary
- echo "<p <p class=\"centerpara\">>";
+ echo "<p class=\"centerpara\">";
survey_print_graph("id=$id&sid=$student&type=student.png");
echo "</p>";
$table = new html_table();
$table->head = array(get_string($question->text, "survey"));
$table->align = array ("left");
- $table->data[] = array(s($answer->answer1)); // no html here, just plain text
+ if (!empty($question->options) && $answer->answer1 > 0) {
+ $answers = explode(',', get_string($question->options, 'survey'));
+ if ($answer->answer1 <= count($answers)) {
+ $table->data[] = array(s($answers[$answer->answer1 - 1])); // No html here, just plain text.
+ } else {
+ $table->data[] = array(s($answer->answer1)); // No html here, just plain text.
+ }
+ } else {
+ $table->data[] = array(s($answer->answer1)); // No html here, just plain text.
+ }
echo html_writer::table($table);
echo $OUTPUT->spacer(array('height'=>30));
}
$number .= ".$currentsection[2]";
}
}
- $toc .= parser_utils::h('p', $number . ". " . parser_utils::h('a', $header[1], array('href' => "#toc-$i")), array('class' => 'wiki-toc-section-' . $header[0] . " wiki-toc-section"));
+ $toc .= parser_utils::h('p', $number . ". " .
+ parser_utils::h('a', str_replace(array('[[', ']]'), '', $header[1]), array('href' => "#toc-$i")),
+ array('class' => 'wiki-toc-section-' . $header[0] . " wiki-toc-section"));
$i++;
}
$this->assertEquals($toc, $actual['toc']);
$this->assertNotEquals(false, $section);
+ // Test toc section names being wikilinks.
+ $input = '<h1>[[Heading 1]]</h1><h2>[[Heading A]]</h2><h2>Heading D</h2>';
+ $regexpoutput = '!<h1><a name="toc-1"></a>' .
+ '<a class="wiki_newentry" href.*mod/wiki/create\.php\?.*title=Heading\+1.*action=new.*>Heading 1<.*' .
+ '<h2><a name="toc-2"></a>' .
+ '<a class="wiki_newentry" href.*mod/wiki/create\.php\?.*title=Heading\+A.*action=new.*>Heading A<.*' .
+ '<h2><a name="toc-3"></a>' .
+ 'Heading D!ms';
+ $regexptoc = '!<a href="#toc-1">Heading 1.*<a href="#toc-2">Heading A</a>.*<a href="#toc-3">Heading D</a>!ms';
+ $section = wiki_parser_proxy::get_section($input, 'html', 'Another [[wikilinked]] test');
+ $actual = wiki_parser_proxy::parse($input, 'html', array(
+ 'link_callback' => '/mod/wiki/locallib.php:wiki_parser_link',
+ 'link_callback_args' => array('swid' => 1)
+ ));
+ $this->assertRegExp($regexpoutput, $actual['parsed_text']);
+ $this->assertRegExp($regexptoc, $actual['toc']);
+
// Now going to test Creole markup.
// Note that Creole uses links to the escaped version of the section.
--- /dev/null
+<?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/>.
+
+/**
+ * The mod_workshop submission deleted event.
+ *
+ * @package mod_workshop
+ * @copyright 2015 Paul Nicholls
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_workshop\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_workshop submission deleted event class.
+ *
+ * @property-read array $other {
+ * Extra information about the event.
+ *
+ * - string submissiontitle: (optional) Submission title.
+ * }
+ *
+ * @package mod_workshop
+ * @since Moodle 3.1
+ * @copyright 2015 Paul Nicholls
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class submission_deleted extends \core\event\base {
+
+ /**
+ * Init method.
+ */
+ protected function init() {
+ $this->data['crud'] = 'd';
+ $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+ $this->data['objecttable'] = 'workshop_submissions';
+ }
+
+ /**
+ * Returns non-localised description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '$this->userid' deleted the submission with id '$this->objectid' for the workshop " .
+ "with course module id '$this->contextinstanceid'.";
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventsubmissiondeleted', 'workshop');
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/mod/workshop/submission.php',
+ array('cmid' => $this->contextinstanceid, 'id' => $this->objectid));
+ }
+
+ /**
+ * Replace add_to_log() statement.
+ *
+ * @return array of parameters to be passed to legacy add_to_log() function.
+ */
+ protected function get_legacy_logdata() {
+ return array($this->courseid, 'workshop', 'delete submission',
+ 'submission.php?cmid=' . $this->contextinstanceid . '&id=' . $this->objectid,
+ $this->objectid, $this->contextinstanceid);
+ }
+
+ /**
+ * Defines mapping of the 'objectid' property when restoring course logs.
+ *
+ * @return array
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'workshop_submissions', 'restore' => 'workshop_submission');
+ }
+
+ /**
+ * Defines mapping of the 'other' property when restoring course logs.
+ *
+ * @return array|bool
+ */
+ public static function get_other_mapping() {
+ // Nothing to map.
+ return false;
+ }
+}
'manager' => CAP_ALLOW
)
),
+
+ // Ability to delete other users' submissions.
+ 'mod/workshop:deletesubmissions' => array(
+ 'captype' => 'write',
+ 'contextlevel' => CONTEXT_MODULE,
+ 'archetypes' => array(
+ 'teacher' => CAP_ALLOW,
+ 'editingteacher' => CAP_ALLOW,
+ 'manager' => CAP_ALLOW
+ )
+ ),
);
$string['daystomorrow'] = 'tomorrow';
$string['daysyesterday'] = 'yesterday';
$string['deadlinesignored'] = 'Time restrictions do not apply to you';
+$string['deletesubmission'] = 'Delete submission';
$string['editassessmentform'] = 'Edit assessment form';
$string['editassessmentformstrategy'] = 'Edit assessment form ({$a})';
$string['editingassessmentform'] = 'Editing assessment form';
$string['eventsubmissioncreated'] = 'Submission created';
$string['eventsubmissionreassessed'] = 'Submission re-assessed';
$string['eventsubmissionupdated'] = 'Submission updated';
+$string['eventsubmissiondeleted'] = 'Submission deleted';
$string['eventsubmissionviewed'] = 'Submission viewed';
$string['eventphaseswitched'] = 'Phase switched';
$string['example'] = 'Example submission';
$string['submissionattachment'] = 'Attachment';
$string['submissionby'] = 'Submission by {$a}';
$string['submissioncontent'] = 'Submission content';
+$string['submissiondeleteconfirm'] = 'Are you sure you want to delete the following submission?';
+$string['submissiondeleteconfirmassess'] = 'Are you sure you want to delete the following submission? Note this will also delete {$a->count} assessments associated with this submission, which may affect the reviewers\' grades.';
$string['submissionend'] = 'Submissions deadline';
$string['submissionendbeforestart'] = 'Submissions deadline can not be specified before the open for submissions date';
$string['submissionendevent'] = '{$a} (submissions deadline)';
$string['workshop:addinstance'] = 'Add a new workshop';
$string['workshop:allocate'] = 'Allocate submissions for review';
$string['workshop:editdimensions'] = 'Edit assessment forms';
+$string['workshop:deletesubmissions'] = 'Delete submissions';
$string['workshop:ignoredeadlines'] = 'Ignore time restrictions';
$string['workshop:manageexamples'] = 'Manage example submissions';
$string['workshopname'] = 'Workshop name';
require_once(dirname(__FILE__).'/locallib.php');
require_once($CFG->dirroot . '/repository/lib.php');
-$cmid = required_param('cmid', PARAM_INT); // course module id
-$id = optional_param('id', 0, PARAM_INT); // submission id
-$edit = optional_param('edit', false, PARAM_BOOL); // open for editing?
-$assess = optional_param('assess', false, PARAM_BOOL); // instant assessment required
-
-$cm = get_coursemodule_from_id('workshop', $cmid, 0, false, MUST_EXIST);
+$cmid = required_param('cmid', PARAM_INT); // Course module id.
+$id = optional_param('id', 0, PARAM_INT); // Submission id.
+$edit = optional_param('edit', false, PARAM_BOOL); // Open the page for editing?
+$assess = optional_param('assess', false, PARAM_BOOL); // Instant assessment required.
+$delete = optional_param('delete', false, PARAM_BOOL); // Submission removal requested.
+$confirm = optional_param('confirm', false, PARAM_BOOL); // Submission removal request confirmed.
+
+$cm = get_coursemodule_from_id('workshop', $cmid, 0, false, MUST_EXIST);
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
require_login($course, false, $cm);
$canallocate = has_capability('mod/workshop:allocate', $workshop->context);
$canpublish = has_capability('mod/workshop:publishsubmissions', $workshop->context);
$canoverride = (($workshop->phase == workshop::PHASE_EVALUATION) and has_capability('mod/workshop:overridegrades', $workshop->context));
+$candeleteall = has_capability('mod/workshop:deletesubmissions', $workshop->context);
$userassessment = $workshop->get_assessment_of_submission_by_user($submission->id, $USER->id);
$isreviewer = !empty($userassessment);
$editable = ($cansubmit and $ownsubmission);
+$deletable = $candeleteall;
$ispublished = ($workshop->phase == workshop::PHASE_CLOSED
and $submission->published == 1
and has_capability('mod/workshop:viewpublishedsubmissions', $workshop->context));
}
$edit = ($editable and $edit);
+if (!$candeleteall and $ownsubmission and $editable) {
+ // Only allow the student to delete their own submission if it's still editable and hasn't been assessed.
+ if (count($workshop->get_assessments_of_submission($submission->id)) > 0) {
+ $deletable = false;
+ } else {
+ $deletable = true;
+ }
+}
+
+if ($submission->id and $delete and $confirm and $deletable) {
+ require_sesskey();
+ $workshop->delete_submission($submission);
+
+ // Event information.
+ $params = array(
+ 'context' => $workshop->context,
+ 'courseid' => $workshop->course->id,
+ 'relateduserid' => $submission->authorid,
+ 'other' => array(
+ 'submissiontitle' => $submission->title
+ )
+ );
+ $params['objectid'] = $submission->id;
+ $event = \mod_workshop\event\submission_deleted::create($params);
+ $event->add_record_snapshot('workshop', $workshoprecord);
+ $event->trigger();
+
+ redirect($workshop->view_url());
+}
+
$seenaspublished = false; // is the submission seen as a published submission?
if ($submission->id and ($ownsubmission or $canviewall or $isreviewer)) {
die();
}
+// Confirm deletion (if requested).
+if ($deletable and $delete) {
+ $prompt = get_string('submissiondeleteconfirm', 'workshop');
+ if ($candeleteall) {
+ $count = count($workshop->get_assessments_of_submission($submission->id));
+ if ($count > 0) {
+ $prompt = get_string('submissiondeleteconfirmassess', 'workshop', ['count' => $count]);
+ }
+ }
+ echo $output->confirm($prompt, new moodle_url($PAGE->url, ['delete' => 1, 'confirm' => 1]), $workshop->view_url());
+}
+
// else display the submission
if ($submission->id) {
echo $output->box(get_string('noyoursubmission', 'workshop'));
}
-if ($editable) {
- if ($submission->id) {
- $btnurl = new moodle_url($PAGE->url, array('edit' => 'on', 'id' => $submission->id));
- $btntxt = get_string('editsubmission', 'workshop');
- } else {
- $btnurl = new moodle_url($PAGE->url, array('edit' => 'on'));
- $btntxt = get_string('createsubmission', 'workshop');
+// If not at removal confirmation screen, some action buttons can be displayed.
+if (!$delete) {
+ if ($editable) {
+ if ($submission->id) {
+ $btnurl = new moodle_url($PAGE->url, array('edit' => 'on', 'id' => $submission->id));
+ $btntxt = get_string('editsubmission', 'workshop');
+ } else {
+ $btnurl = new moodle_url($PAGE->url, array('edit' => 'on'));
+ $btntxt = get_string('createsubmission', 'workshop');
+ }
+ echo $output->single_button($btnurl, $btntxt, 'get');
}
- echo $output->single_button($btnurl, $btntxt, 'get');
-}
-if ($submission->id and !$edit and !$isreviewer and $canallocate and $workshop->assessing_allowed($USER->id)) {
- $url = new moodle_url($PAGE->url, array('assess' => 1));
- echo $output->single_button($url, get_string('assess', 'workshop'), 'post');
+ if ($submission->id and $deletable) {
+ $url = new moodle_url($PAGE->url, array('delete' => 1));
+ echo $output->single_button($url, get_string('deletesubmission', 'workshop'), 'get');
+ }
+
+ if ($submission->id and !$edit and !$isreviewer and $canallocate and $workshop->assessing_allowed($USER->id)) {
+ $url = new moodle_url($PAGE->url, array('assess' => 1));
+ echo $output->single_button($url, get_string('assess', 'workshop'), 'post');
+ }
}
if (($workshop->phase == workshop::PHASE_CLOSED) and ($ownsubmission or $canviewall)) {
--- /dev/null
+@mod @mod_workshop
+Feature: Workshop submission removal
+ In order to get rid of accidentally submitted or otherwise inappropriate contents
+ As a student and as a teacher
+ I need to be able to delete my submission, or any submission respectively
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Sam1 | Student1 | student1@example.com |
+ | student2 | Sam2 | Student2 | student2@example.com |
+ | student3 | Sam3 | Student3 | student3@example.com |
+ | teacher1 | Terry1 | Teacher1 | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname |
+ | Course1 | c1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | c1 | student |
+ | student2 | c1 | student |
+ | student3 | c1 | student |
+ | teacher1 | c1 | editingteacher |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber |
+ | workshop | TestWorkshop | Test workshop description | c1 | workshop1 |
+ # Teacher sets up assessment form and changes the phase to submission.
+ And I log in as "teacher1"
+ And I follow "Course1"
+ And I edit assessment form in workshop "TestWorkshop" as:"
+ | id_description__idx_0_editor | Aspect1 |
+ | id_description__idx_1_editor | Aspect2 |
+ | id_description__idx_2_editor | |
+ And I change phase in workshop "TestWorkshop" to "Submission phase"
+ And I log out
+ # Student1 submits.
+ And I log in as "student1"
+ And I follow "Course1"
+ And I follow "TestWorkshop"
+ And I add a submission in workshop "TestWorkshop" as:"
+ | Title | Submission1 |
+ | Submission content | Some content |
+ And I log out
+ # Student2 submits.
+ And I log in as "student2"
+ And I follow "Course1"
+ And I add a submission in workshop "TestWorkshop" as:"
+ | Title | Submission2 |
+ | Submission content | Some content |
+ And I log out
+ # Teacher allocates student3 to be reviewer of student2's submission.
+ And I log in as "teacher1"
+ And I follow "Course1"
+ And I follow "TestWorkshop"
+ And I allocate submissions in workshop "TestWorkshop" as:"
+ | Participant | Reviewer |
+ | Sam2 Student2 | Sam3 Student3 |
+ And I log out
+
+ Scenario: Students can delete their submissions as long as the submissions are editable and not allocated for assessments
+ Given I log in as "student1"
+ And I follow "Course1"
+ And I follow "TestWorkshop"
+ When I follow "My submission"
+ Then I should see "Submission1"
+ And "Delete submission" "button" should exist
+ And I click on "Delete submission" "button"
+ And I should see "Are you sure you want to delete the following submission?"
+ And I should see "Submission1"
+ And I click on "Continue" "button"
+ And I should see "You have not submitted your work yet"
+
+ Scenario: Students cannot delete their submissions if the submissions are not editable
+ Given I log in as "teacher1"
+ And I follow "Course1"
+ And I follow "TestWorkshop"
+ And I change phase in workshop "TestWorkshop" to "Closed"
+ And I log out
+ And I log in as "student1"
+ And I follow "Course1"
+ And I follow "TestWorkshop"
+ When I follow "My submission"
+ Then I should see "Submission1"
+ And "Delete submission" "button" should not exist
+
+ Scenario: Students cannot delete their submissions if the submissions are allocated for assessments
+ Given I log in as "student2"
+ And I follow "Course1"
+ And I follow "TestWorkshop"
+ When I follow "My submission"
+ Then I should see "Submission2"
+ And "Delete submission" "button" should not exist
+
+ Scenario: Teachers can delete submissions even if the submissions are allocated for assessments.
+ Given I log in as "teacher1"
+ And I follow "Course1"
+ And I follow "TestWorkshop"
+ And "Submission1" "link" should exist
+ And "Submission2" "link" should exist
+ When I follow "Submission2"
+ Then "Delete submission" "button" should exist
+ And I click on "Delete submission" "button"
+ And I should see "Are you sure you want to delete the following submission?"
+ And I should see "Note this will also delete 1 assessments associated with this submission, which may affect the reviewers' grades."
+ And I click on "Continue" "button"
+ And "Submission1" "link" should exist
+ And "Submission2" "link" should not exist
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015111600; // The current module version (YYYYMMDDXX)
+$plugin->version = 2016012101; // The current module version (YYYYMMDDXX)
$plugin->requires = 2015111000; // Requires this Moodle version.
$plugin->component = 'mod_workshop';
$plugin->cron = 60; // Give as a chance every minute.
$mform = $this->_form;
$userid = $mform->getElementValue('id');
+ // Trim required name fields.
+ foreach (useredit_get_required_name_fields() as $field) {
+ $mform->applyFilter($field, 'trim');
+ }
+
if ($user = $DB->get_record('user', array('id' => $userid))) {
// Remove description.
global $USER, $CFG, $DB, $OUTPUT;
$mform = $this->_form;
+
+ // Trim required name fields.
+ foreach (useredit_get_required_name_fields() as $field) {
+ $mform->applyFilter($field, 'trim');
+ }
+
if ($userid = $mform->getElementValue('id')) {
$user = $DB->get_record('user', array('id' => $userid));
} else {
}
$strrequired = get_string('required');
+ $stringman = get_string_manager();
// Add the necessary names.
foreach (useredit_get_required_name_fields() as $fullname) {
$mform->addElement('text', $fullname, get_string($fullname), 'maxlength="100" size="30"');
- $mform->addRule($fullname, $strrequired, 'required', null, 'client');
+ if ($stringman->string_exists('missing'.$fullname, 'core')) {
+ $strmissingfield = get_string('missing'.$fullname, 'core');
+ } else {
+ $strmissingfield = $strrequired;
+ }
+ $mform->addRule($fullname, $strmissingfield, 'required', null, 'client');
$mform->setType($fullname, PARAM_NOTAGS);
}
--- /dev/null
+@core @core_user
+Feature: Both first name and surname are always available for every user
+ In order to easily identify and display users on Moodle pages
+ As any user
+ I need to rely on both first name and surname are always available
+
+ Scenario: Attempting to self-register as a new user with empty names
+ Given the following config values are set as admin:
+ | registerauth | email |
+ | passwordpolicy | 0 |
+ And I am on site homepage
+ And I follow "Log in"
+ And I follow "New Account"
+ When I set the following fields to these values:
+ | Username | mrwhitespace |
+ | Password | Gue$$m3ifY0uC&n |
+ | Email address | mrwhitespace@nas.ty |
+ | Email (again) | mrwhitespace@nas.ty |
+ And I set the field "First name" to " "
+ And I set the field "Surname" to " "
+ And I click on "Create my new account" "button"
+ Then I should see "Missing given name"
+ And I should see "Missing surname"
+
+ Scenario: Attempting to change own names to whitespace
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | foobar | Foo | Bar | foo@bar.com |
+ And I log in as "foobar"
+ And I follow "Profile" in the user menu
+ And I follow "Edit profile"
+ When I set the field "First name" to " "
+ And I set the field "Surname" to " "
+ And I click on "Update profile" "button"
+ Then I should see "Missing given name"
+ And I should see "Missing surname"
+
+ Scenario: Attempting to change someone else's names to whitespace
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | foobar | Foo | Bar | foo@bar.com |
+ And I log in as "admin"
+ And I navigate to "Browse list of users" node in "Site administration > Users > Accounts"
+ And I follow "Foo Bar"
+ And I follow "Edit profile"
+ When I set the field "First name" to " "
+ And I set the field "Surname" to " "
+ And I click on "Update profile" "button"
+ Then I should see "Missing given name"
+ And I should see "Missing surname"
defined('MOODLE_INTERNAL') || die();
-$version = 2016012100.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2016012900.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.1dev (Build: 20160121)'; // Human-friendly version name
+$release = '3.1dev (Build: 20160129)'; // Human-friendly version name
$branch = '31'; // This version's branch.
$maturity = MATURITY_ALPHA; // This version's maturity level.