Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorDavid Monllao <davidm@moodle.com>
Fri, 29 Jan 2016 06:36:47 +0000 (14:36 +0800)
committerDavid Monllao <davidm@moodle.com>
Fri, 29 Jan 2016 06:36:47 +0000 (14:36 +0800)
65 files changed:
blocks/navigation/amd/build/ajax_response_renderer.min.js [new file with mode: 0644]
blocks/navigation/amd/build/nav_loader.min.js [new file with mode: 0644]
blocks/navigation/amd/build/navblock.min.js [new file with mode: 0644]
blocks/navigation/amd/build/site_admin_loader.min.js [new file with mode: 0644]
blocks/navigation/amd/src/ajax_response_renderer.js [new file with mode: 0644]
blocks/navigation/amd/src/nav_loader.js [new file with mode: 0644]
blocks/navigation/amd/src/navblock.js [new file with mode: 0644]
blocks/navigation/amd/src/site_admin_loader.js [new file with mode: 0644]
blocks/navigation/block_navigation.php
blocks/navigation/renderer.php
blocks/navigation/styles.css
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js [deleted file]
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js [deleted file]
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js [deleted file]
blocks/navigation/yui/src/navigation/build.json [deleted file]
blocks/navigation/yui/src/navigation/js/navigation.js [deleted file]
blocks/navigation/yui/src/navigation/meta/navigation.json [deleted file]
blocks/settings/amd/build/settingsblock.min.js [new file with mode: 0644]
blocks/settings/amd/src/settingsblock.js [new file with mode: 0644]
blocks/settings/block_settings.php
blocks/settings/renderer.php
blocks/settings/styles.css
blocks/tests/behat/configure_block_throughout_site.feature
blocks/upgrade.txt
blog/external_blogs.php
completion/criteria/completion_criteria_grade.php
completion/tests/behat/behat_completion.php
course/tests/behat/behat_course.php
lib/amd/build/tree.min.js [new file with mode: 0644]
lib/amd/src/tree.js [new file with mode: 0644]
lib/blocklib.php
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/selection.js
lib/form/advcheckbox.php
lib/formslib.php
lib/moodlelib.php
lib/navigationlib.php
lib/pear/HTML/QuickForm.php
lib/pear/HTML/QuickForm/DHTMLRulesTableless.php
lib/pear/HTML/QuickForm/Renderer/Tableless.php
lib/pear/README_MOODLE.txt
lib/testing/classes/util.php
lib/tests/behat/behat_navigation.php
lib/tests/moodlelib_test.php
lib/upgrade.txt
login/signup_form.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/choice/lang/en/choice.php
mod/data/js.php
mod/forum/markposts.php
mod/quiz/classes/structure.php
mod/scorm/locallib.php
mod/scorm/tests/lib_test.php
mod/scorm/version.php
mod/wiki/parser/markups/wikimarkup.php
mod/wiki/tests/wikiparser_test.php
mod/workshop/classes/event/submission_deleted.php [new file with mode: 0644]
mod/workshop/db/access.php
mod/workshop/lang/en/workshop.php
mod/workshop/submission.php
mod/workshop/tests/behat/delete_submission.feature [new file with mode: 0644]
mod/workshop/version.php
version.php

