Merge branch 'MDL-49817-master' of git://github.com/danpoltawski/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 2 Feb 2016 05:57:43 +0000 (13:57 +0800)
committerDavid Monllao <davidm@moodle.com>
Tue, 2 Feb 2016 05:57:43 +0000 (13:57 +0800)
101 files changed:
backup/backup.php
backup/restore.php
blocks/activity_results/tests/behat/addunsupportedactivity.feature
blocks/activity_results/tests/behat/highscoreswithscales.feature
blocks/activity_results/tests/behat/highscoreswithscalesandgroups.feature
blocks/activity_results/tests/behat/lowscoreswithscales.feature
blocks/activity_results/tests/behat/lowscoreswithscalesandgroups.feature
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
install/lang/he/error.php
install/lang/he/install.php
install/lang/or/langconfig.php [new file with mode: 0644]
install/lang/ro/install.php
install/lang/th/moodle.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/classes/task/delete_incomplete_users_task.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/filelib.php
lib/form/advcheckbox.php
lib/form/modgrade.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_hooks.php
lib/tests/behat/behat_navigation.php
lib/tests/formslib_test.php
lib/tests/moodlelib_test.php
lib/upgrade.txt
login/signup_form.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-debug.js
mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-min.js
mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history.js
mod/assign/yui/src/history/js/history.js
mod/choice/classes/event/answer_deleted.php [new file with mode: 0644]
mod/choice/classes/event/report_downloaded.php [new file with mode: 0644]
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/report.php
mod/choice/tests/events_test.php
mod/choice/version.php
mod/choice/view.php
mod/data/js.php
mod/forum/markposts.php
mod/forum/templates/forum_post_email_htmlemail.mustache
mod/forum/templates/forum_post_email_htmlemail_body.mustache [new file with mode: 0644]
mod/forum/templates/forum_post_emaildigestfull_htmlemail.mustache
mod/quiz/classes/structure.php
mod/scorm/locallib.php
mod/scorm/tests/lib_test.php
mod/scorm/version.php
mod/survey/report.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
user/edit_form.php
user/editadvanced_form.php
user/editlib.php
user/tests/behat/name_fields.feature [new file with mode: 0644]
version.php

index 688726d..4e8ba89 100644 (file)
@@ -138,7 +138,9 @@ if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
     }
 
     // Get HTML from logger.
-    $loghtml = $logger->get_html();
+    if ($CFG->debugdisplay) {
+        $loghtml = $logger->get_html();
+    }
 
     // Hide the progress display and first backup step bar (the 'finished' step will show next).
     echo html_writer::end_div();
index 25a216c..ab97369 100644 (file)
@@ -110,7 +110,9 @@ if (!$restore->is_independent()) {
             // Do actual restore.
             $restore->execute();
             // Get HTML from logger.
-            $loghtml = $logger->get_html();
+            if ($CFG->debugdisplay) {
+                $loghtml = $logger->get_html();
+            }
             // Hide this section because we are now going to make the page show 'finished'.
             echo html_writer::end_div();
             echo html_writer::script('document.getElementById("executionprogress").style.display = "none";');
index 111c96f..012549e 100644 (file)
@@ -35,6 +35,6 @@ Feature: The activity results block displays student scores
     When I follow "Test assignment"
     And I click on "Edit settings" "link" in the "Administration" "block"
     And I set the following fields to these values:
-      | id_modgrade_type | None |
+      | id_grade_modgrade_type | None |
     And I press "Save and return to course"
     Then I should see "Error: the activity selected uses a grading method that is not supported by this block." in the "Activity results" "block"
index 1263382..b5e52f8 100644 (file)
@@ -39,8 +39,8 @@ Feature: The activity results block displays student scores as scales
       | Assignment name | Test assignment |
       | Description | Offline text |
       | assignsubmission_file_enabled | 0 |
-      | id_modgrade_type | Scale |
-      | id_modgrade_scale | My Scale |
+      | id_grade_modgrade_type | Scale |
+      | id_grade_modgrade_scale | My Scale |
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I turn editing mode on
index 7856da5..d546fda 100644 (file)
@@ -56,8 +56,8 @@ Feature: The activity results block displays student scores as scales
       | Assignment name | Test assignment |
       | Description | Offline text |
       | assignsubmission_file_enabled | 0 |
-      | id_modgrade_type | Scale |
-      | id_modgrade_scale | My Scale |
+      | id_grade_modgrade_type | Scale |
+      | id_grade_modgrade_scale | My Scale |
       | Group mode | Separate groups |
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
index 149e0f6..00f31f6 100644 (file)
@@ -39,8 +39,8 @@ Feature: The activity results block displays student scores as scales
       | Assignment name | Test assignment |
       | Description | Offline text |
       | assignsubmission_file_enabled | 0 |
-      | id_modgrade_type | Scale |
-      | id_modgrade_scale | My Scale |
+      | id_grade_modgrade_type | Scale |
+      | id_grade_modgrade_scale | My Scale |
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I turn editing mode on
index 9134339..be5ae33 100644 (file)
@@ -56,8 +56,8 @@ Feature: The activity results block displays student scores as scales
       | Assignment name | Test assignment |
       | Description | Offline text |
       | assignsubmission_file_enabled | 0 |
-      | id_modgrade_type | Scale |
-      | id_modgrade_scale | My Scale |
+      | id_grade_modgrade_type | Scale |
+      | id_grade_modgrade_scale | My Scale |
       | Group mode | Separate groups |
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
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]";
index 6a2e548..f7d304a 100644 (file)
@@ -35,8 +35,8 @@ $string['cannotcreatetempdir'] = 'לא ניתן ליצור סיפרייה זמנ
 $string['cannotdownloadcomponents'] = 'לא ניתן להוריד רכיבים.';
 $string['cannotdownloadzipfile'] = 'לא ניתן להוריד קובץ ZIP.';
 $string['cannotfindcomponent'] = 'הרכיב לא נמצא.';
-$string['cannotsavemd5file'] = 'לא ניתן לשמור קובץ md5.';
-$string['cannotsavezipfile'] = 'לא ניתן לשמור קובץ ZIP.';
+$string['cannotsavemd5file'] = 'לא ניתן לשמור קובץ md5';
+$string['cannotsavezipfile'] = 'לא ניתן לשמור קובץ ZIP';
 $string['cannotunzipfile'] = 'לא ניתן לפתוח את קובץ ה-ZIP.';
 $string['componentisuptodate'] = 'הרכיב מעודכן.';
 $string['downloadedfilecheckfailed'] = 'נכשלה בדיקת הקובץ המורד.';
index 6b87350..8dfd3a2 100644 (file)
@@ -104,7 +104,7 @@ $string['welcomep50'] = 'השימוש בכל היישומים בחבילה זו
 <a href="http://www.opensource.org/docs/definition_plain.html">קוד פתוח</a>
 והיא מופצת תחת רשיון
 <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>';
-$string['welcomep60'] = '×\94×¢×\9e×\95×\93×\99×\9d ×\94×\91×\90×\99×\9d ×\99×\95×\91×\99×\9c×\95 ×\90×\95ת×\9a ×\91צ×\95ר×\94 ×¤×©×\95×\98×\94 ×\93ר×\9a ×\9b×\9e×\94 ×¦×¢×\93×\99×\9d ×\9c×¢×\99צ×\95×\91 ×\94×\92×\93ר×\95ת <strong>Moodle</strong> ×\91×\9e×\97ש×\91×\9a.
ª×\95×\9b×\9c ×\9c×\90שר ×\90ת ×\94×\92×\93ר×\95ת  ×\91ר×\99רת ×\94×\9e×\97×\93×\9c ×\90×\95, ×\91×\90פשר×\95ת×\9a, לשנותם לפי צרכיך.';
+$string['welcomep60'] = '×\94×¢×\9e×\95×\93×\99×\9d ×\94×\91×\90×\99×\9d ×\99×\95×\91×\99×\9c×\95 ×\90×\95ת×\9a ×\91צ×\95ר×\94 ×¤×©×\95×\98×\94 ×\93ר×\9a ×\9b×\9e×\94 ×¦×¢×\93×\99×\9d ×\9cק×\91×\99עת ×\94×\92×\93ר×\95ת <strong>Moodle</strong> ×\91שרת.
 ×\99ת×\9f ×\9c×\90שר ×\94×\92×\93ר×\95ת ×\91ררת־×\94×\9e×\97×\93×\9c ×\90×\95 לשנותם לפי צרכיך.';
 $string['welcomep70'] = 'הקש על לחצן ה"המשך" למטה כדי להמשיך עם הגדרת ה-<strong>Moodle</strong>';
 $string['wwwroot'] = 'כתובת האתר';
diff --git a/install/lang/or/langconfig.php b/install/lang/or/langconfig.php
new file mode 100644 (file)
index 0000000..15cbb4d
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thisdirection'] = 'ltr';
+$string['thislanguage'] = 'ଓଡ଼ିଆ';
index ec01ae5..286a072 100644 (file)
@@ -45,6 +45,9 @@ $string['environmenthead'] = 'Se verifică mediul...';
 $string['installation'] = 'Instalare';
 $string['paths'] = 'Căi';
 $string['pathshead'] = 'Confirmare căi';
+$string['pathssubdirroot'] = '<p>Calea completă către directorul care conține codul Moodle .</p>';
+$string['pathsunsecuredataroot'] = 'Locația dataroot nu este sigură';
+$string['pathswrongadmindir'] = 'Directorul admin nu există';
 $string['phpextension'] = 'extensie PHP {$a}';
 $string['phpversion'] = 'Versiune PHP';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
index ea86f2c..df349d1 100644 (file)
@@ -31,5 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'ภาษาที่ใช้ในเว็บ';
+$string['moodlelogo'] = 'โลโก้ Moodle';
 $string['next'] = 'ต่อไป';
 $string['previous'] = 'หน้าก่อน';