diff --git a/blocks/navigation/amd/build/ajax_response_renderer.min.js b/blocks/navigation/amd/build/ajax_response_renderer.min.js
new file mode 100644 (file)
index 0000000..b7cfef8
Binary files /dev/null and b/blocks/navigation/amd/build/ajax_response_renderer.min.js differ
diff --git a/blocks/navigation/amd/build/nav_loader.min.js b/blocks/navigation/amd/build/nav_loader.min.js
new file mode 100644 (file)
index 0000000..183a19f
Binary files /dev/null and b/blocks/navigation/amd/build/nav_loader.min.js differ
diff --git a/blocks/navigation/amd/build/navblock.min.js b/blocks/navigation/amd/build/navblock.min.js
new file mode 100644 (file)
index 0000000..5a5d7b9
Binary files /dev/null and b/blocks/navigation/amd/build/navblock.min.js differ
diff --git a/blocks/navigation/amd/build/site_admin_loader.min.js b/blocks/navigation/amd/build/site_admin_loader.min.js
new file mode 100644 (file)
index 0000000..9bd76cb
Binary files /dev/null and b/blocks/navigation/amd/build/site_admin_loader.min.js differ
diff --git a/blocks/navigation/amd/src/ajax_response_renderer.js b/blocks/navigation/amd/src/ajax_response_renderer.js
new file mode 100644 (file)
index 0000000..33f508c
--- /dev/null
@@ -0,0 +1,146 @@
+// 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');
+                }
+            }
+        }
+    };
+});
diff --git a/blocks/navigation/amd/src/nav_loader.js b/blocks/navigation/amd/src/nav_loader.js
new file mode 100644 (file)
index 0000000..2cb5137
--- /dev/null
@@ -0,0 +1,64 @@
+// 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;
+        }
+    };
+});
diff --git a/blocks/navigation/amd/src/navblock.js b/blocks/navigation/amd/src/navblock.js
new file mode 100644 (file)
index 0000000..14b14bb
--- /dev/null
@@ -0,0 +1,30 @@
+// 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");
+        }
+    };
+});
diff --git a/blocks/navigation/amd/src/site_admin_loader.js b/blocks/navigation/amd/src/site_admin_loader.js
new file mode 100644 (file)
index 0000000..b203aac
--- /dev/null
@@ -0,0 +1,52 @@
+// 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;
+        }
+    };
+});
index 17a2509..31a3fc8 100644 (file)
@@ -107,25 +107,9 @@ class block_navigation extends block_base {
      * 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());
     }
 
     /**
@@ -134,6 +118,7 @@ class block_navigation extends block_base {
      * @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;
@@ -196,7 +181,21 @@ class block_navigation extends block_base {
             }
         }
 
-        $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');
index 2a92c9f..131c559 100644 (file)
@@ -42,7 +42,11 @@ class block_navigation_renderer extends plugin_renderer_base {
      */
     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);
         }