+$string['reload'] = 'โหลดใหม่';
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 0fb84b8..dda16f7 100644 (file)
@@ -59,6 +59,10 @@ class delete_incomplete_users_task extends scheduled_task {
                 if (isguestuser($user) or is_siteadmin($user)) {
                     continue;
                 }
+                if ($user->lastname !== '' and $user->firstname !== '' and $user->email !== '') {
+                    // This can happen on MySQL - see MDL-52831.
+                    continue;
+                }
                 delete_user($user);
                 mtrace(" Deleted not fully setup user $user->username ($user->id)");
             }
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 4da13f2..4b1e7e8 100644 (file)
@@ -4506,6 +4506,14 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
+            if ($context->get_course_context(false)) {
+                // If block is in course context, then check if user has capability to access course.
+                require_course_login($course);
+            } else if ($CFG->forcelogin) {
+                // If user is logged out, bp record will not be visible, even if the user would have access if logged in.
+                require_login();
+            }
+
             $bprecord = $DB->get_record('block_positions', array('contextid' => $context->id, 'blockinstanceid' => $context->instanceid));
             // User can't access file, if block is hidden or doesn't have block:view capability
             if (($bprecord && !$bprecord->visible) || !has_capability('moodle/block:view', $context)) {
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 dd8ccce..73394f4 100644 (file)
@@ -91,15 +91,15 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group{
         $langscale = get_string('modgradetypescale', 'grades');
         $scaleselect = @MoodleQuickForm::createElement('select', 'modgrade_scale', $langscale, $scales, $attributes);
         $scaleselect->setHiddenLabel = false;
-        $scaleselect->_generateId();
-        $scaleselectid = $scaleselect->getAttribute('id');
+        $scaleselectid = $this->generate_modgrade_subelement_id('modgrade_scale');
+        $scaleselect->updateAttributes(array('id' => $scaleselectid));
 
         // Maximum grade textbox.
         $langmaxgrade = get_string('modgrademaxgrade', 'grades');
         $maxgrade = @MoodleQuickForm::createElement('text', 'modgrade_point', $langmaxgrade, array());
         $maxgrade->setHiddenLabel = false;
-        $maxgrade->_generateId();
-        $maxgradeid = $maxgrade->getAttribute('id');
+        $maxgradeid = $this->generate_modgrade_subelement_id('modgrade_point');
+        $maxgrade->updateAttributes(array('id' => $maxgradeid));
 
         // Grade type select box.
         $gradetype = array(
@@ -110,7 +110,8 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group{
         $langtype = get_string('modgradetype', 'grades');
         $typeselect = @MoodleQuickForm::createElement('select', 'modgrade_type', $langtype, $gradetype, $attributes, true);
         $typeselect->setHiddenLabel = false;
-        $typeselect->_generateId();
+        $typeselectid = $this->generate_modgrade_subelement_id('modgrade_type');
+        $typeselect->updateAttributes(array('id' => $typeselectid));
 
         // Add elements.
 
@@ -315,4 +316,17 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group{
         return parent::onQuickFormEvent($event, $arg, $caller);
     }
 
+    /**
+     * Generates the id attribute for the subelement of the modgrade group.
+     *
+     * Uses algorithm similar to what {@link HTML_QuickForm_element::_generateId()}
+     * does but takes the name of the wrapping modgrade group into account.
+     *
+     * @param string $subname the name of the HTML_QuickForm_element in this modgrade group
+     * @return string
+     */
+    protected function generate_modgrade_subelement_id($subname) {
+        $gid = str_replace(array('[', ']'), array('_', ''), $this->getName());
+        return clean_param('id_'.$gid.'_'.$subname, PARAM_ALPHANUMEXT);
+    }
 }
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 78ea809..cddbe93 100644 (file)
@@ -375,13 +375,22 @@ class behat_hooks extends behat_base {
      * @AfterStep
      */
     public function after_step(StepEvent $event) {
-        global $CFG;
+        global $CFG, $DB;
 
         // Save the page content if the step failed.
         if (!empty($CFG->behat_faildump_path) &&
                 $event->getResult() === StepEvent::FAILED) {
             $this->take_contentdump($event);
         }
+
+        // Abort any open transactions to prevent subsequent tests hanging.
+        // This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't
+        // want to see a message in the behat output.
+        if ($event->hasException()) {
+            if ($DB && $DB->is_transaction_started()) {
+                $DB->force_transaction_rollback();
+            }
+        }
     }
 
     /**
@@ -393,7 +402,17 @@ class behat_hooks extends behat_base {
      * @AfterScenario @_switch_window
      */
     public function after_scenario_switchwindow(ScenarioEvent $event) {
-        $this->getSession()->restart();
+        for ($count = 0; $count < self::EXTENDED_TIMEOUT; $count) {
+            try {
+                $this->getSession()->restart();
+                break;
+            } catch (DriverException $e) {
+                // Wait for timeout and try again.
+                sleep(self::TIMEOUT);
+            }
+        }
+        // If session is not restarted above then it will try to start session before next scenario
+        // and if that fails then exception will be thrown.
     }
 
     /**
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 18e5edc..cfe314c 100644 (file)
@@ -550,6 +550,33 @@ class core_formslib_testcase extends advanced_testcase {
         $data = $mform->get_data();
         $this->assertSame($expectedvalues, (array) $data);
     }
+
+    /**
+     * MDL-52873
+     */
+    public function test_multiple_modgrade_fields() {
+        $this->resetAfterTest(true);
+
+        $form = new formslib_multiple_modgrade_form();
+        ob_start();
+        $form->display();
+        $html = ob_get_clean();
+
+        $this->assertTag(array('id' => 'fgroup_id_grade1'), $html);
+        $this->assertTag(array('id' => 'id_grade1_modgrade_type'), $html);
+        $this->assertTag(array('id' => 'id_grade1_modgrade_point'), $html);
+        $this->assertTag(array('id' => 'id_grade1_modgrade_scale'), $html);
+
+        $this->assertTag(array('id' => 'fgroup_id_grade2'), $html);
+        $this->assertTag(array('id' => 'id_grade2_modgrade_type'), $html);
+        $this->assertTag(array('id' => 'id_grade2_modgrade_point'), $html);
+        $this->assertTag(array('id' => 'id_grade2_modgrade_scale'), $html);
+
+        $this->assertTag(array('id' => 'fgroup_id_grade_3'), $html);
+        $this->assertTag(array('id' => 'id_grade_3_modgrade_type'), $html);
+        $this->assertTag(array('id' => 'id_grade_3_modgrade_point'), $html);
+        $this->assertTag(array('id' => 'id_grade_3_modgrade_scale'), $html);
+    }
 }
 
 
@@ -822,3 +849,15 @@ class formslib_clean_value extends moodleform {
             'repeatnamedgroup[repeatnamedgroupel2]' => array('type' => PARAM_INT)), 'repeatablenamedgroup', 'add', 0);
     }
 }
+
+/**
+ * Used to test that modgrade fields get unique id attributes.
+ */
+class formslib_multiple_modgrade_form extends moodleform {
+    public function definition() {
+        $mform = $this->_form;
+        $mform->addElement('modgrade', 'grade1', 'Grade 1');
+        $mform->addElement('modgrade', 'grade2', 'Grade 2');
+        $mform->addElement('modgrade', 'grade[3]', 'Grade 3');
+    }
+}
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..45966f6 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';
@@ -112,6 +112,11 @@ class login_signup_form extends moodleform {
     function definition_after_data(){
         $mform = $this->_form;
         $mform->applyFilter('username', 'trim');
+
+        // Trim required name fields.
+        foreach (useredit_get_required_name_fields() as $field) {
+            $mform->applyFilter($field, 'trim');
+        }
     }
 
     function validation($data, $files) {
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 73c9357..c3f098f 100644 (file)
Binary files a/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-debug.js and b/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-debug.js differ
index 81dd1a9..9004c3b 100644 (file)
Binary files a/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-min.js and b/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-min.js differ
index 73c9357..c3f098f 100644 (file)
Binary files a/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history.js and b/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history.js differ
index 9dbafaf..a30c3a0 100644 (file)
@@ -48,9 +48,17 @@ var CSS = {
                 panel.set('aria-live', 'polite');
 
                 wrapper.addClass(CSS.LINK);
-                wrapper.addClass(CSS.CLOSED);
+                if (COUNT == 1) {
+                    wrapper.addClass(CSS.OPEN);
+                } else {
+                    wrapper.addClass(CSS.CLOSED);
+                }
                 panel.addClass(CSS.PANEL);
-                panel.hide();
+                if (COUNT == 1) {
+                    panel.show();
+                } else {
+                    panel.hide();
+                }
                 link = null;
             } else {
                 link = this;
diff --git a/mod/choice/classes/event/answer_deleted.php b/mod/choice/classes/event/answer_deleted.php
new file mode 100644 (file)
index 0000000..7b8cb18
--- /dev/null
@@ -0,0 +1,114 @@
+<?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_choice answer deleted event.
+ *
+ * @package    mod_choice
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_choice\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_choice answer updated event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - int choiceid: id of choice.
+ *      - int optionid: id of the option.
+ * }
+ *
+ * @package    mod_choice
+ * @since      Moodle 3.1
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class answer_deleted extends \core\event\base {
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has deleted the option with id '" . $this->other['optionid'] . "' for the
+            user with id '$this->relateduserid' from the choice activity with course module id '$this->contextinstanceid'.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventanswerdeleted', 'mod_choice');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/choice/view.php', array('id' => $this->contextinstanceid));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'choice_answers';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['choiceid'])) {
+            throw new \coding_exception('The \'choiceid\' value must be set in other.');
+        }
+
+        if (!isset($this->other['optionid'])) {
+            throw new \coding_exception('The \'optionid\' value must be set in other.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return array('db' => 'choice_answers', 'restore' => \core\event\base::NOT_MAPPED);
+    }
+
+    public static function get_other_mapping() {
+        $othermapped = array();
+        $othermapped['choiceid'] = array('db' => 'choice', 'restore' => 'choice');
+        $othermapped['optionid'] = array('db' => 'choice_options', 'restore' => 'choice_option');
+
+        return $othermapped;
+    }
+}
diff --git a/mod/choice/classes/event/report_downloaded.php b/mod/choice/classes/event/report_downloaded.php
new file mode 100644 (file)
index 0000000..96f64c0
--- /dev/null
@@ -0,0 +1,114 @@
+<?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_choice report viewed event.
+ *
+ * @package mod_choice
+ * @copyright 2016 Stephen Bourget
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_choice\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_choice report viewed event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - string content: The content we are viewing.
+ *      - string format: The report format
+ *      - int choiced: The id of the choice
+ * }
+ *
+ * @package    mod_choice
+ * @since      Moodle 3.1
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class report_downloaded extends \core\event\base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventreportdownloaded', 'mod_choice');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has downloaded the report in the '".$this->other['format']."' format for
+            the choice activity with course module id '$this->contextinstanceid'";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/choice/report.php', array('id' => $this->contextinstanceid));
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        // Report format downloaded.
+        if (!isset($this->other['content'])) {
+            throw new \coding_exception('The \'content\' value must be set in other.');
+        }
+        // Report format downloaded.
+        if (!isset($this->other['format'])) {
+            throw new \coding_exception('The \'format\' value must be set in other.');
+        }
+        // ID of the choice activity.
+        if (!isset($this->other['choiceid'])) {
+            throw new \coding_exception('The \'choiceid\' value must be set in other.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return false;
+    }
+
+    public static function get_other_mapping() {
+        $othermapped = array();
+        $othermapped['choiceid'] = array('db' => 'choice', 'restore' => 'choice');
+
+        return $othermapped;
+    }
+}
index 48553ed..53a5bf4 100644 (file)
@@ -33,7 +33,9 @@ $string['displayhorizontal'] = 'Display horizontally';
 $string['displaymode'] = 'Display mode for the options';
 $string['displayvertical'] = 'Display vertically';
 $string['eventanswercreated'] = 'Choice made';