@@ -66,7 +70,9 @@ class block_navigation_renderer extends plugin_renderer_base {
 
         // Turn our navigation items into list items.
         $lis = array();
+        $number = 0;
         foreach ($items as $item) {
+            $number++;
             if (!$item->display && !$item->contains_active_node()) {
                 continue;
             }
@@ -100,7 +106,8 @@ class block_navigation_renderer extends plugin_renderer_base {
                 continue;
             }
 
-            $attributes = array();
+            $nodetextid = 'label_' . $depth . '_' . $number;
+            $attributes = array('tabindex' => '-1', 'id' => $nodetextid);
             if ($title !== '') {
                 $attributes['title'] = $title;
             }
@@ -110,7 +117,6 @@ class block_navigation_renderer extends plugin_renderer_base {
             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
@@ -129,12 +135,28 @@ class block_navigation_renderer extends plugin_renderer_base {
             $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';
@@ -152,7 +174,7 @@ class block_navigation_renderer extends plugin_renderer_base {
             }
 
             // 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;
@@ -161,11 +183,16 @@ class block_navigation_renderer extends plugin_renderer_base {
             // 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;
         }
index c35c764..e4e0392 100644 (file)
     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]]');
+}
diff --git a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js
deleted file mode 100644 (file)
index 2d58702..0000000
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js and /dev/null differ
diff --git a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js
deleted file mode 100644 (file)
index 7c67d98..0000000
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js and /dev/null differ
diff --git a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js
deleted file mode 100644 (file)
index b661bfd..0000000
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js and /dev/null differ
diff --git a/blocks/navigation/yui/src/navigation/build.json b/blocks/navigation/yui/src/navigation/build.json
deleted file mode 100644 (file)
index 5e28a6c..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "name": "moodle-block_navigation-navigation",
-  "builds": {
-    "moodle-block_navigation-navigation": {
-      "jsfiles": [
-        "navigation.js"
-      ]
-    }
-  }
-}
diff --git a/blocks/navigation/yui/src/navigation/js/navigation.js b/blocks/navigation/yui/src/navigation/js/navigation.js
deleted file mode 100644 (file)
index 3678bff..0000000
+++ /dev/null
@@ -1,897 +0,0 @@
-/**
- * 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
-        }
-    }
-});
diff --git a/blocks/navigation/yui/src/navigation/meta/navigation.json b/blocks/navigation/yui/src/navigation/meta/navigation.json
deleted file mode 100644 (file)
index c544b51..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "moodle-block_navigation-navigation": {
-    "requires": [
-        "base",
-        "io-base",
-        "node",
-        "event-synthetic",
-        "event-delegate",
-        "json-parse"
-    ]
-  }
-}
diff --git a/blocks/settings/amd/build/settingsblock.min.js b/blocks/settings/amd/build/settingsblock.min.js
new file mode 100644 (file)
index 0000000..91aa6a5
Binary files /dev/null and b/blocks/settings/amd/build/settingsblock.min.js differ
diff --git a/blocks/settings/amd/src/settingsblock.js b/blocks/settings/amd/src/settingsblock.js
new file mode 100644 (file)
index 0000000..965aec5
--- /dev/null
@@ -0,0 +1,35 @@
+// 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>');
+            }
+        }
+    };
+});
index 14c486b..beaa87f 100644 (file)
@@ -90,13 +90,19 @@ class block_settings extends block_base {
     }
 
     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));
     }
 
     /**
index 4de8b83..d5572e8 100644 (file)
@@ -32,14 +32,26 @@ class block_settings_renderer extends plugin_renderer_base {
                 $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
@@ -49,7 +61,9 @@ class block_settings_renderer extends plugin_renderer_base {
 
         // array of nested li elements
         $lis = array();
+        $number = 0;
         foreach ($items as $item) {
+            $number++;
             if (!$item->display) {
                 continue;
             }
@@ -65,19 +79,27 @@ class block_settings_renderer extends plugin_renderer_base {
             // 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) {
@@ -92,15 +114,19 @@ class block_settings_renderer extends plugin_renderer_base {
             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 '';
index bc057b5..6cb127c 100644 (file)
 .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%;}
@@ -31,5 +36,8 @@
 .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]]);}
index 95e1705..bf2e8b0 100644 (file)
@@ -48,11 +48,11 @@ Feature: Add and configure blocks throughout the site
     # 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"
index 34305e1..cdda9f3 100644 (file)
@@ -1,6 +1,12 @@
 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)
index 6595848..89d9acb 100644 (file)
@@ -95,13 +95,15 @@ if (!empty($blogs)) {
         $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 . '&nbsp'. $deleteicon));
+                                                  $editicon . $deleteicon));
     }
     echo html_writer::table($table);
 }
index e43d324..99ad750 100644 (file)
@@ -65,11 +65,13 @@ class completion_criteria_grade extends completion_criteria {
         $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));
         }
     }
 
@@ -141,7 +143,10 @@ class completion_criteria_grade extends completion_criteria {
      * @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);
     }
 
@@ -161,11 +166,15 @@ class completion_criteria_grade extends completion_criteria {
      * @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');
         }
@@ -235,15 +244,19 @@ class completion_criteria_grade extends completion_criteria {
      *     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;
index 3762290..f1b4215 100644 (file)
@@ -88,22 +88,10 @@ class behat_completion extends behat_base {
      * @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 . '"');
     }
 
     /**
index ffe7470..e0ba5c1 100644 (file)
@@ -215,6 +215,9 @@ class behat_course extends behat_base {
             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')]";
@@ -300,6 +303,11 @@ class behat_course extends behat_base {
      */
     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()) {
@@ -316,6 +324,11 @@ class behat_course extends behat_base {
      */
     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()) {
@@ -964,11 +977,6 @@ class behat_course extends behat_base {
         // 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]";
@@ -996,11 +1004,6 @@ class behat_course extends behat_base {
         // 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]";
diff --git a/lib/amd/build/tree.min.js b/lib/amd/build/tree.min.js
new file mode 100644 (file)
index 0000000..837d4dc
Binary files /dev/null and b/lib/amd/build/tree.min.js differ
diff --git a/lib/amd/src/tree.js b/lib/amd/src/tree.js
new file mode 100644 (file)
index 0000000..195258f
--- /dev/null
@@ -0,0 +1,496 @@
+// 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;
+});
index b045eb2..595cf3b 100644 (file)
@@ -1093,38 +1093,36 @@ class block_manager {
         }
 
         // 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)) {
index e089c1c..09ab53f 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index f68b5f1..b6ff702 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 1a8ea70..bbc68c7 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 12086a3..e90b30a 100644 (file)
@@ -64,6 +64,16 @@ EditorSelection.prototype = {
      */
     _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.
      *
@@ -94,13 +104,21 @@ EditorSelection.prototype = {
                 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;
     },
index ff679d4..0d1c3cc 100644 (file)
@@ -61,7 +61,7 @@ class MoodleQuickForm_advcheckbox extends HTML_QuickForm_advcheckbox{
             $values = array(0, 1);
         }
 
-        if (!is_null($attributes['group'])) {
+        if (!empty($attributes['group'])) {
 
             $this->_group = 'checkboxgroup' . $attributes['group'];
             unset($attributes['group']);
index 51fc83c..a616133 100644 (file)
@@ -1426,6 +1426,12 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
      */
     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
      *
@@ -2026,7 +2032,7 @@ class MoodleQuickForm extends 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;
         }
 
     }
@@ -2055,16 +2061,14 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
              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;
             }
         }
     }