+$string['eventanswerdeleted'] = 'Choice answer deleted';
 $string['eventanswerupdated'] = 'Choice updated';
+$string['eventreportdownloaded'] = 'Choice report downloaded';
 $string['eventreportviewed'] = 'Choice report viewed';
 $string['expired'] = 'Sorry, this activity closed on {$a} and is no longer available';
 $string['atleastoneoption'] = 'You need to provide at least one possible answer.';
@@ -86,8 +88,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 900cead..0deb0a8 100644 (file)
@@ -513,7 +513,7 @@ function prepare_choice_show_results($choice, $course, $cm, $allresponses) {
  * @return bool
  */
 function choice_delete_responses($attemptids, $choice, $cm, $course) {
-    global $DB, $CFG;
+    global $DB, $CFG, $USER;
     require_once($CFG->libdir.'/completionlib.php');
 
     if(!is_array($attemptids) || empty($attemptids)) {
@@ -526,16 +526,36 @@ function choice_delete_responses($attemptids, $choice, $cm, $course) {
         }
     }
 
+    $context = context_module::instance($cm->id);
     $completion = new completion_info($course);
     foreach($attemptids as $attemptid) {
         if ($todelete = $DB->get_record('choice_answers', array('choiceid' => $choice->id, 'id' => $attemptid))) {
+            // Trigger the event answer deleted.
+            $eventdata = array();
+            $eventdata['objectid'] = $todelete->id;
+            $eventdata['context'] = $context;
+            $eventdata['userid'] = $USER->id;
+            $eventdata['courseid'] = $course->id;
+            $eventdata['relateduserid'] = $todelete->userid;
+            $eventdata['other'] = array();
+            $eventdata['other']['choiceid'] = $choice->id;
+            $eventdata['other']['optionid'] = $todelete->optionid;
+            $event = \mod_choice\event\answer_deleted::create($eventdata);
+            $event->add_record_snapshot('course', $course);
+            $event->add_record_snapshot('course_modules', $cm);
+            $event->add_record_snapshot('choice', $choice);
+            $event->add_record_snapshot('choice_answers', $todelete);
+            $event->trigger();
+
             $DB->delete_records('choice_answers', array('choiceid' => $choice->id, 'id' => $attemptid));
-            // Update completion state
-            if ($completion->is_enabled($cm) && $choice->completionsubmit) {
-                $completion->update_state($cm, COMPLETION_INCOMPLETE, $attemptid);
-            }
         }
     }
+
+    // Update completion state.
+    if ($completion->is_enabled($cm) && $choice->completionsubmit) {
+        $completion->update_state($cm, COMPLETION_INCOMPLETE);
+    }
+
     return true;
 }
 
index 9620cf1..59ac1b6 100644 (file)
         }
     } else {
         $groupmode = groups_get_activity_groupmode($cm);
+
+        // Trigger the report downloaded event.
+        $eventdata = array();
+        $eventdata['context'] = $context;
+        $eventdata['courseid'] = $course->id;
+        $eventdata['other']['content'] = 'choicereportcontentviewed';
+        $eventdata['other']['format'] = $download;
+        $eventdata['other']['choiceid'] = $choice->id;
+        $event = \mod_choice\event\report_downloaded::create($eventdata);
+        $event->trigger();
+
     }
 
     // Check if we want to include responses from inactive users.
index 71bb599..d383ed4 100644 (file)
@@ -201,6 +201,42 @@ class mod_choice_events_testcase extends advanced_testcase {
         $this->assertEventContextNotUsed($event);
     }
 
+    /**
+     * Test to ensure that event data is being stored correctly.
+     */
+    public function test_answer_deleted() {
+        global $DB, $USER;
+        // Generate user data.
+        $user = $this->getDataGenerator()->create_user();
+
+        $optionids = array_keys($DB->get_records('choice_options', array('choiceid' => $this->choice->id)));
+
+        // Create the first answer.
+        choice_user_submit_response($optionids[2], $this->choice, $user->id, $this->course, $this->cm);
+        // Get the users response.
+        $answer = $DB->get_record('choice_answers', array('userid' => $user->id, 'choiceid' => $this->choice->id),
+                '*', $strictness = IGNORE_MULTIPLE);
+
+        // Redirect event.
+        $sink = $this->redirectEvents();
+        // Now delete the answer.
+        choice_delete_responses(array($answer->id), $this->choice, $this->cm, $this->course);
+
+        // Get our event event.
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Data checking.
+        $this->assertInstanceOf('\mod_choice\event\answer_deleted', $event);
+        $this->assertEquals($USER->id, $event->userid);
+        $this->assertEquals($user->id, $event->relateduserid);
+        $this->assertEquals(context_module::instance($this->choice->cmid), $event->get_context());
+        $this->assertEquals($this->choice->id, $event->other['choiceid']);
+        $this->assertEquals($answer->optionid, $event->other['optionid']);
+        $this->assertEventContextNotUsed($event);
+        $sink->close();
+    }
+
     /**
      * Test to ensure that event data is being stored correctly.
      */