@@ -2080,7 +2084,7 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
      */
     function getValidationScript()
     {
-        if (empty($this->_rules) || empty($this->_attributes['onsubmit'])) {
+        if (empty($this->_rules) || $this->clientvalidation === false) {
             return '';
         }
 
@@ -2164,60 +2168,62 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
 
 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
@@ -2228,64 +2234,86 @@ function qf_errorHandler(element, _qfMsg, escapedName) {
                 '/[_\[\]-]/',
                 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;
index 813d94b..5e04581 100644 (file)
@@ -5589,6 +5589,25 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '',
         }
     }
 
+    // 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;
     }
@@ -7530,7 +7549,7 @@ function count_words($string) {
     // 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);
 
index 3bf5cc3..22ae98d 100644 (file)
@@ -141,6 +141,8 @@ class navigation_node implements renderable {
     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
@@ -612,6 +614,7 @@ class navigation_node implements renderable {
             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);
@@ -3549,7 +3552,7 @@ class settings_navigation extends navigation_node {
                 }
                 $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';
             }
         }
 
@@ -4950,6 +4953,7 @@ class navigation_json {
         $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(
index bf8062e..c53ab75 100644 (file)
@@ -1061,9 +1061,6 @@ class HTML_QuickForm extends HTML_Common {
         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,
@@ -1132,9 +1129,6 @@ class HTML_QuickForm extends HTML_Common {
                         $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) {
@@ -1166,9 +1160,6 @@ class HTML_QuickForm extends HTML_Common {
             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
 
index cf89ead..cc0e4ee 100644 (file)
@@ -198,11 +198,6 @@ function validate_' . $this->_attributes['id'] . '(frm) {
     } // end func getValidationScript
 
     // }}}
-
-    function display() {
-        $this->getValidationScript();
-        return parent::display();
-    }
 }
 
-?>
\ No newline at end of file
+?>
index 4ef5a33..5b444ac 100644 (file)
@@ -267,7 +267,7 @@ class HTML_QuickForm_Renderer_Tableless extends HTML_QuickForm_Renderer_Default
         $this->_html = str_replace('></label>', '>&nbsp;</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
 
index a427440..02eb5b7 100644 (file)
@@ -28,6 +28,8 @@ MDL-20876 - replaced split() with explode() or preg_split() where appropriate
 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
index 1017b1b..c03c685 100644 (file)
@@ -685,7 +685,11 @@ abstract class testing_util {
             }
 
             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;
                 }
 
@@ -707,9 +711,8 @@ abstract class testing_util {
             }
 
             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;
index 4d96310..f594740 100644 (file)
@@ -65,8 +65,8 @@ class behat_navigation extends behat_base {
         $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.
@@ -79,12 +79,15 @@ class behat_navigation extends behat_base {
             $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);
@@ -108,10 +111,11 @@ class behat_navigation extends behat_base {
 
         $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;
     }
 
     /**
@@ -131,10 +135,11 @@ class behat_navigation extends behat_base {
 
         $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());
     }
 
     /**
@@ -257,7 +262,9 @@ class behat_navigation extends behat_base {
             }
 
             // 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);
 
@@ -265,9 +272,10 @@ class behat_navigation extends behat_base {
                 $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);
                 }
@@ -320,8 +328,14 @@ class behat_navigation extends behat_base {
             "//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;
     }
index c976de3..229e918 100644 (file)
@@ -2846,7 +2846,7 @@ class core_moodlelib_testcase extends advanced_testcase {
         $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);
index c4e9f6f..255b764 100644 (file)
@@ -37,6 +37,8 @@ information provided here is intended especially for developers.
 * 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 ===
 
index 2d43f70..a463e9d 100644 (file)
@@ -63,7 +63,7 @@ class login_signup_form extends moodleform {
         $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';
index c1dfa7a..36e3765 100644 (file)
@@ -60,7 +60,7 @@ Feature: In an assignment, teacher can annotate PDF files during grading
     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"
index 48553ed..4f5bbc6 100644 (file)
@@ -86,8 +86,8 @@ $string['noresultsviewable'] = 'The results are not currently viewable.';
 $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';
index f76349d..96e4855 100644 (file)
@@ -38,7 +38,7 @@ if ($data = $DB->get_record('data', array('id'=>$d))) {
     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
index 382b41f..e6b9442 100644 (file)
@@ -57,9 +57,9 @@ $user = $USER;
 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
index a02fabe..36c3499 100644 (file)
@@ -181,7 +181,7 @@ class structure {
             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);
         }
@@ -190,14 +190,19 @@ class structure {
             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;
+        }
     }
 
     /**
index 17c662e..b948906 100644 (file)
@@ -237,12 +237,12 @@ function scorm_parse($scorm, $full) {
                 // 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;
                 }
@@ -812,8 +812,9 @@ function scorm_get_last_completed_attempt($scormid, $userid) {
     $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 {
index 31f4db0..e5db5f1 100644 (file)
@@ -181,4 +181,12 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         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));
+    }
 }
index 5175760..379070d 100644 (file)
@@ -24,7 +24,7 @@
 
 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;
index 4f5135d..3e1efc0 100644 (file)
@@ -237,7 +237,9 @@ abstract class wiki_markup_parser extends generic_parser {
                     $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++;
         }
 
index 8f74d12..678b53a 100644 (file)
@@ -128,6 +128,23 @@ class mod_wiki_wikiparser_test extends basic_testcase {
         $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.
 
diff --git a/mod/workshop/classes/event/submission_deleted.php b/mod/workshop/classes/event/submission_deleted.php
new file mode 100644 (file)
index 0000000..69affe6
--- /dev/null
@@ -0,0 +1,111 @@
+<?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;
+    }
+}
index e3e42c4..dc7cec4 100644 (file)
@@ -233,4 +233,15 @@ $capabilities = array(
             '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
+        )
+    ),
 );
index e327aab..cd1170d 100644 (file)
@@ -92,6 +92,7 @@ $string['daystoday'] = 'today';
 $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';
@@ -113,6 +114,7 @@ $string['eventsubmissionassessmentsreset'] = 'Submission assessments cleared';
 $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';
@@ -255,6 +257,8 @@ $string['submission'] = '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)';
@@ -316,6 +320,7 @@ $string['withoutsubmission'] = 'Reviewer without own submission';
 $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';
index c889f87..0299712 100644 (file)
@@ -27,12 +27,14 @@ require_once(dirname(dirname(dirname(__FILE__))).'/config.php');
 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);
@@ -85,9 +87,11 @@ $cansubmit      = has_capability('mod/workshop:submit', $workshop->context);
 $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));
@@ -127,6 +131,36 @@ if ($editable and $workshop->useexamples and $workshop->examplesmode == workshop
 }
 $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)) {
@@ -313,6 +347,18 @@ if ($edit) {
     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) {
@@ -326,20 +372,28 @@ 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)) {
diff --git a/mod/workshop/tests/behat/delete_submission.feature b/mod/workshop/tests/behat/delete_submission.feature
new file mode 100644 (file)
index 0000000..a23c1df
--- /dev/null
@@ -0,0 +1,106 @@
+@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
index 62250f2..5f8e196 100644 (file)
@@ -24,7 +24,7 @@
 
 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.
index d40392a..9ab10d4 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016012100.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016012100.02;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.