@@ -238,6 +274,43 @@ class mod_choice_events_testcase extends advanced_testcase {
         $sink->close();
     }
 
+    /**
+     * Test to ensure that event data is being stored correctly.
+     */
+    public function test_report_downloaded() {
+        global $USER;
+
+        $this->resetAfterTest();
+
+        // Generate user data.
+        $this->setAdminUser();
+
+        $eventdata = array();
+        $eventdata['context'] = $this->context;
+        $eventdata['courseid'] = $this->course->id;
+        $eventdata['other']['content'] = 'choicereportcontentviewed';
+        $eventdata['other']['format'] = 'csv';
+        $eventdata['other']['choiceid'] = $this->choice->id;
+
+        // This is fired in a page view so we can't run this through a function.
+        $event = \mod_choice\event\report_downloaded::create($eventdata);
+
+        // Redirect event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $event = $sink->get_events();
+
+        // Data checking.
+        $this->assertCount(1, $event);
+        $this->assertInstanceOf('\mod_choice\event\report_downloaded', $event[0]);
+        $this->assertEquals($USER->id, $event[0]->userid);
+        $this->assertEquals(context_module::instance($this->choice->cmid), $event[0]->get_context());
+        $this->assertEquals('csv', $event[0]->other['format']);
+        $this->assertEquals($this->choice->id, $event[0]->other['choiceid']);
+        $this->assertEventContextNotUsed($event[0]);
+        $sink->close();
+    }
+
     /**
      * Test to ensure that event data is being stored correctly.
      */
index 0b2d8c6..2b62ca9 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016020100;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;    // Requires this Moodle version
 $plugin->component = 'mod_choice';     // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 98d4840..7ced17e 100644 (file)
@@ -40,13 +40,10 @@ if ($action == 'delchoice' and confirm_sesskey() and is_enrolled($context, NULL,
         and $choiceavailable) {
     $answercount = $DB->count_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $USER->id));
     if ($answercount > 0) {
-        $DB->delete_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $USER->id));
-
-        // Update completion state
-        $completion = new completion_info($course);
-        if ($completion->is_enabled($cm) && $choice->completionsubmit) {
-            $completion->update_state($cm, COMPLETION_INCOMPLETE);
-        }
+        $choiceanswers = $DB->get_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $USER->id),
+            '', 'id');
+        $todelete = array_keys($choiceanswers);
+        choice_delete_responses($todelete, $choice, $cm, $course);
         redirect("view.php?id=$cm->id");
     }
 }
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 f354b23..a30885d 100644 (file)
 {{/ showdiscussionname }}
 </div>
 
-<table border="0" cellpadding="3" cellspacing="0" class="forumpost">
-    <tr class="header">
-        <td width="35" valign="top" class="picture left">
-            {{{ authorpicture }}}
-        </td>
-        <td class="topic {{# firstpost }}starter{{/ firstpost }}">
-            <div class="subject">
-                {{{ subject }}}
-            </div>
-            <div class="author">
-                {{# str }} bynameondate, forum, { "name": "<a target='_blank' href='{{{ authorlink }}}'>{{ authorfullname }}</a>", "date": "{{ postdate }}" } {{/ str }}
-            </div>
-        </td>
-    </tr>
-    <tr>
-        <td class="left side" valign="top">
-            {{# grouppicture }}
-                {{{ grouppicture }}}
-            {{/ grouppicture }}
-            {{^ grouppicture }}
-                &nbsp;
-            {{/ grouppicture }}
-        </td>
-        <td class="content">
-            {{# attachments }}
-                <div class="attachments">
-                    {{{ attachments }}}
-                </div>
-            {{/ attachments }}
-            {{{ message }}}
-
-            <div class="commands">
-                {{^ firstpost }}
-                    <a target="_blank" href="{{{ parentpostlink }}}">
-                        {{# str }} parent, forum {{/ str }}
-                    </a>
-                    {{# canreply }}
-                        |
-                    {{/ canreply }}
-                {{/ firstpost }}
-                {{# canreply }}
-                    <a target="_blank" href="{{{ replylink }}}">
-                        {{# str }} reply, forum {{/ str }}
-                    </a>
-                {{/ canreply }}
-            </div>
-
-            <div class="link">
-                <a target="_blank" href="{{{ permalink }}}">
-                    {{# str }} postincontext, forum {{/ str }}
-                </a>
-            </div>
-        </td>
-    </tr>
-</table>
+{{> mod_forum/forum_post_email_htmlemail_body }}
 
 <hr />
 <div class="mdl-align unsubscribelink">
diff --git a/mod/forum/templates/forum_post_email_htmlemail_body.mustache b/mod/forum/templates/forum_post_email_htmlemail_body.mustache
new file mode 100644 (file)
index 0000000..3798903
--- /dev/null
@@ -0,0 +1,134 @@
+{{!
+    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/>.
+}}
+{{!
+    @template mod_forum/forum_post_emaildigestfull_htmlemail_body
+
+    Template which defines the body component of a forum post for sending in a single-post HTML email.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * courselink
+    * coursename
+    * forumindexlink
+    * forumviewlink
+    * forumname
+    * discussionlink
+    * discussionname
+    * showdiscussionname
+    * firstpost
+    * subject
+    * authorlink
+    * authorpicture
+    * authorfullname
+    * postdate
+    * grouppicture
+    * attachments
+    * message
+    * parentpostlink
+    * canreply
+    * replylink
+    * permalink
+    * unsubscribeforumlink
+    * unsubscribediscussionlink
+
+    Example context (json):
+    {
+        "courselink": "https://example.com/course/view.php?id=2",
+        "coursename": "Example course",
+        "forumindexlink": "https://example.com/mod/forum/index.php?id=2",
+        "forumviewlink": "https://example.com/mod/forum/view.php?f=2",
+        "forumname": "Lorem ipsum dolor",
+        "discussionlink": "https://example.com/mod/forum/discuss.php?d=70",
+        "discussionname": "Is Lorem ipsum Latin?",
+        "showdiscussionname": 1,
+        "firstpost": 1,
+        "subject": "Is Lorem ipsum Latin?",
+        "authorlink": "https://example.com/user/view.php?id=2&course=2",
+        "authorpicture": "<a href=\"https://example.com/user/view.php?id=2&amp;course=6\"><img src=\"https://example.com/theme/image.php?theme=clean&amp;component=core&amp;image=u%2Ff2&amp;svg=0\" alt=\"Picture of Admin User\" title=\"Picture of Admin User\" class=\"userpicture defaultuserpic\" width=\"35\" height=\"35\" /></a>",
+        "authorfullname": "Lucius Caecilius lucundus",
+        "postdate": "Sunday, 13 September 2015, 2:22 pm",
+        "grouppicture": "",
+        "attachments": "",
+        "message": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et auctor libero. Quisque porta egestas neque, et bibendum libero dignissim at. Nulla facilisi. Morbi eget accumsan felis. Nunc et vulputate odio, vel venenatis nisl. Nunc maximus ipsum sed tincidunt mollis. Integer nunc erat, luctus sit amet arcu tincidunt, volutpat dignissim mi. Sed ut magna quam.  Mauris accumsan porta turpis sed aliquam. Etiam at justo tristique, imperdiet augue quis, consectetur sapien. Ut nec erat malesuada sem suscipit lobortis. Vivamus posuere nibh eu ipsum porta fringilla.  Sed vitae dapibus ipsum, ac condimentum enim. Sed dignissim ante at elit mollis, ac tempor lacus iaculis. Etiam nec lectus vitae nibh vulputate volutpat. Nulla quis tellus aliquam, commodo nisi et, dictum est.</p><p><br /></p>",
+        "parentpostlink": "",
+        "canreply": 1,
+        "replylink": "https://example.com/mod/forum/post.php?reply=2",
+        "permalink": "https://example.com/mod/forum/discuss.php?d=2#2",
+        "unsubscribeforumlink": "https://example.com/mod/forum/subscribe.php?id=2",
+        "unsubscribediscussionlink": "https://example.com/mod/discussion/subscribe.php?id=2&d=2"
+    }
+}}
+<table border="0" cellpadding="3" cellspacing="0" class="forumpost">
+    <tr class="header">
+        <td width="35" valign="top" class="picture left">
+            {{{ authorpicture }}}
+        </td>
+        <td class="topic {{# firstpost }}starter{{/ firstpost }}">
+            <div class="subject">
+                {{{ subject }}}
+            </div>
+            <div class="author">
+                {{# str }} bynameondate, forum, { "name": "<a target='_blank' href='{{{ authorlink }}}'>{{ authorfullname }}</a>", "date": "{{ postdate }}" } {{/ str }}
+            </div>
+        </td>
+    </tr>
+    <tr>
+        <td class="left side" valign="top">
+            {{# grouppicture }}
+                {{{ grouppicture }}}
+            {{/ grouppicture }}
+            {{^ grouppicture }}
+                &nbsp;
+            {{/ grouppicture }}
+        </td>
+        <td class="content">
+            {{# attachments }}
+                <div class="attachments">
+                    {{{ attachments }}}
+                </div>
+            {{/ attachments }}
+            {{{ message }}}
+
+            <div class="commands">
+                {{^ firstpost }}
+                    <a target="_blank" href="{{{ parentpostlink }}}">
+                        {{# str }} parent, forum {{/ str }}
+                    </a>
+                    {{# canreply }}
+                        |
+                    {{/ canreply }}
+                {{/ firstpost }}
+                {{# canreply }}
+                    <a target="_blank" href="{{{ replylink }}}">
+                        {{# str }} reply, forum {{/ str }}
+                    </a>
+                {{/ canreply }}
+            </div>
+
+            <div class="link">
+                <a target="_blank" href="{{{ permalink }}}">
+                    {{# str }} postincontext, forum {{/ str }}
+                </a>
+            </div>
+        </td>
+    </tr>
+</table>
index 5b2240f..74dc589 100644 (file)
@@ -77,4 +77,4 @@
         "unsubscribediscussionlink": "https://example.com/mod/discussion/subscribe.php?id=2&d=2"
     }
 }}
-{{> mod_forum/forum_post_email_htmlemail }}
+{{> mod_forum/forum_post_email_htmlemail_body }}
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 66d366d..dad4a8b 100644 (file)
              }
          }
 
-         echo "<p <p class=\"centerpara\">";
+         echo "<p class=\"centerpara\">";
          echo $OUTPUT->user_picture($user, array('courseid'=>$course->id));
          echo "</p>";
 
 
          if ($showscales) {
              // Print overall summary
-             echo "<p <p class=\"centerpara\">>";
+            echo "<p class=\"centerpara\">";
              survey_print_graph("id=$id&amp;sid=$student&amp;type=student.png");
              echo "</p>";
 
                     $table = new html_table();
                      $table->head = array(get_string($question->text, "survey"));
                      $table->align = array ("left");
-                     $table->data[] = array(s($answer->answer1)); // no html here, just plain text
+                    if (!empty($question->options) && $answer->answer1 > 0) {
+                        $answers = explode(',', get_string($question->options, 'survey'));
+                        if ($answer->answer1 <= count($answers)) {
+                            $table->data[] = array(s($answers[$answer->answer1 - 1])); // No html here, just plain text.
+                        } else {
+                            $table->data[] = array(s($answer->answer1)); // No html here, just plain text.
+                        }
+                    } else {
+                         $table->data[] = array(s($answer->answer1)); // No html here, just plain text.
+                    }
                      echo html_writer::table($table);
                      echo $OUTPUT->spacer(array('height'=>30));
                  }
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 721b538..3eb12c8 100644 (file)
@@ -99,6 +99,11 @@ class user_edit_form extends moodleform {
         $mform = $this->_form;
         $userid = $mform->getElementValue('id');
 
+        // Trim required name fields.
+        foreach (useredit_get_required_name_fields() as $field) {
+            $mform->applyFilter($field, 'trim');
+        }
+
         if ($user = $DB->get_record('user', array('id' => $userid))) {
 
             // Remove description.
index 2dad585..b689923 100644 (file)
@@ -140,6 +140,12 @@ class user_editadvanced_form extends moodleform {
         global $USER, $CFG, $DB, $OUTPUT;
 
         $mform = $this->_form;
+
+        // Trim required name fields.
+        foreach (useredit_get_required_name_fields() as $field) {
+            $mform->applyFilter($field, 'trim');
+        }
+
         if ($userid = $mform->getElementValue('id')) {
             $user = $DB->get_record('user', array('id' => $userid));
         } else {
index ae9504d..5051f76 100644 (file)
@@ -284,11 +284,17 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
     }
 
     $strrequired = get_string('required');
+    $stringman = get_string_manager();
 
     // Add the necessary names.
     foreach (useredit_get_required_name_fields() as $fullname) {
         $mform->addElement('text', $fullname,  get_string($fullname),  'maxlength="100" size="30"');
-        $mform->addRule($fullname, $strrequired, 'required', null, 'client');
+        if ($stringman->string_exists('missing'.$fullname, 'core')) {
+            $strmissingfield = get_string('missing'.$fullname, 'core');
+        } else {
+            $strmissingfield = $strrequired;
+        }
+        $mform->addRule($fullname, $strmissingfield, 'required', null, 'client');
         $mform->setType($fullname, PARAM_NOTAGS);
     }
 
diff --git a/user/tests/behat/name_fields.feature b/user/tests/behat/name_fields.feature
new file mode 100644 (file)
index 0000000..ab61512
--- /dev/null
@@ -0,0 +1,50 @@
+@core @core_user
+Feature: Both first name and surname are always available for every user
+  In order to easily identify and display users on Moodle pages
+  As any user
+  I need to rely on both first name and surname are always available
+
+  Scenario: Attempting to self-register as a new user with empty names
+    Given the following config values are set as admin:
+      | registerauth    | email |
+      | passwordpolicy  | 0     |
+    And I am on site homepage
+    And I follow "Log in"
+    And I follow "New Account"
+    When I set the following fields to these values:
+      | Username      | mrwhitespace        |
+      | Password      | Gue$$m3ifY0uC&n     |
+      | Email address | mrwhitespace@nas.ty |
+      | Email (again) | mrwhitespace@nas.ty |
+    And I set the field "First name" to " "
+    And I set the field "Surname" to " "
+    And I click on "Create my new account" "button"
+    Then I should see "Missing given name"
+    And I should see "Missing surname"
+
+  Scenario: Attempting to change own names to whitespace
+    Given the following "users" exist:
+      | username | firstname | lastname | email       |
+      | foobar   | Foo       | Bar      | foo@bar.com |
+    And I log in as "foobar"
+    And I follow "Profile" in the user menu
+    And I follow "Edit profile"
+    When I set the field "First name" to " "
+    And I set the field "Surname" to " "
+    And I click on "Update profile" "button"
+    Then I should see "Missing given name"
+    And I should see "Missing surname"
+
+  Scenario: Attempting to change someone else's names to whitespace
+    Given the following "users" exist:
+      | username | firstname | lastname | email       |
+      | foobar   | Foo       | Bar      | foo@bar.com |
+    And I log in as "admin"
+    And I navigate to "Browse list of users" node in "Site administration > Users > Accounts"
+    And I follow "Foo Bar"
+    And I follow "Edit profile"
+    When I set the field "First name" to " "
+    And I set the field "Surname" to " "
+    And I click on "Update profile" "button"
+    Then I should see "Missing given name"
+    And I should see "Missing surname"
index d40392a..c60895d 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016012100.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016012900.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.1dev (Build: 20160121)'; // Human-friendly version name
+$release  = '3.1dev (Build: 20160129)'; // Human-friendly version name
 
 $branch   = '31';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.