Merge branch 'MDL-52780-mailoriginscript' of https://github.com/brendanheywood/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 26 Jan 2016 00:01:12 +0000 (01:01 +0100)
committerDavid Monllao <davidm@moodle.com>
Fri, 29 Jan 2016 02:15:20 +0000 (10:15 +0800)
157 files changed:
admin/index.php
admin/renderer.php
admin/tool/log/classes/log/manager.php
backup/util/helper/restore_structure_parser_processor.class.php
blocks/admin_bookmarks/tests/behat/bookmark_admin_pages.feature [new file with mode: 0644]
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/rss_client/block_rss_client.php
blocks/rss_client/classes/output/block.php [new file with mode: 0644]
blocks/rss_client/classes/output/channel_image.php [new file with mode: 0644]
blocks/rss_client/classes/output/feed.php [new file with mode: 0644]
blocks/rss_client/classes/output/footer.php [new file with mode: 0644]
blocks/rss_client/classes/output/item.php [new file with mode: 0644]
blocks/rss_client/classes/output/renderer.php [new file with mode: 0644]
blocks/rss_client/templates/block.mustache [new file with mode: 0644]
blocks/rss_client/templates/channel_image.mustache [new file with mode: 0644]
blocks/rss_client/templates/feed.mustache [new file with mode: 0644]
blocks/rss_client/templates/footer.mustache [new file with mode: 0644]
blocks/rss_client/templates/item.mustache [new file with mode: 0644]
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
calendar/preferences_form.php
calendar/tests/behat/calendar_lookahead.feature [new file with mode: 0644]
cohort/externallib.php
cohort/upgrade.txt
completion/criteria/completion_criteria_grade.php
completion/tests/behat/behat_completion.php
course/format/renderer.php
course/tests/behat/behat_course.php
enrol/ldap/tests/ldap_test.php
grade/grading/form/rubric/edit_form.php
grade/grading/form/rubric/js/rubric.js
grade/grading/form/rubric/js/rubriceditor.js
grade/grading/form/rubric/lang/en/gradingform_rubric.php
grade/grading/form/rubric/lib.php
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/rubriceditor.php
grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php
grade/grading/form/rubric/tests/behat/edit_rubric.feature
grade/grading/form/rubric/tests/behat/reuse_own_rubrics.feature
grade/report/grader/lib.php
grade/report/grader/tests/behat/switch_views.feature [new file with mode: 0644]
grade/report/history/index.php
grade/report/history/tests/behat/basic_functionality.feature
install/lang/cs/install.php
lang/en/admin.php
lib/amd/build/str.min.js
lib/amd/build/tree.min.js [new file with mode: 0644]
lib/amd/src/str.js
lib/amd/src/tree.js [new file with mode: 0644]
lib/blocklib.php
lib/classes/log/sql_internal_reader.php [deleted file]
lib/classes/log/sql_select_reader.php [deleted file]
lib/db/upgrade.php
lib/deprecatedlib.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/eventslib.php
lib/formslib.php
lib/grade/grade_category.php
lib/grade/tests/grade_category_test.php
lib/moodlelib.php
lib/navigationlib.php
lib/pear/HTML/QuickForm.php
lib/pear/HTML/QuickForm/Renderer/Tableless.php
lib/pear/README_MOODLE.txt
lib/phpunit/classes/advanced_testcase.php
lib/plagiarismlib.php
lib/rsslib.php
lib/testing/classes/tests_finder.php
lib/testing/classes/util.php
lib/tests/behat/behat_navigation.php
lib/tests/event_test.php
lib/tests/eventslib_test.php
lib/upgrade.txt
lib/weblib.php
lib/wiki_to_markdown.php
login/signup_form.php
message/externallib.php
message/lib.php
message/tests/externallib_test.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/tests/behat/reopen_locked_submission.feature [new file with mode: 0644]
mod/assign/tests/locallib_test.php
mod/data/js.php
mod/forum/markposts.php
mod/lesson/lib.php
mod/lesson/reformat.php [deleted file]
mod/lesson/tests/behat/lesson_outline_report.feature [new file with mode: 0644]
mod/lesson/upgrade.txt
mod/quiz/accessmanager.php
mod/quiz/accessmanager_form.php
mod/quiz/accessrule/timelimit/lang/en/quizaccess_timelimit.php
mod/quiz/accessrule/timelimit/rule.php
mod/quiz/amd/build/preflightcheck.min.js [new file with mode: 0644]
mod/quiz/amd/src/preflightcheck.js [new file with mode: 0644]
mod/quiz/attemptlib.php
mod/quiz/classes/structure.php
mod/quiz/comment.php
mod/quiz/index.php
mod/quiz/lang/en/quiz.php
mod/quiz/locallib.php
mod/quiz/module.js
mod/quiz/processattempt.php
mod/quiz/renderer.php
mod/quiz/review.php
mod/quiz/reviewquestion.php
mod/quiz/startattempt.php
mod/quiz/styles.css
mod/quiz/tests/behat/add_quiz.feature
mod/quiz/tests/behat/attempt_basic.feature
mod/quiz/tests/behat/attempt_begin.feature [new file with mode: 0644]
mod/quiz/tests/behat/attempt_redo_questions.feature
mod/quiz/tests/behat/attempt_require_previous.feature
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/behat/quiz_reset.feature
mod/quiz/upgrade.txt
mod/quiz/view.php
mod/scorm/lib.php
mod/scorm/locallib.php
mod/scorm/report/userreport.php
mod/scorm/report/userreportinteractions.php
mod/scorm/report/userreporttracks.php
mod/scorm/tests/lib_test.php
mod/scorm/upgrade.txt
mod/workshop/form/rubric/styles.css
mod/workshop/styles.css
plagiarism/lib.php
plagiarism/upgrade.txt [new file with mode: 0644]
report/log/classes/table_log.php
version.php

index b8de2e6..1000f93 100644 (file)
@@ -843,6 +843,8 @@ $buggyiconvnomb = (!function_exists('mb_convert_encoding') and @iconv('UTF-8', '
 $registered = $DB->count_records('registration_hubs', array('huburl' => HUB_MOODLEORGHUBURL, 'confirmed' => 1));
 // Check if there are any cache warnings.
 $cachewarnings = cache_helper::warnings();
+// Check if there are events 1 API handlers.
+$eventshandlers = $DB->get_records_sql('SELECT DISTINCT component FROM {events_handlers}');
 
 admin_externalpage_setup('adminnotifications');
 
@@ -850,4 +852,4 @@ $output = $PAGE->get_renderer('core', 'admin');
 
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
                                        $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
-                                       $registered, $cachewarnings);
+                                       $registered, $cachewarnings, $eventshandlers);
index 12d9dc3..7c0dfe2 100644 (file)
@@ -275,12 +275,13 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param array|null $availableupdates array of \core\update\info objects or null
      * @param int|null $availableupdatesfetch timestamp of the most recent updates fetch or null (unknown)
      * @param string[] $cachewarnings An array containing warnings from the Cache API.
+     * @param array $eventshandlers Events 1 API handlers.
      *
      * @return string HTML to output.
      */
     public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
-            $buggyiconvnomb, $registered, array $cachewarnings = array()) {
+            $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0) {
         global $CFG;
         $output = '';
 
@@ -294,6 +295,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->db_problems($dbproblems);
         $output .= $this->maintenance_mode_warning($maintenancemode);
         $output .= $this->cache_warnings($cachewarnings);
+        $output .= $this->events_handlers($eventshandlers);
         $output .= $this->registration_warning($registered);
 
         //////////////////////////////////////////////////////////////////////////////////////////////////
@@ -594,6 +596,23 @@ class core_admin_renderer extends plugin_renderer_base {
         return join("\n", array_map(array($this, 'warning'), $cachewarnings));
     }
 
+    /**
+     * Renders events 1 API handlers warning.
+     *
+     * @param array $eventshandlers
+     * @return string
+     */
+    public function events_handlers($eventshandlers) {
+        if ($eventshandlers) {
+            $components = '';
+            foreach ($eventshandlers as $eventhandler) {
+                $components .= $eventhandler->component . ', ';
+            }
+            $components = rtrim($components, ', ');
+            return $this->warning(get_string('eventshandlersinuse', 'admin', $components));
+        }
+    }
+
     /**
      * Render an appropriate message if the site in in maintenance mode.
      * @param bool $maintenancemode
index 4077347..2b1d491 100644 (file)
@@ -106,38 +106,6 @@ class manager implements \core\log\manager {
             if (empty($interface) || ($reader instanceof $interface)) {
                 $return[$plugin] = $reader;
             }
-            // TODO MDL-49291 These conditions should be removed as part of the 2nd stage deprecation.
-            if ($reader instanceof \core\log\sql_internal_reader) {
-                debugging('\core\log\sql_internal_reader has been deprecated in favour of \core\log\sql_internal_table_reader.' .
-                    ' Update ' . get_class($reader) . ' to use the new interface.', DEBUG_DEVELOPER);
-            } else if ($reader instanceof \core\log\sql_select_reader) {
-                debugging('\core\log\sql_select_reader has been deprecated in favour of \core\log\sql_reader. Update ' .
-                    get_class($reader) . ' to use the new interface.', DEBUG_DEVELOPER);
-            }
-        }
-
-        // TODO MDL-49291 This section below (until the final return) should be removed as part of the 2nd stage deprecation.
-        $isselectreader = (ltrim($interface, '\\') === 'core\log\sql_select_reader');
-        $isinternalreader = (ltrim($interface, '\\') === 'core\log\sql_internal_reader');
-        if ($isselectreader || $isinternalreader) {
-
-            if ($isselectreader) {
-                $alternative = '\core\log\sql_reader';
-            } else {
-                $alternative = '\core\log\sql_internal_table_reader';
-            }
-
-            if (count($return) === 0) {
-                // If there are no classes implementing the provided interface and the provided interface is one of
-                // the deprecated ones, we return the non-deprecated alternatives. It should be safe as the new interface
-                // is adding a new method but not changing the existing ones.
-                debugging($interface . ' has been deprecated in favour of ' . $alternative . '. Returning ' . $alternative .
-                    ' instances instead. Please call get_readers() using the new interface.', DEBUG_DEVELOPER);
-                $return = $this->get_readers($alternative);
-            } else {
-                debugging($interface . ' has been deprecated in favour of ' . $alternative .
-                    '. Please call get_readers() using the new interface.', DEBUG_DEVELOPER);
-            }
         }
 
         return $return;
index 0cfdfe7..500fa0c 100644 (file)
@@ -62,7 +62,7 @@ class restore_structure_parser_processor extends grouped_parser_processor {
         }
         // Decode file.php calls
         $search = array ("$@FILEPHP@$");
-        $replace = array(get_file_url($this->courseid));
+        $replace = array(moodle_url::make_legacyfile_url($this->courseid, null));
         $result = str_replace($search, $replace, $cdata);
         // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799
         $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$');
diff --git a/blocks/admin_bookmarks/tests/behat/bookmark_admin_pages.feature b/blocks/admin_bookmarks/tests/behat/bookmark_admin_pages.feature
new file mode 100644 (file)
index 0000000..1574fe8
--- /dev/null
@@ -0,0 +1,36 @@
+@block @block_admin_bookmarks
+Feature: Add a bookmarks to an admin pages
+  In order to speed up common tasks
+  As an admin
+  I need to add and access pages through bookmarks
+
+  Background:
+    Given I log in as "admin"
+    And I navigate to "Scheduled tasks" node in "Site administration > Server"
+    And I click on "Bookmark this page" "link" in the "Admin bookmarks" "block"
+    And I log out
+
+  # Test bookmark functionality using the "User profile fields" page as our bookmark.
+  Scenario: Admin page can be bookmarked
+    Given I log in as "admin"
+    And I navigate to "User profile fields" node in "Site administration > Users > Accounts"
+    When I click on "Bookmark this page" "link" in the "Admin bookmarks" "block"
+    Then I should see "User profile fields" in the "Admin bookmarks" "block"
+    # See the existing bookmark is there too.
+    And I should see "Scheduled tasks" in the "Admin bookmarks" "block"
+
+  Scenario: Admin page can be accessed through bookmarks block
+    Given I log in as "admin"
+    And I navigate to "Notifications" node in "Site administration"
+    And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block"
+    # Verify that we are on the right page.
+    Then I should see "Scheduled tasks" in the "h1" "css_element"
+
+  Scenario: Admin page can be removed from bookmarks
+    Given I log in as "admin"
+    And I navigate to "Notifications" node in "Site administration"
+    And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block"
+    When I click on "Unbookmark this page" "link" in the "Admin bookmarks" "block"
+    Then I should see "Bookmark deleted"
+    And I wait to be redirected
+    And I should not see "Scheduled tasks" in the "Admin bookmarks" "block"
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..3f5162d
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..1416899
--- /dev/null
@@ -0,0 +1,170 @@
+// 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 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
+    };
+
+    /**
+     * 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></a>');
+                link.attr('title', node.title);
+                link.attr('href', node.link);
+
+                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..3c8932c 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,27 @@ 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'
+                    );
+                } 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 +173,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 +182,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"
-    ]
-  }
-}
index 68a13ac..e41e68c 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+/**
+ * Contains block_rss_client
+ * @package    block_rss_client
+ * @copyright  Daryl Hawes
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
+ */
+
 /**
  * A block which displays Remote feeds
  *
         }
     }
 
+    /**
+     * Gets the footer, which is the channel link of the last feed in our list of feeds
+     *
+     * @param array $feedrecords The feed records from the database.
+     * @return block_rss_client\output\footer|null The renderable footer or null if none should be displayed.
+     */
+    protected function get_footer($feedrecords) {
+        $footer = null;
+
+        if ($this->config->block_rss_client_show_channel_link) {
+            global $CFG;
+            require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
+
+            $feedrecord     = array_pop($feedrecords);
+            $feed           = new moodle_simplepie($feedrecord->url);
+            $channellink    = new moodle_url($feed->get_link());
+
+            if (!empty($channellink)) {
+                $footer = new block_rss_client\output\footer($channellink);
+            }
+        }
+
+        return $footer;
+    }
+
     function get_content() {
         global $CFG, $DB;
 
             $maxentries = intval($CFG->block_rss_client_num_entries);
         }
 
-
         /* ---------------------------------
          * Begin Normal Display of Block Content
          * --------------------------------- */
 
-        $output = '';
-
+        $renderer = $this->page->get_renderer('block_rss_client');
+        $block = new \block_rss_client\output\block();
 
         if (!empty($this->config->rssid)) {
-            list($rss_ids_sql, $params) = $DB->get_in_or_equal($this->config->rssid);
-
-            $rss_feeds = $DB->get_records_select('block_rss_client', "id $rss_ids_sql", $params);
+            list($rssidssql, $params) = $DB->get_in_or_equal($this->config->rssid);
+            $rssfeeds = $DB->get_records_select('block_rss_client', "id $rssidssql", $params);
+
+            if (!empty($rssfeeds)) {
+                $showtitle = false;
+                if (count($rssfeeds) > 1) {
+                    // When many feeds show the title for each feed.
+                    $showtitle = true;
+                }
 
-            $showtitle = false;
-            if (count($rss_feeds) > 1) {
-                // when many feeds show the title for each feed
-                $showtitle = true;
-            }
+                foreach ($rssfeeds as $feed) {
+                    if ($renderablefeed = $this->get_feed($feed, $maxentries, $showtitle)) {
+                        $block->add_feed($renderablefeed);
+                    }
+                }
 
-            foreach($rss_feeds as $feed){
-                $output.= $this->get_feed_html($feed, $maxentries, $showtitle);
+                $footer = $this->get_footer($rssfeeds);
             }
         }
 
-        $this->content->text = $output;
+        $this->content->text = $renderer->render_block($block);
+        if (isset($footer)) {
+            $this->content->footer = $renderer->render_footer($footer);
+        }
 
         return $this->content;
     }
      * @param mixed feedrecord The feed record from the database
      * @param int maxentries The maximum number of entries to be displayed
      * @param boolean showtitle Should the feed title be displayed in html
-     * @return string html representing the rss feed content
+     * @return block_rss_client\output\feed|null The renderable feed or null of there is an error
      */
-    function get_feed_html($feedrecord, $maxentries, $showtitle){
+    public function get_feed($feedrecord, $maxentries, $showtitle) {
         global $CFG;
         require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
 
-        $feed = new moodle_simplepie($feedrecord->url);
+        $simplepiefeed = new moodle_simplepie($feedrecord->url);
 
         if(isset($CFG->block_rss_client_timeout)){
-            $feed->set_cache_duration($CFG->block_rss_client_timeout*60);
-        }
-
-        if ($CFG->debugdeveloper && $feed->error()) {
-            return '<p>'. $feedrecord->url .' Failed with code: '.$feed->error().'</p>';
+            $simplepiefeed->set_cache_duration($CFG->block_rss_client_timeout * 60);
         }
 
-        $r = ''; // return string
-
-        if($this->config->block_rss_client_show_channel_image){
-            if($image = $feed->get_image_url()){
-                $imagetitle = s($feed->get_image_title());
-                $imagelink  = $feed->get_image_link();
-
-                $r.='<div class="image" title="'.$imagetitle.'">'."\n";
-                if($imagelink){
-                    $r.='<a href="'.$imagelink.'">';
-                }
-                $r.='<img src="'.$image.'" alt="'.$imagetitle.'" />'."\n";
-                if($imagelink){
-                    $r.='</a>';
-                }
-                $r.= '</div>';
-            }
+        if ($simplepiefeed->error()) {
+            debugging($feedrecord->url .' Failed with code: '.$simplepiefeed->error());
+            return null;
         }
 
         if(empty($feedrecord->preferredtitle)){
-            $feedtitle = $this->format_title($feed->get_title());
+            $feedtitle = $this->format_title($simplepiefeed->get_title());
         }else{
             $feedtitle = $this->format_title($feedrecord->preferredtitle);
         }
 
-        if($showtitle){
-            $r.='<div class="title">'.$feedtitle.'</div>';
-        }
-
-
-        $r.='<ul class="list no-overflow">'."\n";
-
-        $feeditems = $feed->get_items(0, $maxentries);
-        foreach($feeditems as $item){
-            $r.= $this->get_item_html($item);
-        }
-
-        $r.='</ul>';
-
-
-        if ($this->config->block_rss_client_show_channel_link) {
-
-            $channellink = $feed->get_link();
-
-            if (!empty($channellink)){
-                //NOTE: this means the 'last feed' display wins the block title - but
-                //this is exiting behaviour..
-                $this->content->footer = '<a href="'.htmlspecialchars(clean_param($channellink,PARAM_URL)).'">'. get_string('clientchannellink', 'block_rss_client') .'</a>';
-            }
-        }
-
         if (empty($this->config->title)){
             //NOTE: this means the 'last feed' displayed wins the block title - but
             //this is exiting behaviour..
             $this->title = strip_tags($feedtitle);
         }
 
-        return $r;
-    }
-
-
-    /**
-     * Returns the html list item of a feed item
-     *
-     * @param mixed item simplepie_item representing the feed item
-     * @return string html li representing the rss feed item
-     */
-    function get_item_html($item){
-
-        $link        = $item->get_link();
-        $title       = $item->get_title();
-        $description = $item->get_description();
-
-
-        if(empty($title)){
-            // no title present, use portion of description
-            $title = core_text::substr(strip_tags($description), 0, 20) . '...';
-        }else{
-            $title = break_up_long_words($title, 30);
+        $feed = new \block_rss_client\output\feed($feedtitle, $showtitle, $this->config->block_rss_client_show_channel_image);
+
+        if ($simplepieitems = $simplepiefeed->get_items(0, $maxentries)) {
+            foreach ($simplepieitems as $simplepieitem) {
+                try {
+                    $item = new \block_rss_client\output\item(
+                        $simplepieitem->get_id(),
+                        new moodle_url($simplepieitem->get_link()),
+                        $simplepieitem->get_title(),
+                        $simplepieitem->get_description(),
+                        new moodle_url($simplepieitem->get_permalink()),
+                        $simplepieitem->get_date('U'),
+                        $this->config->display_description
+                    );
+
+                    $feed->add_item($item);
+                } catch (moodle_exception $e) {
+                    // If there is an error with the RSS item, we don't
+                    // want to crash the page. Specifically, moodle_url can
+                    // throw an exception of the param is an extremely
+                    // malformed url.
+                    debugging($e->getMessage());
+                }
+            }
         }
 
-        if(empty($link)){
-            $link = $item->get_id();
-        } else {
+        // Feed image.
+        if ($imageurl = $simplepiefeed->get_image_url()) {
             try {
-                // URLs in our RSS cache will be escaped (correctly as theyre store in XML)
-                // html_writer::link() will re-escape them. To prevent double escaping unescape here.
-                // This can by done using htmlspecialchars_decode() but moodle_url also has that effect.
-                $link = new moodle_url($link);
+                $image = new \block_rss_client\output\channel_image(
+                    new moodle_url($imageurl),
+                    $simplepiefeed->get_image_title(),
+                    new moodle_url($simplepiefeed->get_image_link())
+                );
+
+                $feed->set_image($image);
             } catch (moodle_exception $e) {
-                // Catching the exception to prevent the whole site to crash in case of malformed RSS feed
-                $link = '';
+                // If there is an error with the RSS image, we don'twant to
+                // crash the page. Specifically, moodle_url can throw an
+                // exception if the param is an extremely malformed url.
+                debugging($e->getMessage());
             }
         }
 
-        $r = html_writer::start_tag('li');
-            $r.= html_writer::start_tag('div',array('class'=>'link'));
-                $r.= html_writer::link($link, s($title), array('onclick'=>'this.target="_blank"'));
-            $r.= html_writer::end_tag('div');
-
-            if($this->config->display_description && !empty($description)){
-
-                $formatoptions = new stdClass();
-                $formatoptions->para = false;
-
-                $r.= html_writer::start_tag('div',array('class'=>'description'));
-                    $description = format_text($description, FORMAT_HTML, $formatoptions, $this->page->course->id);
-                    $description = break_up_long_words($description, 30);
-                    $r.= $description;
-                $r.= html_writer::end_tag('div');
-            }
-        $r.= html_writer::end_tag('li');
-
-        return $r;
+        return $feed;
     }
 
     /**
         return $newskiptime;
     }
 }
-
-
diff --git a/blocks/rss_client/classes/output/block.php b/blocks/rss_client/classes/output/block.php
new file mode 100644 (file)
index 0000000..7789f0c
--- /dev/null
@@ -0,0 +1,104 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\block
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to help display an RSS Feeds block
+ *
+ * @package   block_rss_client
+ * @copyright 2016 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block implements \renderable, \templatable {
+
+    /**
+     * An array of renderable feeds
+     *
+     * @var array
+     */
+    protected $feeds;
+
+    /**
+     * Contruct
+     *
+     * @param array $feeds An array of renderable feeds
+     */
+    public function __construct(array $feeds = array()) {
+        $this->feeds = $feeds;
+    }
+
+    /**
+     * Prepare data for use in a template
+     *
+     * @param \renderer_base $output
+     * @return array
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = array('feeds' => array());
+
+        foreach ($this->feeds as $feed) {
+            $data['feeds'][] = $feed->export_for_template($output);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Add a feed
+     *
+     * @param \block_rss_client\output\feed $feed
+     * @return \block_rss_client\output\block
+     */
+    public function add_feed(feed $feed) {
+        $this->feeds[] = $feed;
+
+        return $this;
+    }
+
+    /**
+     * Set the feeds
+     *
+     * @param array $feeds
+     * @return \block_rss_client\output\block
+     */
+    public function set_feeds(array $feeds) {
+        $this->feeds = $feeds;
+
+        return $this;
+    }
+
+    /**
+     * Get feeds
+     *
+     * @return array
+     */
+    public function get_feeds() {
+        return $this->feeds;
+    }
+}
diff --git a/blocks/rss_client/classes/output/channel_image.php b/blocks/rss_client/classes/output/channel_image.php
new file mode 100644 (file)
index 0000000..af9e22f
--- /dev/null
@@ -0,0 +1,151 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\channel_image
+ *
+ * @package   block_rss_client
+ * @copyright 2016 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to display RSS channel images
+ *
+ * @package   block_rss_client
+ * @copyright 2016 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class channel_image implements \renderable, \templatable {
+
+    /**
+     * The URL location of the image
+     *
+     * @var string
+     */
+    protected $url;
+
+    /**
+     * The title of the image
+     *
+     * @var string
+     */
+    protected $title;
+
+    /**
+     * The URL of the image link
+     *
+     * @var string
+     */
+    protected $link;
+
+    /**
+     * Contructor
+     *
+     * @param \moodle_url $url The URL location of the image
+     * @param string $title The title of the image
+     * @param \moodle_url $link The URL of the image link
+     */
+    public function __construct(\moodle_url $url, $title, \moodle_url $link = null) {
+        $this->url      = $url;
+        $this->title    = $title;
+        $this->link     = $link;
+    }
+
+    /**
+     * Export this for use in a mustache template context.
+     *
+     * @see templatable::export_for_template()
+     * @param renderer_base $output
+     * @return array The data for the template
+     */
+    public function export_for_template(\renderer_base $output) {
+        return array(
+            'url'   => clean_param($this->url, PARAM_URL),
+            'title' => $this->title,
+            'link'  => clean_param($this->link, PARAM_URL),
+        );
+    }
+
+    /**
+     * Set the URL
+     *
+     * @param \moodle_url $url
+     * @return \block_rss_client\output\channel_image
+     */
+    public function set_url(\moodle_url $url) {
+        $this->url = $url;
+
+        return $this;
+    }
+
+    /**
+     * Get the URL
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return $this->url;
+    }
+
+    /**
+     * Set the title
+     *
+     * @param string $title
+     * @return \block_rss_client\output\channel_image
+     */
+    public function set_title($title) {
+        $this->title = $title;
+
+        return $this;
+    }
+
+    /**
+     * Get the title
+     *
+     * @return string
+     */
+    public function get_title() {
+        return $this->title;
+    }
+
+    /**
+     * Set the link
+     *
+     * @param \moodle_url $link
+     * @return \block_rss_client\output\channel_image
+     */
+    public function set_link($link) {
+        $this->link = $link;
+
+        return $this;
+    }
+
+    /**
+     * Get the link
+     *
+     * @return \moodle_url
+     */
+    public function get_link() {
+        return $this->link;
+    }
+}
diff --git a/blocks/rss_client/classes/output/feed.php b/blocks/rss_client/classes/output/feed.php
new file mode 100644 (file)
index 0000000..02f7e2d
--- /dev/null
@@ -0,0 +1,224 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\feed
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to help display an RSS Feed
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class feed implements \renderable, \templatable {
+
+    /**
+     * The feed's title
+     *
+     * @var string
+     */
+    protected $title = null;
+
+    /**
+     * An array of renderable feed items
+     *
+     * @var array
+     */
+    protected $items = array();
+
+    /**
+     * The channel image
+     *
+     * @var channel_image
+     */
+    protected $image = null;
+
+    /**
+     * Whether or not to show the title
+     *
+     * @var boolean
+     */
+    protected $showtitle;
+
+    /**
+     * Whether or not to show the channel image
+     *
+     * @var boolean
+     */
+    protected $showimage;
+
+    /**
+     * Contructor
+     *
+     * @param string $title The title of the RSS feed
+     * @param boolean $showtitle Whether to show the title
+     * @param boolean $showimage Whether to show the channel image
+     */
+    public function __construct($title, $showtitle = true, $showimage = true) {
+        $this->title = $title;
+        $this->showtitle = $showtitle;
+        $this->showimage = $showimage;
+    }
+
+    /**
+     * Export this for use in a mustache template context.
+     *
+     * @see templatable::export_for_template()
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = array(
+            'title' => $this->showtitle ? $this->title : null,
+            'image' => null,
+            'items' => array(),
+        );
+
+        if ($this->showimage && $this->image) {
+            $data['image'] = $this->image->export_for_template($output);
+        }
+
+        foreach ($this->items as $item) {
+            $data['items'][] = $item->export_for_template($output);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Set the feed title
+     *
+     * @param string $title
+     * @return \block_rss_client\output\feed
+     */
+    public function set_title($title) {
+        $this->title = $title;
+
+        return $this;
+    }
+
+    /**
+     * Get the feed title
+     *
+     * @return string
+     */
+    public function get_title() {
+        return $this->title;
+    }
+
+    /**
+     * Add an RSS item
+     *
+     * @param \block_rss_client\output\item $item
+     */
+    public function add_item(item $item) {
+        $this->items[] = $item;
+
+        return $this;
+    }
+
+    /**
+     * Set the RSS items
+     *
+     * @param array $items An array of renderable RSS items
+     */
+    public function set_items(array $items) {
+        $this->items = $items;
+
+        return $this;
+    }
+
+    /**
+     * Get the RSS items
+     *
+     * @return array An array of renderable RSS items
+     */
+    public function get_items() {
+        return $this->items;
+    }
+
+    /**
+     * Set the channel image
+     *
+     * @param \block_rss_client\output\channel_image $image
+     */
+    public function set_image(channel_image $image) {
+        $this->image = $image;
+    }
+
+    /**
+     * Get the channel image
+     *
+     * @return channel_image
+     */
+    public function get_image() {
+        return $this->image;
+    }
+
+    /**
+     * Set showtitle
+     *
+     * @param boolean $showtitle
+     * @return \block_rss_client\output\feed
+     */
+    public function set_showtitle($showtitle) {
+        $this->showtitle = boolval($showtitle);
+
+        return $this;
+    }
+
+    /**
+     * Get showtitle
+     *
+     * @return boolean
+     */
+    public function get_showtitle() {
+        return $this->showtitle;
+    }
+
+    /**
+     * Set showimage
+     *
+     * @param boolean $showimage
+     * @return \block_rss_client\output\feed
+     */
+    public function set_showimage($showimage) {
+        $this->showimage = boolval($showimage);
+
+        return $this;
+    }
+
+    /**
+     * Get showimage
+     *
+     * @return boolean
+     */
+    public function get_showimage() {
+        return $this->showimage;
+    }
+}
diff --git a/blocks/rss_client/classes/output/footer.php b/blocks/rss_client/classes/output/footer.php
new file mode 100644 (file)
index 0000000..3da2039
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\footer
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to help display an RSS Block footer
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class footer implements \renderable, \templatable {
+
+    /**
+     * The link provided in the RSS channel
+     *
+     * @var \moodle_url
+     */
+    protected $channelurl;
+
+    /**
+     * Constructor
+     *
+     * @param \moodle_url $channelurl The link provided in the RSS channel
+     */
+    public function __construct(\moodle_url $channelurl) {
+        $this->channelurl = $channelurl;
+    }
+
+    /**
+     * Set the channel url
+     *
+     * @param \moodle_url $channelurl
+     * @return \block_rss_client\output\footer
+     */
+    public function set_channelurl(\moodle_url $channelurl) {
+        $this->channelurl = $channelurl;
+
+        return $this;
+    }
+
+    /**
+     * Get the channel url
+     *
+     * @return \moodle_url
+     */
+    public function get_channelurl() {
+        return $this->channelurl;
+    }
+
+    /**
+     * Export context for use in mustache templates
+     *
+     * @see templatable::export_for_template()
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = new \stdClass();
+        $data->channellink = clean_param($this->channelurl, PARAM_URL);
+
+        return $data;
+    }
+}
diff --git a/blocks/rss_client/classes/output/item.php b/blocks/rss_client/classes/output/item.php
new file mode 100644 (file)
index 0000000..71a71dc
--- /dev/null
@@ -0,0 +1,286 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\feed
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to help display an RSS Item
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class item implements \renderable, \templatable {
+
+    /**
+     * The unique id of the item
+     *
+     * @var string
+     */
+    protected $id;
+
+    /**
+     * The link to the item
+     *
+     * @var \moodle_url
+     */
+    protected $link;
+
+    /**
+     * The title of the item
+     *
+     * @var string
+     */
+    protected $title;
+
+    /**
+     * The description of the item
+     *
+     * @var string
+     */
+    protected $description;
+
+    /**
+     * The item's permalink
+     *
+     * @var \moodle_url
+     */
+    protected $permalink;
+
+    /**
+     * The publish date of the item in Unix timestamp format
+     *
+     * @var int
+     */
+    protected $timestamp;
+
+    /**
+     * Whether or not to show the item's description
+     *
+     * @var string
+     */
+    protected $showdescription;
+
+    /**
+     * Contructor
+     *
+     * @param string $id The id of the RSS item
+     * @param \moodle_url $link The URL of the RSS item
+     * @param string $title The title pf the RSS item
+     * @param string $description The description of the RSS item
+     * @param \moodle_url $permalink The permalink of the RSS item
+     * @param int $timestamp The Unix timestamp that represents the published date
+     * @param boolean $showdescription Whether or not to show the description
+     */
+    public function __construct($id, \moodle_url $link, $title, $description, \moodle_url $permalink, $timestamp,
+            $showdescription = true) {
+        $this->id               = $id;
+        $this->link             = $link;
+        $this->title            = $title;
+        $this->description      = $description;
+        $this->permalink        = $permalink;
+        $this->timestamp        = $timestamp;
+        $this->showdescription  = $showdescription;
+    }
+
+    /**
+     * Export context for use in mustache templates
+     *
+     * @see templatable::export_for_template()
+     * @param renderer_base $output
+     * @return array
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = array(
+            'id'            => $this->id,
+            'permalink'     => clean_param($this->permalink, PARAM_URL),
+            'datepublished' => $output->format_published_date($this->timestamp),
+            'link'          => clean_param($this->link, PARAM_URL),
+        );
+
+        // If the item does not have a title, create one from the description.
+        $title = $this->title;
+        if (!$title) {
+            $title = strip_tags($this->description);
+            $title = core_text::substr($title, 0, 20) . '...';
+        }
+
+        // Allow the renderer to format the title and description.
+        $data['title']          = $output->format_title($title);
+        $data['description']    = $this->showdescription ? $output->format_description($this->description) : null;
+
+        return $data;
+    }
+
+    /**
+     * Set id
+     *
+     * @param string $id
+     * @return \block_rss_client\output\item
+     */
+    public function set_id($id) {
+        $this->id = $id;
+
+        return $this;
+    }
+
+    /**
+     * Get id
+     *
+     * @return string
+     */
+    public function get_id() {
+        return $this->id;
+    }
+
+    /**
+     * Set link
+     *
+     * @param \moodle_url $link
+     * @return \block_rss_client\output\item
+     */
+    public function set_link(\moodle_url $link) {
+        $this->link = $link;
+
+        return $this;
+    }
+
+    /**
+     * Get link
+     *
+     * @return \moodle_url
+     */
+    public function get_link() {
+        return $this->link;
+    }
+
+    /**
+     * Set title
+     *
+     * @param string $title
+     * @return \block_rss_client\output\item
+     */
+    public function set_title($title) {
+        $this->title = $title;
+
+        return $this;
+    }
+
+    /**
+     * Get title
+     *
+     * @return string
+     */
+    public function get_title() {
+        return $this->title;
+    }
+
+    /**
+     * Set description
+     *
+     * @param string $description
+     * @return \block_rss_client\output\item
+     */
+    public function set_description($description) {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    /**
+     * Get description
+     *
+     * @return string
+     */
+    public function get_description() {
+        return $this->description;
+    }
+
+    /**
+     * Set permalink
+     *
+     * @param string $permalink
+     * @return \block_rss_client\output\item
+     */
+    public function set_permalink($permalink) {
+        $this->permalink = $permalink;
+
+        return $this;
+    }
+
+    /**
+     * Get permalink
+     *
+     * @return string
+     */
+    public function get_permalink() {
+        return $this->permalink;
+    }
+
+    /**
+     * Set timestamp
+     *
+     * @param int $timestamp
+     * @return \block_rss_client\output\item
+     */
+    public function set_timestamp($timestamp) {
+        $this->timestamp = $timestamp;
+
+        return $this;
+    }
+
+    /**
+     * Get timestamp
+     *
+     * @return string
+     */
+    public function get_timestamp() {
+        return $this->timestamp;
+    }
+
+    /**
+     * Set showdescription
+     *
+     * @param boolean $showdescription
+     * @return \block_rss_client\output\item
+     */
+    public function set_showdescription($showdescription) {
+        $this->showdescription = boolval($showdescription);
+
+        return $this;
+    }
+
+    /**
+     * Get showdescription
+     *
+     * @return boolean
+     */
+    public function get_showdescription() {
+        return $this->showdescription;
+    }
+}
diff --git a/blocks/rss_client/classes/output/renderer.php b/blocks/rss_client/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..7a03280
--- /dev/null
@@ -0,0 +1,121 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\block_renderer_html
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Renderer for RSS Client block
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+    /**
+     * Render an RSS Item
+     *
+     * @param templatable $item
+     * @return string|boolean
+     */
+    public function render_item(\templatable $item) {
+        $data = $item->export_for_template($this);
+
+        return $this->render_from_template('block_rss_client/item', $data);
+    }
+
+    /**
+     * Render an RSS Feed
+     *
+     * @param templatable $feed
+     * @return string|boolean
+     */
+    public function render_feed(\templatable $feed) {
+        $data = $feed->export_for_template($this);
+
+        return $this->render_from_template('block_rss_client/feed', $data);
+    }
+
+    /**
+     * Render an RSS feeds block
+     *
+     * @param \templatable $block
+     * @return string|boolean
+     */
+    public function render_block(\templatable $block) {
+        $data = $block->export_for_template($this);
+
+        return $this->render_from_template('block_rss_client/block', $data);
+    }
+
+    /**
+     * Render the block footer
+     *
+     * @param templatable $footer
+     * @return string|boolean
+     */
+    public function render_footer(\templatable $footer) {
+        $data = $footer->export_for_template($this);
+
+        return $this->render_from_template('block_rss_client/footer', $data);
+    }
+
+    /**
+     * Format a timestamp to use as a published date
+     *
+     * @param int $timestamp Unix timestamp
+     * @return string
+     */
+    public function format_published_date($timestamp) {
+        return strftime(get_string('strftimerecentfull', 'langconfig'), $timestamp);
+        return date('j F Y, g:i a', $timestamp);
+    }
+
+    /**
+     * Format an RSS item title
+     *
+     * @param string $title
+     * @return string
+     */
+    public function format_title($title) {
+        return break_up_long_words($title, 30);
+    }
+
+    /**
+     * Format an RSS item description
+     *
+     * @param string $description
+     * @return string
+     */
+    public function format_description($description) {
+        $description = format_text($description, FORMAT_HTML, array('para' => false));
+        $description = break_up_long_words($description, 30);
+
+        return $description;
+    }
+}
diff --git a/blocks/rss_client/templates/block.mustache b/blocks/rss_client/templates/block.mustache
new file mode 100644 (file)
index 0000000..6cc2c71
--- /dev/null
@@ -0,0 +1,91 @@
+{{!
+    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 block_rss_client/block
+
+    Template which defines an RSS Feeds block
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * feeds - array: An array of RSS feeds.
+
+    Example context (json):
+    {
+        "feeds": [
+            {
+                "title": "News from around my living room",
+                "image": {
+                    "url": "https://www.example.com/feeds/news/poster.jpg",
+                    "title": "Example News Logo",
+                    "link": "https://www.example.com/feeds/news/"
+                },
+                "items": [
+                    {
+                        "id": "https://www.example.com/node/12",
+                        "link": "https://www.example.com/my-turtle-story.html",
+                        "title": "My Turtle Story",
+                        "description": "This is a story about my turtle.",
+                        "permalink": "https://www.example.com/my-turtle-story.html",
+                        "datepublished": "11 January 2016, 7:11 pm"
+                    },
+                    {
+                        "id": "https://www.example.com/node/12",
+                        "link": "https://www.example.com/my-cat-story.html",
+                        "title": "My Story",
+                        "description": "This is a story about my cats.",
+                        "permalink": "https://www.example.com/my-cat-story.html",
+                        "datepublished": "12 January 2016, 9:12 pm"
+                    }
+                ]
+            },
+            {
+                "title": "News from around my kitchen",
+                "image": {
+                    "url": "https://www.example.com/feeds/news/kitchen.jpg",
+                    "title": "Picture of My Kitchen",
+                    "link": "https://www.example.com/feeds/news/kitchen/"
+                },
+                "items": [
+                    {
+                        "id": "https://www.example.com/node/10",
+                        "link": "https://www.example.com/oven-smoke.html",
+                        "title": "Why is the Oven Smoking?",
+                        "description": "There is something smoking in the oven.",
+                        "permalink": "https://www.example.com/oven-smoke.html",
+                        "datepublished": "10 January 2016, 1:13 pm"
+                    },
+                    {
+                        "id": "https://www.example.com/node/13",
+                        "link": "https://www.example.com/coffee-is-good.html",
+                        "title": "Why My Coffee Machine is So Great!",
+                        "description": "Don't be fancy; drips are best.",
+                        "permalink": "https://www.example.com/oven-smoke.html",
+                        "datepublished": "13 January 2016, 8:25 pm"
+                    }
+                ]
+            }
+        ]
+    }
+}}
+{{#feeds}}
+    {{> block_rss_client/feed}}
+{{/feeds}}
diff --git a/blocks/rss_client/templates/channel_image.mustache b/blocks/rss_client/templates/channel_image.mustache
new file mode 100644 (file)
index 0000000..f20166e
--- /dev/null
@@ -0,0 +1,50 @@
+{{!
+    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 block_rss_client/channel_image
+
+    Template which defines an item in an RSS Feed
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * url - string: The escaped URL of the image.
+    * title - string: The title of the image.
+    * link - string: Optionally, a URL to link the image to. Must be escaped.
+
+    Example context (json):
+    {
+        "url": "http://www.example.com/images/catpic.jpg",
+        "title": "A picture of my cat",
+        "link": "http://www.example.com/cat-news/"
+    }
+}}
+<div class="image" title="{{title}}">
+    {{#link}}
+        <a href="{{{link}}}">
+    {{/link}}
+
+    <img src="{{{url}}}" alt="{{title}}" />
+
+    {{#link}}
+        </a>
+    {{/link}}
+</div>
diff --git a/blocks/rss_client/templates/feed.mustache b/blocks/rss_client/templates/feed.mustache
new file mode 100644 (file)
index 0000000..ad9ae3e
--- /dev/null
@@ -0,0 +1,79 @@
+{{!
+    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 block_rss_client/feed
+
+    Template which defines an item in an RSS Feed
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * channel_image - object: URL, title and link for the channel image.
+    * title - string: The title of the feed.
+    * items - array: An array of feed items.
+
+    Example context (json):
+    {
+        "title": "News from around my living room",
+        "image": {
+            "url": "https://www.example.com/feeds/news/poster.jpg",
+            "title": "Example News Logo",
+            "link": "https://www.example.com/feeds/news/"
+        },
+        "feeditems": [
+            {
+                "id": "https://www.example.com/node/12",
+                "link": "https://www.example.com/my-turtle-story.html",
+                "title": "My Turtle Story",
+                "description": "This is a story about my turtle.",
+                "permalink": "https://www.example.com/my-turtle-story.html",
+                "datepublished": "11 January 2016, 7:11 pm"
+            },
+            {
+                "id": "https://www.example.com/node/12",
+                "link": "https://www.example.com/my-cat-story.html",
+                "title": "My Story",
+                "description": "This is a story about my cats.",
+                "permalink": "https://www.example.com/my-cat-story.html",
+                "datepublished": "12 January 2016, 9:12 pm"
+            }
+        ]
+    }
+}}
+{{$image}}
+    {{#image}}
+        {{> block_rss_client/channel_image}}
+    {{/image}}
+{{/image}}
+
+{{$title}}
+    {{#title}}
+        <div class="title">{{feedtitle}}</div>
+    {{/title}}
+{{/title}}
+
+{{$items}}
+    <ul class="list no-overflow">
+        {{#items}}
+            {{> block_rss_client/item}}
+        {{/items}}
+    </ul>
+{{/items}}
diff --git a/blocks/rss_client/templates/footer.mustache b/blocks/rss_client/templates/footer.mustache
new file mode 100644 (file)
index 0000000..b1aa373
--- /dev/null
@@ -0,0 +1,36 @@
+{{!
+    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 block_rss_client/footer
+
+    Template which defines an item in an RSS Feed
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * channellink - string: The channel URL. Must be escaped.
+
+    Example context (json):
+    {
+        "channellink": "https://www.example.com/feeds/rss"
+    }
+}}
+<a href="{{{channellink}}}">{{#str}} clientchannellink, block_rss_client {{/str}}</a>
diff --git a/blocks/rss_client/templates/item.mustache b/blocks/rss_client/templates/item.mustache
new file mode 100644 (file)
index 0000000..b21bf11
--- /dev/null
@@ -0,0 +1,60 @@
+{{!
+    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 block_rss_client/item
+
+    Template which defines an item in an RSS Feed
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * id - string: A unique id for the feed item.
+    * link - string: The URL of the feed item. Must already be escaped.
+    * title - string: The title of the feed item.
+    * description - string: The text description of the feed item.
+    * permalink - string: The permalink of the feed item. Must already be escaped.
+    * datepublished - string: The date the feed item was published.
+
+    Example context (json):
+    {
+        "id": "https://www.example.com/node",
+        "link": "https://www.example.com/my-cat-story.html",
+        "title": "My Story",
+        "description": "This is a story about my cats.",
+        "permalink": "https://www.example.com/my-cat-story.html",
+        "datepublished": "12 January 2016, 9:12 pm"
+    }
+}}
+<li>
+    {{$title}}
+        <div class="link">
+            <a href="{{{link}}}" onclick='this.target="_blank"'>{{title}}</a>
+        </div>
+    {{/title}}
+
+    {{$content}}
+        {{#description}}
+            <div class="description">
+                {{{description}}}
+            </div>
+        {{/description}}
+    {{/content}}
+</li>
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..58a9fbc
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..e7789e9
--- /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 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() {
+            new Tree(".block_settings .block_tree");
+        }
+    };
+});
index 14c486b..ccb0af2 100644 (file)
@@ -96,7 +96,7 @@ class block_settings extends block_base {
             '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());
     }
 
     /**
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 0ad0fb2..ff3a9e9 100644 (file)
@@ -70,10 +70,23 @@ class calendar_preferences_form extends moodleform {
         $mform->addElement('select', 'maxevents', get_string('pref_maxevents', 'calendar'), $options);
         $mform->addHelpButton('maxevents', 'pref_maxevents', 'calendar');
 
-        $options = array();
-        for ($i=1; $i<=99; $i++) {
-            $options[$i] = $i;
-        }
+        $options = array(365 => new lang_string('numyear', '', 1),
+                270 => get_string('nummonths', '', 9),
+                180 => get_string('nummonths', '', 6),
+                150 => get_string('nummonths', '', 5),
+                120 => get_string('nummonths', '', 4),
+                90  => get_string('nummonths', '', 3),
+                60  => get_string('nummonths', '', 2),
+                30  => get_string('nummonth', '', 1),
+                21  => get_string('numweeks', '', 3),
+                14  => get_string('numweeks', '', 2),
+                7  => get_string('numweek', '', 1),
+                6  => get_string('numdays', '', 6),
+                5  => get_string('numdays', '', 5),
+                4  => get_string('numdays', '', 4),
+                3  => get_string('numdays', '', 3),
+                2  => get_string('numdays', '', 2),
+                1  => get_string('numday', '', 1));
         $mform->addElement('select', 'lookahead', get_string('pref_lookahead', 'calendar'), $options);
         $mform->addHelpButton('lookahead', 'pref_lookahead', 'calendar');
 
diff --git a/calendar/tests/behat/calendar_lookahead.feature b/calendar/tests/behat/calendar_lookahead.feature
new file mode 100644 (file)
index 0000000..e95c018
--- /dev/null
@@ -0,0 +1,38 @@
+@core @core_calendar
+Feature: Limit displayed upcoming events
+  In order to filter what is displayed on the calendar
+  As a user
+  I need to interact with the calendar
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+
+  Scenario: I view calendar details for a future event
+    Given I follow "C1"
+    And I turn editing mode on
+    And I add the "Calendar" block
+    And I follow "This month"
+    And I click on "a.next" "css_element"
+    And I click on "a.next" "css_element"
+    And I create a calendar event:
+      | Type of event     | course |
+      | Event title       | Two months away event |
+    When I follow "C1"
+    Then I should not see "Two months away event"
+    And I follow "Go to calendar"
+    And I click on "Preferences" "button"
+    And I set the following fields to these values:
+      | Upcoming events look-ahead | 3 months |
+    And I press "Save changes"
+    And I wait to be redirected
+    And I follow "C1"
+    And I should see "Two months away event"
index 88defef..233be52 100644 (file)
@@ -208,7 +208,7 @@ class core_cohort_external extends external_api {
         return new external_function_parameters(
             array(
                 'cohortids' => new external_multiple_structure(new external_value(PARAM_INT, 'Cohort ID')
-                    , 'List of cohort id. A cohort id is an integer.'),
+                    , 'List of cohort id. A cohort id is an integer.', VALUE_DEFAULT, array()),
             )
         );
     }
@@ -220,16 +220,19 @@ class core_cohort_external extends external_api {
      * @return array of cohort objects (id, courseid, name)
      * @since Moodle 2.5
      */
-    public static function get_cohorts($cohortids) {
+    public static function get_cohorts($cohortids = array()) {
         global $DB;
 
         $params = self::validate_parameters(self::get_cohorts_parameters(), array('cohortids' => $cohortids));
 
-        $cohorts = array();
-        foreach ($params['cohortids'] as $cohortid) {
-            // Validate params.
-            $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
+        if (empty($cohortids)) {
+            $cohorts = $DB->get_records('cohort');
+        } else {
+            $cohorts = $DB->get_records_list('cohort', 'id', $params['cohortids']);
+        }
 
+        $cohortsinfo = array();
+        foreach ($cohorts as $cohort) {
             // Now security checks.
             $context = context::instance_by_id($cohort->contextid, MUST_EXIST);
             if ($context->contextlevel != CONTEXT_COURSECAT and $context->contextlevel != CONTEXT_SYSTEM) {
@@ -244,12 +247,12 @@ class core_cohort_external extends external_api {
                 external_format_text($cohort->description, $cohort->descriptionformat,
                         $context->id, 'cohort', 'description', $cohort->id);
 
-            $cohorts[] = (array) $cohort;
+            $cohortsinfo[] = (array) $cohort;
         }
-
-        return $cohorts;
+        return $cohortsinfo;
     }
 
+
     /**
      * Returns description of method result value
      *
index d776e6f..eb61247 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /cohort/ information provided here is intended
 especially for developers.
 
+=== 3.1 ===
+* The Webservice core_cohort_get_cohorts now has the added functionality of getting all cohorts
+  by not passing any parameters
+
 === 2.6 ===
 * Webservice core_cohort_update_cohorts was incorrectly specifiying float as the parameter type
   for cohort id. This field is actually int and input is now reported and processed as such.
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 0e0a143..77a34a5 100644 (file)
@@ -314,8 +314,9 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 $streditsection = get_string('editsection');
             }
 
+            $sectionreturn = $onsectionpage ? $section->section : 0;
             $controls['edit'] = array(
-                'url'   => new moodle_url('/course/editsection.php', array('id' => $section->id, 'sr' => $onsectionpage)),
+                'url'   => new moodle_url('/course/editsection.php', array('id' => $section->id, 'sr' => $sectionreturn)),
                 'icon' => 'i/settings',
                 'name' => $streditsection,
                 'pixattr' => array('class' => '', 'alt' => $streditsection),
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 2ac3c38..be471bd 100644 (file)
@@ -83,7 +83,7 @@ class enrol_ldap_testcase extends advanced_testcase {
         $enrol->set_config('start_tls', 0);
         $enrol->set_config('ldap_version', 3);
         $enrol->set_config('ldapencoding', 'utf-8');
-        $enrol->set_config('page_size', '2');
+        $enrol->set_config('pagesize', '2');
         $enrol->set_config('bind_dn', TEST_ENROL_LDAP_BIND_DN);
         $enrol->set_config('bind_pw', TEST_ENROL_LDAP_BIND_PW);
         $enrol->set_config('course_search_sub', 0);
index 94e329a..67aaf11 100644 (file)
@@ -50,8 +50,8 @@ class gradingform_rubric_editrubric extends moodleform {
         $form->setType('returnurl', PARAM_LOCALURL);
 
         // name
-        $form->addElement('text', 'name', get_string('name', 'gradingform_rubric'), array('size'=>52));
-        $form->addRule('name', get_string('required'), 'required');
+        $form->addElement('text', 'name', get_string('name', 'gradingform_rubric'), array('size' => 52, 'aria-required' => 'true'));
+        $form->addRule('name', get_string('required'), 'required', null, 'client');
         $form->setType('name', PARAM_TEXT);
 
         // description
index d44639b..dbed9a8 100644 (file)
@@ -5,6 +5,10 @@ M.gradingform_rubric = {};
  */
 M.gradingform_rubric.init = function(Y, options) {
     Y.on('click', M.gradingform_rubric.levelclick, '#rubric-'+options.name+' .level', null, Y, options.name);
+    // Capture also space and enter keypress.
+    Y.on('key', M.gradingform_rubric.levelclick, '#rubric-' + options.name + ' .level', 'space', Y, options.name);
+    Y.on('key', M.gradingform_rubric.levelclick, '#rubric-' + options.name + ' .level', 'enter', Y, options.name);
+
     Y.all('#rubric-'+options.name+' .radio').setStyle('display', 'none')
     Y.all('#rubric-'+options.name+' .level').each(function (node) {
       if (node.one('input[type=radio]').get('checked')) {
@@ -19,12 +23,19 @@ M.gradingform_rubric.levelclick = function(e, Y, name) {
     if (!el) return
     e.preventDefault();
     el.siblings().removeClass('checked');
+
+    // Set aria-checked attribute for siblings to false.
+    el.siblings().setAttribute('aria-checked', 'false');
     chb = el.one('input[type=radio]')
     if (!chb.get('checked')) {
         chb.set('checked', true)
         el.addClass('checked')
+        // Set aria-checked attribute to true if checked.
+        el.setAttribute('aria-checked', 'true');
     } else {
         el.removeClass('checked');
+        // Set aria-checked attribute to false if unchecked.
+        el.setAttribute('aria-checked', 'false');
         el.get('parentNode').all('input[type=radio]').set('checked', false)
     }
 }
index 5304b82..7b84e29 100644 (file)
@@ -143,9 +143,11 @@ M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
         elements_str = '#rubric-'+name+' .criterion'
     }
     // prepare the id of the next inserted level or criterion
+    var newlevid = 0;
+    var newid = 0;
     if (action == 'addcriterion' || action == 'addlevel' || action == 'duplicate' ) {
-        var newid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .criterion')
-        var newlevid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .level')
+        newid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .criterion');
+        newlevid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .level');
     }
     var dialog_options = {
         'scope' : this,
@@ -164,7 +166,10 @@ M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
         for (levidx;levidx<3;levidx++) levelsscores[levidx] = parseFloat(levelsscores[levidx-1])+1
         var levelsstr = '';
         for (levidx=0;levidx<levelsscores.length;levidx++) {
-            levelsstr += M.gradingform_rubriceditor.templates[name]['level'].replace(/\{LEVEL-id\}/g, 'NEWID'+(newlevid+levidx)).replace(/\{LEVEL-score\}/g, levelsscores[levidx])
+            levelsstr += M.gradingform_rubriceditor.templates[name].level.
+                replace(/\{LEVEL-id\}/g, 'NEWID'+(newlevid+levidx)).
+                replace(/\{LEVEL-score\}/g, levelsscores[levidx]).
+                replace(/\{LEVEL-index\}/g, levidx + 1);
         }
         var newcriterion = M.gradingform_rubriceditor.templates[name]['criterion'].replace(/\{LEVELS\}/, levelsstr)
         parentel.append(newcriterion.replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, ''))
@@ -172,14 +177,23 @@ M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
         M.gradingform_rubriceditor.addhandlers();
         M.gradingform_rubriceditor.disablealleditors()
         M.gradingform_rubriceditor.assignclasses(elements_str)
-        M.gradingform_rubriceditor.editmode(Y.one('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-description'),true)
+        M.gradingform_rubriceditor.editmode(
+            Y.one('#rubric-' + name + ' #' + name + '-criteria-NEWID' + newid + '-description-cell'), true
+        );
     } else if (chunks.length == 5 && action == 'addlevel') {
         // ADD NEW LEVEL
         var newscore = 0;
         parent = Y.one('#'+name+'-criteria-'+chunks[2]+'-levels')
-        parent.all('.level').each(function (node) { newscore = Math.max(newscore, parseFloat(node.one('.score input[type=text]').get('value'))+1) })
+        var levelIndex = 1;
+        parent.all('.level').each(function (node) {
+            newscore = Math.max(newscore, parseFloat(node.one('.score input[type=text]').get('value')) + 1);
+            levelIndex++;
+        });
         var newlevel = M.gradingform_rubriceditor.templates[name]['level'].
-            replace(/\{CRITERION-id\}/g, chunks[2]).replace(/\{LEVEL-id\}/g, 'NEWID'+newlevid).replace(/\{LEVEL-score\}/g, newscore).replace(/\{.+?\}/g, '')
+            replace(/\{CRITERION-id\}/g, chunks[2]).replace(/\{LEVEL-id\}/g, 'NEWID'+newlevid).
+            replace(/\{LEVEL-score\}/g, newscore).
+            replace(/\{LEVEL-index\}/g, levelIndex).
+            replace(/\{.+?\}/g, '');
         parent.append(newlevel)
         M.gradingform_rubriceditor.addhandlers();
         M.gradingform_rubriceditor.disablealleditors()
@@ -238,7 +252,7 @@ M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
         M.gradingform_rubriceditor.addhandlers();
         M.gradingform_rubriceditor.disablealleditors();
         M.gradingform_rubriceditor.assignclasses(elements_str);
-        M.gradingform_rubriceditor.editmode(Y.one('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-description'),true);
+        M.gradingform_rubriceditor.editmode(Y.one('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-description-cell'),true);
     } else if (chunks.length == 6 && action == 'delete') {
         // DELETE LEVEL
         if (confirmed) {
index f23b914..cf9455d 100644 (file)
@@ -29,12 +29,14 @@ $string['alwaysshowdefinition'] = 'Allow users to preview rubric used in the mod
 $string['backtoediting'] = 'Back to editing';
 $string['confirmdeletecriterion'] = 'Are you sure you want to delete this criterion?';
 $string['confirmdeletelevel'] = 'Are you sure you want to delete this level?';
+$string['criterion'] = 'Criterion {$a}';
 $string['criterionaddlevel'] = 'Add level';
 $string['criteriondelete'] = 'Delete criterion';
 $string['criterionduplicate'] = 'Duplicate criterion';
 $string['criterionempty'] = 'Click to edit criterion';
 $string['criterionmovedown'] = 'Move down';
 $string['criterionmoveup'] = 'Move up';
+$string['criterionremark'] = 'Remark for criterion {$a->description}: {$a->remark}';
 $string['definerubric'] = 'Define rubric';
 $string['description'] = 'Description';
 $string['enableremarks'] = 'Allow grader to add text remarks for each criterion';
@@ -45,8 +47,11 @@ $string['err_nodescription'] = 'Criterion description can not be empty';
 $string['err_scoreformat'] = 'Number of points for each level must be a valid non-negative number';
 $string['err_totalscore'] = 'Maximum number of points possible when graded by the rubric must be more than zero';
 $string['gradingof'] = '{$a} grading';
-$string['leveldelete'] = 'Delete level';
+$string['level'] = 'Level {$a->definition}, {$a->score} points.';
+$string['leveldelete'] = 'Delete level {$a}';
+$string['leveldefinition'] = 'Level {$a} definition';
 $string['levelempty'] = 'Click to edit level';
+$string['levelsgroup'] = 'Levels group';
 $string['name'] = 'Name';
 $string['needregrademessage'] = 'The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade.';
 $string['pluginname'] = 'Rubric';
@@ -68,6 +73,7 @@ $string['rubricstatus'] = 'Current rubric status';
 $string['save'] = 'Save';
 $string['saverubric'] = 'Save rubric and make it ready';
 $string['saverubricdraft'] = 'Save as draft';
+$string['scoreinputforlevel'] = 'Score input for level {$a}';
 $string['scorepostfix'] = '{$a}points';
 $string['showdescriptionstudent'] = 'Display rubric description to those being graded';
 $string['showdescriptionteacher'] = 'Display rubric description during evaluation';
index d4d4b6f..9504b42 100644 (file)
@@ -927,7 +927,8 @@ class gradingform_rubric_instance extends gradingform_instance {
         }
         $currentinstance = $this->get_current_instance();
         if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) {
-            $html .= html_writer::tag('div', get_string('needregrademessage', 'gradingform_rubric'), array('class' => 'gradingform_rubric-regrade'));
+            $html .= html_writer::div(get_string('needregrademessage', 'gradingform_rubric'), 'gradingform_rubric-regrade',
+                                      array('role' => 'alert'));
         }
         $haschanges = false;
         if ($currentinstance) {
index c539535..6ea8a4b 100644 (file)
@@ -74,12 +74,22 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             foreach (array('moveup', 'delete', 'movedown', 'duplicate') as $key) {
                 $value = get_string('criterion'.$key, 'gradingform_rubric');
                 $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}]['.$key.']',
-                    'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value, 'tabindex' => -1));
+                    'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value));
                 $criteriontemplate .= html_writer::tag('div', $button, array('class' => $key));
             }
+            $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
+                                                                        'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]',
+                                                                        'value' => $criterion['sortorder']));
             $criteriontemplate .= html_writer::end_tag('td'); // .controls
-            $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
-            $description = html_writer::tag('textarea', s($criterion['description']), array('name' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '10', 'rows' => '5'));
+
+            // Criterion description text area.
+            $descriptiontextareaparams = array(
+                'name' => '{NAME}[criteria][{CRITERION-id}][description]',
+                'id' => '{NAME}-criteria-{CRITERION-id}-description',
+                'aria-label' => get_string('criterion', 'gradingform_rubric', ''),
+                'cols' => '10', 'rows' => '5'
+            );
+            $description = html_writer::tag('textarea', s($criterion['description']), $descriptiontextareaparams);
         } else {
             if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
                 $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
@@ -91,8 +101,35 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         if (isset($criterion['error_description'])) {
             $descriptionclass .= ' error';
         }
-        $criteriontemplate .= html_writer::tag('td', $description, array('class' => $descriptionclass, 'id' => '{NAME}-criteria-{CRITERION-id}-description'));
-        $levelsstrtable = html_writer::tag('table', html_writer::tag('tr', $levelsstr, array('id' => '{NAME}-criteria-{CRITERION-id}-levels')));
+
+        // Description cell params.
+        $descriptiontdparams = array(
+            'class' => $descriptionclass,
+            'id' => '{NAME}-criteria-{CRITERION-id}-description-cell'
+        );
+        if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL &&
+            $mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
+            // Set description's cell as tab-focusable.
+            $descriptiontdparams['tabindex'] = '0';
+            // Set label for the criterion cell.
+            $descriptiontdparams['aria-label'] = get_string('criterion', 'gradingform_rubric', s($criterion['description']));
+        }
+
+        // Description cell.
+        $criteriontemplate .= html_writer::tag('td', $description, $descriptiontdparams);
+
+        // Levels table.
+        $levelsrowparams = array('id' => '{NAME}-criteria-{CRITERION-id}-levels');
+        if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
+            $levelsrowparams['role'] = 'radiogroup';
+        }
+        $levelsrow = html_writer::tag('tr', $levelsstr, $levelsrowparams);
+
+        $levelstableparams = array(
+            'id' => '{NAME}-criteria-{CRITERION-id}-levels-table',
+            'aria-label' => get_string('levelsgroup', 'gradingform_rubric')
+        );
+        $levelsstrtable = html_writer::tag('table', $levelsrow, $levelstableparams);
         $levelsclass = 'levels';
         if (isset($criterion['error_levels'])) {
             $levelsclass .= ' error';
@@ -101,7 +138,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
             $value = get_string('criterionaddlevel', 'gradingform_rubric');
             $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][addlevel]',
-                'id' => '{NAME}-criteria-{CRITERION-id}-levels-addlevel', 'value' => $value, 'title' => $value));
+                'id' => '{NAME}-criteria-{CRITERION-id}-levels-addlevel', 'value' => $value));
             $criteriontemplate .= html_writer::tag('td', $button, array('class' => 'addlevel'));
         }
         $displayremark = ($options['enableremarks'] && ($mode != gradingform_rubric_controller::DISPLAY_VIEW || $options['showremarksstudent']));
@@ -110,13 +147,34 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             if (isset($value['remark'])) {
                 $currentremark = $value['remark'];
             }
+
+            // Label for criterion remark.
+            $remarkinfo = new stdClass();
+            $remarkinfo->description = s($criterion['description']);
+            $remarkinfo->remark = $currentremark;
+            $remarklabeltext = get_string('criterionremark', 'gradingform_rubric', $remarkinfo);
+
             if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
-                $input = html_writer::tag('textarea', s($currentremark), array('name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'cols' => '10', 'rows' => '5'));
+                // HTML parameters for remarks text area.
+                $remarkparams = array(
+                    'name' => '{NAME}[criteria][{CRITERION-id}][remark]',
+                    'id' => '{NAME}-criteria-{CRITERION-id}-remark',
+                    'cols' => '10', 'rows' => '5',
+                    'aria-label' => $remarklabeltext
+                );
+                $input = html_writer::tag('textarea', s($currentremark), $remarkparams);
                 $criteriontemplate .= html_writer::tag('td', $input, array('class' => 'remark'));
             } else if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN) {
                 $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark));
             }else if ($mode == gradingform_rubric_controller::DISPLAY_REVIEW || $mode == gradingform_rubric_controller::DISPLAY_VIEW) {
-                $criteriontemplate .= html_writer::tag('td', s($currentremark), array('class' => 'remark'));
+                // HTML parameters for remarks cell.
+                $remarkparams = array(
+                    'class' => 'remark',
+                    'tabindex' => '0',
+                    'id' => '{NAME}-criteria-{CRITERION-id}-remark',
+                    'aria-label' => $remarklabeltext
+                );
+                $criteriontemplate .= html_writer::tag('td', s($currentremark), $remarkparams);
             }
         }
         $criteriontemplate .= html_writer::end_tag('tr'); // .criterion
@@ -153,7 +211,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         if (!isset($level['id'])) {
             $level = array('id' => '{LEVEL-id}', 'definition' => '{LEVEL-definition}', 'score' => '{LEVEL-score}', 'class' => '{LEVEL-class}', 'checked' => false);
         } else {
-            foreach (array('score', 'definition', 'class', 'checked') as $key) {
+            foreach (array('score', 'definition', 'class', 'checked', 'index') as $key) {
                 // set missing array elements to empty strings to avoid warnings
                 if (!array_key_exists($key, $level)) {
                     $level[$key] = '';
@@ -161,17 +219,37 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             }
         }
 
+        // Get level index.
+        $levelindex = isset($level['index']) ? $level['index'] : '{LEVEL-index}';
+
         // Template for one level within one criterion
-        $tdattributes = array('id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}', 'class' => 'level'. $level['class']);
+        $tdattributes = array(
+            'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}',
+            'class' => 'level' . $level['class']
+        );
         if (isset($level['tdwidth'])) {
             $tdattributes['width'] = round($level['tdwidth']).'%';
         }
-        $leveltemplate = html_writer::start_tag('td', $tdattributes);
-        $leveltemplate .= html_writer::start_tag('div', array('class' => 'level-wrapper'));
+
+        $leveltemplate = html_writer::start_tag('div', array('class' => 'level-wrapper'));
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
-            $definition = html_writer::tag('textarea', s($level['definition']), array('name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'cols' => '10', 'rows' => '4'));
-            $score = html_writer::label(get_string('criterionempty', 'gradingform_rubric'), '{NAME}criteria{CRITERION-id}levels{LEVEL-id}', false, array('class' => 'accesshide'));
-            $score .= html_writer::empty_tag('input', array('type' => 'text','id' => '{NAME}criteria{CRITERION-id}levels{LEVEL-id}', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'size' => '3', 'value' => $level['score']));
+            $definitionparams = array(
+                'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition',
+                'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]',
+                'aria-label' => get_string('leveldefinition', 'gradingform_rubric', $levelindex),
+                'cols' => '10', 'rows' => '4'
+            );
+            $definition = html_writer::tag('textarea', s($level['definition']), $definitionparams);
+
+            $scoreparams = array(
+                'type' => 'text',
+                'id' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]',
+                'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]',
+                'aria-label' => get_string('scoreinputforlevel', 'gradingform_rubric', $levelindex),
+                'size' => '3',
+                'value' => $level['score']
+            );
+            $score = html_writer::empty_tag('input', $scoreparams);
         } else {
             if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
                 $leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'value' => $level['definition']));
@@ -181,19 +259,58 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             $score = $level['score'];
         }
         if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
-            $input = html_writer::empty_tag('input', array('type' => 'radio', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id']) +
-                    ($level['checked'] ? array('checked' => 'checked') : array()));
-            $leveltemplate .= html_writer::tag('div', $input, array('class' => 'radio'));
+            $levelradioparams = array(
+                'type' => 'radio',
+                'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition',
+                'name' => '{NAME}[criteria][{CRITERION-id}][levelid]',
+                'value' => $level['id']
+            );
+            if ($level['checked']) {
+                $levelradioparams['checked'] = 'checked';
+            }
+            $input = html_writer::empty_tag('input', $levelradioparams);
+            $leveltemplate .= html_writer::div($input, 'radio');
         }
         if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN && $level['checked']) {
-            $leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id']));
+            $leveltemplate .= html_writer::empty_tag('input',
+                array(
+                    'type' => 'hidden',
+                    'name' => '{NAME}[criteria][{CRITERION-id}][levelid]',
+                    'value' => $level['id']
+                )
+            );
         }
         $score = html_writer::tag('span', $score, array('id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-score', 'class' => 'scorevalue'));
         $definitionclass = 'definition';
         if (isset($level['error_definition'])) {
             $definitionclass .= ' error';
         }
-        $leveltemplate .= html_writer::tag('div', $definition, array('class' => $definitionclass, 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition'));
+
+        if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL &&
+            $mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
+
+            $tdattributes['tabindex'] = '0';
+            $levelinfo = new stdClass();
+            $levelinfo->definition = s($level['definition']);
+            $levelinfo->score = $level['score'];
+            $tdattributes['aria-label'] = get_string('level', 'gradingform_rubric', $levelinfo);
+
+            if ($mode != gradingform_rubric_controller::DISPLAY_PREVIEW &&
+                $mode != gradingform_rubric_controller::DISPLAY_PREVIEW_GRADED) {
+                // Add role of radio button to level cell if not in edit and preview mode.
+                $tdattributes['role'] = 'radio';
+                if ($level['checked']) {
+                    $tdattributes['aria-checked'] = 'true';
+                } else {
+                    $tdattributes['aria-checked'] = 'false';
+                }
+            }
+        }
+
+        $leveltemplateparams = array(
+            'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition-container'
+        );
+        $leveltemplate .= html_writer::div($definition, $definitionclass, $leveltemplateparams);
         $displayscore = true;
         if (!$options['showscoreteacher'] && in_array($mode, array(gradingform_rubric_controller::DISPLAY_EVAL, gradingform_rubric_controller::DISPLAY_EVAL_FROZEN, gradingform_rubric_controller::DISPLAY_REVIEW))) {
             $displayscore = false;
@@ -209,12 +326,19 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             $leveltemplate .= html_writer::tag('div', get_string('scorepostfix', 'gradingform_rubric', $score), array('class' => $scoreclass));
         }
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
-            $value = get_string('leveldelete', 'gradingform_rubric');
-            $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][delete]', 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-delete', 'value' => $value, 'title' => $value, 'tabindex' => -1));
+            $value = get_string('leveldelete', 'gradingform_rubric', $levelindex);
+            $buttonparams = array(
+                'type' => 'submit',
+                'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][delete]',
+                'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-delete',
+                'value' => $value
+            );
+            $button = html_writer::empty_tag('input', $buttonparams);
             $leveltemplate .= html_writer::tag('div', $button, array('class' => 'delete'));
         }
         $leveltemplate .= html_writer::end_tag('div'); // .level-wrapper
-        $leveltemplate .= html_writer::end_tag('td'); // .level
+
+        $leveltemplate = html_writer::tag('td', $leveltemplate, $tdattributes); // The .level cell.
 
         $leveltemplate = str_replace('{NAME}', $elementname, $leveltemplate);
         $leveltemplate = str_replace('{CRITERION-id}', $criterionid, $leveltemplate);
@@ -262,10 +386,23 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         }
 
         $rubrictemplate = html_writer::start_tag('div', array('id' => 'rubric-{NAME}', 'class' => 'clearfix gradingform_rubric'.$classsuffix));
-        $rubrictemplate .= html_writer::tag('table', $criteriastr, array('class' => 'criteria', 'id' => '{NAME}-criteria'));
+
+        // Rubric table.
+        $rubrictableparams = array(
+            'class' => 'criteria',
+            'id' => '{NAME}-criteria',
+            'aria-label' => get_string('rubric', 'gradingform_rubric'));
+        $rubrictable = html_writer::tag('table', $criteriastr, $rubrictableparams);
+        $rubrictemplate .= $rubrictable;
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
             $value = get_string('addcriterion', 'gradingform_rubric');
-            $input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][addcriterion]', 'id' => '{NAME}-criteria-addcriterion', 'value' => $value, 'title' => $value));
+            $criteriainputparams = array(
+                'type' => 'submit',
+                'name' => '{NAME}[criteria][addcriterion]',
+                'id' => '{NAME}-criteria-addcriterion',
+                'value' => $value
+            );
+            $input = html_writer::empty_tag('input', $criteriainputparams);
             $rubrictemplate .= html_writer::tag('div', $input, array('class' => 'addcriterion'));
         }
         $rubrictemplate .= $this->rubric_edit_options($mode, $options);
@@ -364,6 +501,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             } else {
                 $criterionvalue = null;
             }
+            $index = 1;
             foreach ($criterion['levels'] as $levelid => $level) {
                 $level['id'] = $levelid;
                 $level['class'] = $this->get_css_class_suffix($levelcnt++, sizeof($criterion['levels']) -1);
@@ -376,7 +514,9 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
                     $level['class'] .= ' currentchecked';
                 }
                 $level['tdwidth'] = 100/count($criterion['levels']);
+                $level['index'] = $index;
                 $levelsstr .= $this->level_template($mode, $options, $elementname, $id, $level);
+                $index++;
             }
             $criteriastr .= $this->criterion_template($mode, $options, $elementname, $criterion, $levelsstr, $criterionvalue);
         }
@@ -462,7 +602,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
      * @return string
      */
     public function display_regrade_confirmation($elementname, $changelevel, $value) {
-        $html = html_writer::start_tag('div', array('class' => 'gradingform_rubric-regrade'));
+        $html = html_writer::start_tag('div', array('class' => 'gradingform_rubric-regrade', 'role' => 'alert'));
         if ($changelevel<=2) {
             $html .= html_writer::label(get_string('regrademessage1', 'gradingform_rubric'), 'menu' . $elementname . 'regrade');
             $selectoptions = array(
index d990126..533e81e 100644 (file)
@@ -119,7 +119,7 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
                 'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'),
                 'strings' => array(array('confirmdeletecriterion', 'gradingform_rubric'), array('confirmdeletelevel', 'gradingform_rubric'),
                     array('criterionempty', 'gradingform_rubric'), array('levelempty', 'gradingform_rubric')
-                    ));
+                ));
             $PAGE->requires->js_init_call('M.gradingform_rubriceditor.init', array(
                 array('name' => $this->getName(),
                     'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()),
@@ -141,7 +141,7 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
             $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
         }
         if ($this->validationerrors) {
-            $html .= $renderer->notification($this->validationerrors, 'error');
+            $html .= html_writer::div($renderer->notification($this->validationerrors, 'error'), '', array('role' => 'alert'));
         }
         $html .= $renderer->display_rubric($data['criteria'], $data['options'], $mode, $this->getName());
         return $html;
index 4ab11d2..e01387b 100644 (file)
@@ -81,7 +81,7 @@ class behat_gradingform_rubric extends behat_base {
         $addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_rubric'));
 
         // Cleaning the current ones.
-        $deletebuttons = $this->find_all('css', "input[title='" . get_string('criteriondelete', 'gradingform_rubric') . "']");
+        $deletebuttons = $this->find_all('css', "input[value='" . get_string('criteriondelete', 'gradingform_rubric') . "']");
         if ($deletebuttons) {
 
             // We should reverse the deletebuttons because otherwise once we delete
@@ -100,6 +100,12 @@ class behat_gradingform_rubric extends behat_base {
 
         if ($criteria) {
             foreach ($criteria as $criterionit => $criterion) {
+                // Unset empty levels in criterion.
+                foreach ($criterion as $i => $value) {
+                    if (empty($value)) {
+                        unset($criterion[$i]);
+                    }
+                }
 
                 // Checking the number of cells.
                 if (count($criterion) % 2 === 0) {
index e699683..c2ee6f4 100644 (file)
@@ -35,9 +35,9 @@ Feature: Rubrics can be created and edited
       | TMP Criterion 4 | TMP Level 41 | 41 | TMP Level 42 | 42 |
     # Checking that only the last ones are saved.
     And I define the following rubric:
-      | Criterion 1 | Level 11 | 1 | Level 12 | 20 | Level 13 | 40 | Level 14 | 50 |
-      | Criterion 2 | Level 21 | 10 | Level 22 | 20 | Level 23 | 30 |
-      | Criterion 3 | Level 31 | 5 | Level 32 | 20 |
+      | Criterion 1 | Level 11 | 1  | Level 12 | 20 | Level 13 | 40 | Level 14  | 50  |
+      | Criterion 2 | Level 21 | 10 | Level 22 | 20 | Level 23 | 30 |           |     |
+      | Criterion 3 | Level 31 | 5  | Level 32 | 20 |          |    |           |     |
     And I press "Save as draft"
     And I go to "Test assignment 1 name" advanced grading definition page
     And I click on "Move down" "button" in the "Criterion 1" "table_row"
index e26c776..1f19673 100644 (file)
@@ -28,7 +28,7 @@ Feature: Reuse my rubrics in other activities
     And I define the following rubric:
       | Criterion 1 | Level 11 | 11 | Level 12 | 12 | Level 3 | 13 |
       | Criterion 2 | Level 21 | 21 | Level 22 | 22 | Level 3 | 23 |
-      | Criterion 3 | Level 31 | 31 | Level 32 | 32 |
+      | Criterion 3 | Level 31 | 31 | Level 32 | 32 |         |    |
     And I press "Save rubric and make it ready"
     And I follow "Course 1"
     When I add a "Assignment" to section "1" and I fill the form with:
@@ -40,7 +40,6 @@ Feature: Reuse my rubrics in other activities
     And I should see "Criterion 1"
     And I should see "Criterion 2"
     And I should see "Criterion 3"
-    And I follow "C1"
     And I go to "Test assignment 1 name" advanced grading definition page
     And I should see "Criterion 1"
     And I should see "Criterion 2"
index 64881d0..47c31e9 100644 (file)
@@ -38,6 +38,13 @@ class grade_report_grader extends grade_report {
      */
     public $grades;
 
+    /**
+     * Contains all the grades for the course - even the ones not displayed in the grade tree.
+     *
+     * @var array $allgrades
+     */
+    private $allgrades;
+
     /**
      * Array of errors for bulk grades updating.
      * @var array $gradeserror
@@ -538,8 +545,10 @@ class grade_report_grader extends grade_report {
 
         if ($grades = $DB->get_records_sql($sql, $params)) {
             foreach ($grades as $graderec) {
+                $grade = new grade_grade($graderec, false);
+                $this->allgrades[$graderec->userid][$graderec->itemid] = $grade;
                 if (in_array($graderec->userid, $userids) and array_key_exists($graderec->itemid, $this->gtree->get_items())) { // some items may not be present!!
-                    $this->grades[$graderec->userid][$graderec->itemid] = new grade_grade($graderec, false);
+                    $this->grades[$graderec->userid][$graderec->itemid] = $grade;
                     $this->grades[$graderec->userid][$graderec->itemid]->grade_item = $this->gtree->get_item($graderec->itemid); // db caching
                 }
             }
@@ -553,6 +562,8 @@ class grade_report_grader extends grade_report {
                     $this->grades[$userid][$itemid]->itemid = $itemid;
                     $this->grades[$userid][$itemid]->userid = $userid;
                     $this->grades[$userid][$itemid]->grade_item = $this->gtree->get_item($itemid); // db caching
+
+                    $this->allgrades[$userid][$itemid] = $this->grades[$userid][$itemid];
                 }
             }
         }
@@ -589,9 +600,16 @@ class grade_report_grader extends grade_report {
         $rows = array();
 
         $showuserimage = $this->get_pref('showuserimage');
-        $canseeuserreport = has_capability('gradereport/'.$CFG->grade_profilereport.':view', $this->context);
-        $canseesingleview = has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
+        // FIXME: MDL-52678 This get_capability_info is hacky and we should have an API for inserting grade row links instead.
+        $canseeuserreport = false;
+        $canseesingleview = false;
+        if (get_capability_info('gradereport/'.$CFG->grade_profilereport.':view')) {
+            $canseeuserreport = has_capability('gradereport/'.$CFG->grade_profilereport.':view', $this->context);
+        }
+        if (get_capability_info('gradereport/singleview:view')) {
+            $canseesingleview = has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
             'moodle/grade:edit'), $this->context);
+        }
         $hasuserreportcell = $canseeuserreport || $canseesingleview;
 
         $strfeedback  = $this->get_lang_string("feedback");
@@ -838,17 +856,21 @@ class grade_report_grader extends grade_report {
                     }
 
                     $singleview = '';
-                    if (has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
-                        'moodle/grade:edit'), $this->context)) {
-
-                        $url = new moodle_url('/grade/report/singleview/index.php', array(
-                            'id' => $this->course->id,
-                            'item' => 'grade',
-                            'itemid' => $element['object']->id));
-                        $singleview = $OUTPUT->action_icon(
-                            $url,
-                            new pix_icon('t/editstring', get_string('singleview', 'grades', $element['object']->get_name()))
-                        );
+
+                    // FIXME: MDL-52678 This is extremely hacky we should have an API for inserting grade column links.
+                    if (get_capability_info('gradereport/singleview:view')) {
+                        if (has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
+                            'moodle/grade:edit'), $this->context)) {
+
+                            $url = new moodle_url('/grade/report/singleview/index.php', array(
+                                'id' => $this->course->id,
+                                'item' => 'grade',
+                                'itemid' => $element['object']->id));
+                            $singleview = $OUTPUT->action_icon(
+                                $url,
+                                new pix_icon('t/editstring', get_string('singleview', 'grades', $element['object']->get_name()))
+                            );
+                        }
                     }
 
                     $itemcell->colspan = $colspan;
@@ -887,13 +909,23 @@ class grade_report_grader extends grade_report {
         }
         $jsscales = $scalesarray;
 
+        // Get all the grade items if the user can not view hidden grade items.
+        // It is possible that the user is simply viewing the 'Course total' by switching to the 'Aggregates only' view
+        // and that this user does not have the ability to view hidden items. In this case we still need to pass all the
+        // grade items (in case one has been hidden) as the course total shown needs to be adjusted for this particular
+        // user.
+        if (!$this->canviewhidden) {
+            $allgradeitems = grade_item::fetch_all(array('courseid' => $this->courseid));
+        }
+
         foreach ($this->users as $userid => $user) {
 
             if ($this->canviewhidden) {
                 $altered = array();
                 $unknown = array();
             } else {
-                $hidingaffected = grade_grade::get_hiding_affected($this->grades[$userid], $this->gtree->get_items());
+                $usergrades = $this->allgrades[$userid];
+                $hidingaffected = grade_grade::get_hiding_affected($usergrades, $allgradeitems);
                 $altered = $hidingaffected['altered'];
                 $unknown = $hidingaffected['unknown'];
                 unset($hidingaffected);
diff --git a/grade/report/grader/tests/behat/switch_views.feature b/grade/report/grader/tests/behat/switch_views.feature
new file mode 100644 (file)
index 0000000..db1d5b7
--- /dev/null
@@ -0,0 +1,121 @@
+@gradereport @gradereport_grader
+Feature: We can change what we are viewing on the grader report
+  In order to check the expected results are displayed
+  As a teacher
+  I need to assign grades and check that they display correctly in the gradebook when switching between views.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment name 1 |
+      | Description | Submit your online text |
+      | assignsubmission_onlinetext_enabled | 1 |
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment name 2 |
+      | Description | Submit your online text |
+      | assignsubmission_onlinetext_enabled | 1 |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment name 1"
+    When I press "Add submission"
+    And I set the following fields to these values:
+      | Online text | This is a submission for assignment 1 |
+    And I press "Save changes"
+    Then I should see "Submitted for grading"
+    And I follow "Course 1"
+    And I follow "Test assignment name 2"
+    When I press "Add submission"
+    And I set the following fields to these values:
+      | Online text | This is a submission for assignment 2 |
+    And I press "Save changes"
+    Then I should see "Submitted for grading"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name 1"
+    And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment name 2"
+    And I press "Save changes"
+    And I turn editing mode off
+
+  @javascript
+  Scenario: View and minimise the grader report containing hidden activities
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I open "Test assignment name 2" actions menu
+    And I click on "Hide" "link" in the "Test assignment name 2" activity
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I select "Grader report" from the "Grade report" singleselect
+    And I should see "Test assignment name 1"
+    And I should see "Test assignment name 2"
+    And I should see "Course total"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -4-       | -5-       | -6-       |
+      | Student 1          | 80        | 90        | 170       |
+    And I click on "Change to aggregates only" "link"
+    And I should not see "Test assignment name 1"
+    And I should not see "Test assignment name 2"
+    And I should see "Course total"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -4-       |
+      | Student 1          | 170       |
+    And I click on "Change to grades only" "link"
+    And I should see "Test assignment name 1"
+    And I should see "Test assignment name 2"
+    And I should not see "Course total"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -4-       | -5-       |
+      | Student 1          | 80        | 90        |
+
+  @javascript
+  Scenario: View and minimise the grader report containing hidden activities without the 'moodle/grade:viewhidden' capability
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I open "Test assignment name 2" actions menu
+    And I click on "Hide" "link" in the "Test assignment name 2" activity
+    And I log out
+    And I log in as "admin"
+    And I set the following system permissions of "Teacher" role:
+      | capability | permission |
+      | moodle/grade:viewhidden | Prevent |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I select "Grader report" from the "Grade report" singleselect
+    And I should see "Test assignment name 1"
+    And I should see "Test assignment name 2"
+    And I should see "Course total"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -4-       | -5-       | -6-       |
+      | Student 1          | 80        | -         | 80        |
+    And I click on "Change to aggregates only" "link"
+    And I should not see "Test assignment name 1"
+    And I should not see "Test assignment name 2"
+    And I should see "Course total"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -4-       |
+      | Student 1          | 80        |
+    And I click on "Change to grades only" "link"
+    And I should see "Test assignment name 1"
+    And I should see "Test assignment name 2"
+    And I should not see "Course total"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -4-       | -5-       |
+      | Student 1          | 80        | -         |
index 905f91e..9c62a71 100644 (file)
@@ -30,9 +30,10 @@ require_once($CFG->dirroot.'/grade/lib.php');
 $download      = optional_param('download', '', PARAM_ALPHA);
 $courseid      = required_param('id', PARAM_INT);        // Course id.
 $page          = optional_param('page', 0, PARAM_INT);   // Active page.
+$showreport    = optional_param('showreport', 0, PARAM_INT);
 
 $PAGE->set_pagelayout('report');
-$url = new moodle_url('/grade/report/history/index.php', array('id' => $courseid));
+$url = new moodle_url('/grade/report/history/index.php', array('id' => $courseid, 'showreport' => 1));
 $PAGE->set_url($url);
 
 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
@@ -105,15 +106,17 @@ if ($table->is_downloading()) {
 print_grade_page_head($COURSE->id, 'report', 'history', get_string('pluginname', 'gradereport_history'), false, '');
 $mform->display();
 
-// Render table.
-echo $output->render($table);
+if ($showreport) {
+    // Only display report after form has been submitted.
+    echo $output->render($table);
 
-$event = \gradereport_history\event\grade_report_viewed::create(
-    array(
-        'context' => $context,
-        'courseid' => $courseid
-    )
-);
-$event->trigger();
+    $event = \gradereport_history\event\grade_report_viewed::create(
+        array(
+            'context' => $context,
+            'courseid' => $courseid
+        )
+    );
+    $event->trigger();
+}
 
 echo $OUTPUT->footer();
index 238fbf6..0cfc905 100644 (file)
@@ -47,7 +47,8 @@ Feature: A teacher checks the grade history report in a course
     And I give the grade "70.00" to the user "Student 2" for the grade item "The greatest assignment ever"
     And I give the grade "80.00" to the user "Student 2" for the grade item "Rewarding assignment"
     And I press "Save changes"
-    When I follow "Grade history"
+    And I follow "Grade history"
+    When I press "Submit"
     Then the following should exist in the "gradereport_history" table:
       | First name/Surname | Grade item                    | Original grade | Revised grade | Grader    |
       | Student 1          | The greatest assignment ever  |                | 50.00         | Teacher 1 |
index 3a79b41..dc2ca33 100644 (file)
@@ -49,19 +49,19 @@ $string['environmentsub2'] = 'Každé vydání Moodle vyžaduje určitou minimá
 $string['errorsinenvironment'] = 'Kontrola serverového prostředí selhala!';
 $string['installation'] = 'Instalace';
 $string['langdownloaderror'] = 'Bohužel, jazyk "{$a}" se nepodařilo nainstalovat. Instalace bude pokračovat v angličtine.';
-$string['memorylimithelp'] = '<p>Limit paměti pro PHP skripty je na vašem serveru momentálně nastaven na hodnotu {$a}.</p>
+$string['memorylimithelp'] = '<p>Limit paměti pro PHP skripty je na vašem serveru momentálně nastaven na {$a}.</p>
 
 <p>To může později způsobovat Moodlu problémy, zvláště při větším množství modulů a/nebo uživatelů.</p>
 
-<p>Je-li to možné, doporučujeme vám nastavit v PHP vyšší limit, např. 40M. Můžete to provést několika způsoby:
+<p>Je-li to možné, doporučujeme vám nastavit v PHP vyšší limit, např. 40M. Můžete to provést několika způsoby:</p>
 <ol>
 <li>Můžete-li, překompilujte PHP s volbou <i>--enable-memory-limit</i>.
 Moodle si tak bude sám moci nastavit potřebný limit.</li>
 <li>Máte-li přístup k souboru php.ini, změňte nastavení <b>memory_limit</b>
 na hodnotu blízkou 40M. Nemáte-li taková práva, požádejte správce vašeho webového serveru, aby toto nastavení provedl on.</li>
 <li>Na některých serverech můžete v kořenovém adresáři Moodlu vytvořit soubor .htaccess s následujícím řádkem:
-<p><blockquote>php_value memory_limit 40M</blockquote></p>
-<p>Bohužel, v některých případech tím vyřadíte z provozu <b>všechny</b> PHP stránky (při jejich prohlížení uvidíte chybová hlášení), takže budete muset soubor .htaccess zase odstranit.</li>
+<blockquote><div>php_value memory_limit 40M</div></blockquote>
+<p>Bohužel, v některých případech tím vyřadíte z provozu <b>všechny</b> PHP stránky (při jejich prohlížení uvidíte chybová hlášení), takže budete muset soubor .htaccess zase odstranit.</p></li>
 </ol>';
 $string['paths'] = 'Cesty';
 $string['pathserrcreatedataroot'] = 'Datový adresář ({$a->dataroot}) nemůže být tímto průvodcem instalací vytvořen.';
index 1ecd467..0826c57 100644 (file)
@@ -504,6 +504,7 @@ $string['environmentxmlerror'] = 'Error reading environment data ({$a->error_cod
 $string['errordeletingconfig'] = 'An error occurred while deleting the configuration records for plugin \'{$a}\'.';
 $string['errorsetting'] = 'Could not save setting:';
 $string['errorwithsettings'] = 'Some settings were not changed due to an error.';
+$string['eventshandlersinuse'] = 'The following plugins in your system are using Events 1 API deprecated handlers: \'{$a}\'. Please, update them to use Events 2 API. See https://docs.moodle.org/dev/Event_2#Event_dispatching_and_observers.';
 $string['everyonewhocan'] = 'Everyone who can \'{$a}\'';
 $string['exceptions'] = 'exceptions';
 $string['execpathnotallowed'] = 'Setting executable and local paths disabled in config.php';
@@ -1032,7 +1033,7 @@ $string['taskgradecron'] = 'Background processing for gradebook';
 $string['tasklegacycron'] = 'Legacy cron processing for plugins';
 $string['taskmessagingcleanup'] = 'Background processing for messaging';
 $string['taskpasswordresetcleanup'] = 'Cleanup password reset attempts';
-$string['taskplagiarismcron'] = 'Background processing for plagiarism plugins';
+$string['taskplagiarismcron'] = 'Background processing for legacy cron in plagiarism plugins';
 $string['taskportfoliocron'] = 'Background processing for portfolio plugins';
 $string['taskquestioncron'] = 'Background processing for question engine';
 $string['taskregistrationcron'] = 'Site registration';
index 33d26c9..d1e7660 100644 (file)
Binary files a/lib/amd/build/str.min.js and b/lib/amd/build/str.min.js differ
diff --git a/lib/amd/build/tree.min.js b/lib/amd/build/tree.min.js
new file mode 100644 (file)
index 0000000..abc9f1d
Binary files /dev/null and b/lib/amd/build/tree.min.js differ
index 76830d3..c62ca7c 100644 (file)
@@ -96,7 +96,7 @@ define(['jquery', 'core/ajax', 'core/localstorage'], function($, ajax, storage)
             for (i = 0; i < requests.length; i++) {
                 request = requests[i];
                 if (typeof request.lang === "undefined") {
-                    request.lang = $('html').attr('lang');
+                    request.lang = $('html').attr('lang').replace('-', '_');
                 }
                 if (typeof M.str[request.component] === "undefined" ||
                         typeof M.str[request.component][request.key] === "undefined") {
diff --git a/lib/amd/src/tree.js b/lib/amd/src/tree.js
new file mode 100644 (file)
index 0000000..cb6d6ef
--- /dev/null
@@ -0,0 +1,465 @@
+// 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 !== null) {
+            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);
+    };
+
+    /**
+     * 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' && item.attr('aria-expanded') !== 'true') {
+            // 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 already collapsed then do nothing.
+        if (item.attr('aria-expanded') === 'false') {
+            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: {
+                // If this is a goup item then collapse it and focus the parent group
+                // in accordance with the aria spec.
+                if (this.isGroupItem(item)) {
+                    this.collapseGroup(item);
+                    // Move up to the parent.
+                    var visibleGroups = this.getVisibleItems().filter(SELECTORS.GROUP);
+                    var index = visibleGroups.index(item);
+                    var prevIndex = (index - 1) > 0 ? index - 1 : 0;
+                    visibleGroups.eq(prevIndex).focus();
+                }
+
+                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)) {
+                    this.expandGroup(item).done(function() {
+                        // 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)) {
diff --git a/lib/classes/log/sql_internal_reader.php b/lib/classes/log/sql_internal_reader.php
deleted file mode 100644 (file)
index d5fcc3d..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-<?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/>.
-
-/**
- * Log storage sql reader interface.
- *
- * @package    core
- * @copyright  2014 Petr Skoda
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core\log;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Sql internal reader.
- *
- * @deprecated since Moodle 2.9 MDL-48595 - please do not use this interface any more.
- * @see        sql_reader
- * @todo       MDL-49291 This will be deleted in Moodle 3.1.
- * @package    core
- * @copyright  2013 Petr Skoda {@link http://skodak.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-interface sql_internal_reader extends sql_select_reader {
-
-    /**
-     * Returns name of the table or database view that holds the log data in standardised format.
-     *
-     * Note: this table must be used for reading only,
-     * it is strongly recommended to use this in complex reports only.
-     *
-     * @return string
-     */
-    public function get_internal_log_table_name();
-}
diff --git a/lib/classes/log/sql_select_reader.php b/lib/classes/log/sql_select_reader.php
deleted file mode 100644 (file)
index 65b94e1..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<?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/>.
-
-/**
- * Log storage reader interface.
- *
- * @package    core
- * @copyright  2013 Petr Skoda {@link http://skodak.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core\log;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Sql select reader.
- *
- * @deprecated since Moodle 2.9 MDL-48595 - please do not use this interface any more.
- * @see        sql_reader
- * @todo       MDL-49291 This will be deleted in Moodle 3.1.
- * @package    core
- * @copyright  2013 Petr Skoda {@link http://skodak.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-interface sql_select_reader extends reader {
-    /**
-     * Fetch records using given criteria.
-     *
-     * @param string $selectwhere
-     * @param array $params
-     * @param string $sort
-     * @param int $limitfrom
-     * @param int $limitnum
-     * @return \core\event\base[]
-     */
-    public function get_events_select($selectwhere, array $params, $sort, $limitfrom, $limitnum);
-
-    /**
-     * Return number of events matching given criteria.
-     *
-     * @param string $selectwhere
-     * @param array $params
-     * @return int
-     */
-    public function get_events_select_count($selectwhere, array $params);
-}
index a88ad10..3975de3 100644 (file)
@@ -4807,5 +4807,47 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2016011301.00);
     }
 
+    if ($oldversion < 2016011901.00) {
+
+        // Convert calendar_lookahead to nearest new value.
+        $transaction = $DB->start_delegated_transaction();
+
+        // Count all users who curretly have that preference set (for progress bar).
+        $total = $DB->count_records_select('user_preferences', "name = 'calendar_lookahead' AND value != '0'");
+        $pbar = new progress_bar('upgradecalendarlookahead', 500, true);
+
+        // Get all these users, one at a time.
+        $rs = $DB->get_recordset_select('user_preferences', "name = 'calendar_lookahead' AND value != '0'");
+        $i = 0;
+        foreach ($rs as $userpref) {
+
+            // Calculate and set new lookahead value.
+            if ($userpref->value > 90) {
+                $newvalue = 120;
+            } else if ($userpref->value > 60 and $userpref->value < 90) {
+                $newvalue = 90;
+            } else if ($userpref->value > 30 and $userpref->value < 60) {
+                $newvalue = 60;
+            } else if ($userpref->value > 21 and $userpref->value < 30) {
+                $newvalue = 30;
+            } else if ($userpref->value > 14 and $userpref->value < 21) {
+                $newvalue = 21;
+            } else if ($userpref->value > 7 and $userpref->value < 14) {
+                $newvalue = 14;
+            } else {
+                $newvalue = $userpref->value;
+            }
+
+            $DB->set_field('user_preferences', 'value', $newvalue, array('id' => $userpref->id));
+
+            // Update progress.
+            $i++;
+            $pbar->update($i, $total, "Upgrading user preference settings - $i/$total.");
+        }
+        $rs->close();
+        $transaction->allow_commit();
+
+        upgrade_main_savepoint(true, 2016011901.00);
+    }
     return true;
 }
index 879fb48..21c56f5 100644 (file)
@@ -508,7 +508,9 @@ function httpsrequired() {
 /**
  * Given a physical path to a file, returns the URL through which it can be reached in Moodle.
  *
- * @deprecated use moodle_url factory methods instead
+ * @deprecated since 3.1 - replacement legacy file API methods can be found on the moodle_url class, for example:
+ * The moodle_url::make_legacyfile_url() method can be used to generate a legacy course file url. To generate
+ * course module file.php url the moodle_url::make_file_url() should be used.
  *
  * @param string $path Physical path to a file
  * @param array $options associative array of GET variables to append to the URL
@@ -516,6 +518,7 @@ function httpsrequired() {
  * @return string URL to file
  */
 function get_file_url($path, $options=null, $type='coursefile') {
+    debugging('Function get_file_url() is deprecated, please use moodle_url factory methods instead.', DEBUG_DEVELOPER);
     global $CFG;
 
     $path = str_replace('//', '/', $path);
@@ -3879,3 +3882,497 @@ function tag_cloud_sort($a, $b) {
         return 0;
     }
 }
+
+/**
+ * Loads the events definitions for the component (from file). If no
+ * events are defined for the component, we simply return an empty array.
+ *
+ * @access protected To be used from eventslib only
+ * @deprecated since Moodle 3.1
+ * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
+ * @return array Array of capabilities or empty array if not exists
+ */
+function events_load_def($component) {
+    global $CFG;
+    if ($component === 'unittest') {
+        $defpath = $CFG->dirroot.'/lib/tests/fixtures/events.php';
+    } else {
+        $defpath = core_component::get_component_directory($component).'/db/events.php';
+    }
+
+    $handlers = array();
+
+    if (file_exists($defpath)) {
+        require($defpath);
+    }
+
+    // make sure the definitions are valid and complete; tell devs what is wrong
+    foreach ($handlers as $eventname => $handler) {
+        if ($eventname === 'reset') {
+            debugging("'reset' can not be used as event name.");
+            unset($handlers['reset']);
+            continue;
+        }
+        if (!is_array($handler)) {
+            debugging("Handler of '$eventname' must be specified as array'");
+            unset($handlers[$eventname]);
+            continue;
+        }
+        if (!isset($handler['handlerfile'])) {
+            debugging("Handler of '$eventname' must include 'handlerfile' key'");
+            unset($handlers[$eventname]);
+            continue;
+        }
+        if (!isset($handler['handlerfunction'])) {
+            debugging("Handler of '$eventname' must include 'handlerfunction' key'");
+            unset($handlers[$eventname]);
+            continue;
+        }
+        if (!isset($handler['schedule'])) {
+            $handler['schedule'] = 'instant';
+        }
+        if ($handler['schedule'] !== 'instant' and $handler['schedule'] !== 'cron') {
+            debugging("Handler of '$eventname' must include valid 'schedule' type (instant or cron)'");
+            unset($handlers[$eventname]);
+            continue;
+        }
+        if (!isset($handler['internal'])) {
+            $handler['internal'] = 1;
+        }
+        $handlers[$eventname] = $handler;
+    }
+
+    return $handlers;
+}
+
+/**
+ * Puts a handler on queue
+ *
+ * @access protected To be used from eventslib only
+ * @deprecated since Moodle 3.1
+ * @param stdClass $handler event handler object from db
+ * @param stdClass $event event data object
+ * @param string $errormessage The error message indicating the problem
+ * @return int id number of new queue handler
+ */
+function events_queue_handler($handler, $event, $errormessage) {
+    global $DB;
+
+    if ($qhandler = $DB->get_record('events_queue_handlers', array('queuedeventid'=>$event->id, 'handlerid'=>$handler->id))) {
+        debugging("Please check code: Event id $event->id is already queued in handler id $qhandler->id");
+        return $qhandler->id;
+    }
+
+    // make a new queue handler
+    $qhandler = new stdClass();
+    $qhandler->queuedeventid  = $event->id;
+    $qhandler->handlerid      = $handler->id;
+    $qhandler->errormessage   = $errormessage;
+    $qhandler->timemodified   = time();
+    if ($handler->schedule === 'instant' and $handler->status == 1) {
+        $qhandler->status     = 1; //already one failed attempt to dispatch this event
+    } else {
+        $qhandler->status     = 0;
+    }
+
+    return $DB->insert_record('events_queue_handlers', $qhandler);
+}
+
+/**
+ * trigger a single event with a specified handler
+ *
+ * @access protected To be used from eventslib only
+ * @deprecated since Moodle 3.1
+ * @param stdClass $handler This shoudl be a row from the events_handlers table.
+ * @param stdClass $eventdata An object containing information about the event
+ * @param string $errormessage error message indicating problem
+ * @return bool|null True means event processed, false means retry event later; may throw exception, NULL means internal error
+ */
+function events_dispatch($handler, $eventdata, &$errormessage) {
+    global $CFG;
+
+    debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.', DEBUG_DEVELOPER);
+
+    $function = unserialize($handler->handlerfunction);
+
+    if (is_callable($function)) {
+        // oki, no need for includes
+
+    } else if (file_exists($CFG->dirroot.$handler->handlerfile)) {
+        include_once($CFG->dirroot.$handler->handlerfile);
+
+    } else {
+        $errormessage = "Handler file of component $handler->component: $handler->handlerfile can not be found!";
+        return null;
+    }
+
+    // checks for handler validity
+    if (is_callable($function)) {
+        $result = call_user_func($function, $eventdata);
+        if ($result === false) {
+            $errormessage = "Handler function of component $handler->component: $handler->handlerfunction requested resending of event!";
+            return false;
+        }
+        return true;
+
+    } else {
+        $errormessage = "Handler function of component $handler->component: $handler->handlerfunction not callable function or class method!";
+        return null;
+    }
+}
+
+/**
+ * given a queued handler, call the respective event handler to process the event
+ *
+ * @access protected To be used from eventslib only
+ * @deprecated since Moodle 3.1
+ * @param stdClass $qhandler events_queued_handler row from db
+ * @return boolean true means event processed, false means retry later, NULL means fatal failure
+ */
+function events_process_queued_handler($qhandler) {
+    global $DB;
+
+    // get handler
+    if (!$handler = $DB->get_record('events_handlers', array('id'=>$qhandler->handlerid))) {
+        debugging("Error processing queue handler $qhandler->id, missing handler id: $qhandler->handlerid");
+        //irrecoverable error, remove broken queue handler
+        events_dequeue($qhandler);
+        return NULL;
+    }
+
+    // get event object
+    if (!$event = $DB->get_record('events_queue', array('id'=>$qhandler->queuedeventid))) {
+        // can't proceed with no event object - might happen when two crons running at the same time
+        debugging("Error processing queue handler $qhandler->id, missing event id: $qhandler->queuedeventid");
+        //irrecoverable error, remove broken queue handler
+        events_dequeue($qhandler);
+        return NULL;
+    }
+
+    // call the function specified by the handler
+    try {
+        $errormessage = 'Unknown error';
+        if (events_dispatch($handler, unserialize(base64_decode($event->eventdata)), $errormessage)) {
+            //everything ok
+            events_dequeue($qhandler);
+            return true;
+        }
+    } catch (Exception $e) {
+        // the problem here is that we do not want one broken handler to stop all others,
+        // cron handlers are very tricky because the needed data might have been deleted before the cron execution
+        $errormessage = "Handler function of component $handler->component: $handler->handlerfunction threw exception :" .
+                $e->getMessage() . "\n" . format_backtrace($e->getTrace(), true);
+        if (!empty($e->debuginfo)) {
+            $errormessage .= $e->debuginfo;
+        }
+    }
+
+    //dispatching failed
+    $qh = new stdClass();
+    $qh->id           = $qhandler->id;
+    $qh->errormessage = $errormessage;
+    $qh->timemodified = time();
+    $qh->status       = $qhandler->status + 1;
+    $DB->update_record('events_queue_handlers', $qh);
+
+    debugging($errormessage);
+
+    return false;
+}
+
+/**
+ * Updates all of the event definitions within the database.
+ *
+ * Unfortunately this isn't as simple as removing them all and then readding
+ * the updated event definitions. Chances are queued items are referencing the
+ * existing definitions.
+ *
+ * Note that the absence of the db/events.php event definition file
+ * will cause any queued events for the component to be removed from
+ * the database.
+ *
+ * @category event
+ * @deprecated since Moodle 3.1
+ * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
+ * @return boolean always returns true
+ */
+function events_update_definition($component='moodle') {
+    global $DB;
+
+    // load event definition from events.php
+    $filehandlers = events_load_def($component);
+
+    if ($filehandlers) {
+        debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.', DEBUG_DEVELOPER);
+    }
+
+    // load event definitions from db tables
+    // if we detect an event being already stored, we discard from this array later
+    // the remaining needs to be removed
+    $cachedhandlers = events_get_cached($component);
+
+    foreach ($filehandlers as $eventname => $filehandler) {
+        if (!empty($cachedhandlers[$eventname])) {
+            if ($cachedhandlers[$eventname]['handlerfile'] === $filehandler['handlerfile'] &&
+                $cachedhandlers[$eventname]['handlerfunction'] === serialize($filehandler['handlerfunction']) &&
+                $cachedhandlers[$eventname]['schedule'] === $filehandler['schedule'] &&
+                $cachedhandlers[$eventname]['internal'] == $filehandler['internal']) {
+                // exact same event handler already present in db, ignore this entry
+
+                unset($cachedhandlers[$eventname]);
+                continue;
+
+            } else {
+                // same event name matches, this event has been updated, update the datebase
+                $handler = new stdClass();
+                $handler->id              = $cachedhandlers[$eventname]['id'];
+                $handler->handlerfile     = $filehandler['handlerfile'];
+                $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
+                $handler->schedule        = $filehandler['schedule'];
+                $handler->internal        = $filehandler['internal'];
+
+                $DB->update_record('events_handlers', $handler);
+
+                unset($cachedhandlers[$eventname]);
+                continue;
+            }
+
+        } else {
+            // if we are here, this event handler is not present in db (new)
+            // add it
+            $handler = new stdClass();
+            $handler->eventname       = $eventname;
+            $handler->component       = $component;
+            $handler->handlerfile     = $filehandler['handlerfile'];
+            $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
+            $handler->schedule        = $filehandler['schedule'];
+            $handler->status          = 0;
+            $handler->internal        = $filehandler['internal'];
+
+            $DB->insert_record('events_handlers', $handler);
+        }
+    }
+
+    // clean up the left overs, the entries in cached events array at this points are deprecated event handlers
+    // and should be removed, delete from db
+    events_cleanup($component, $cachedhandlers);
+
+    events_get_handlers('reset');
+
+    return true;
+}
+
+/**
+ * Events cron will try to empty the events queue by processing all the queued events handlers
+ *
+ * @access public Part of the public API
+ * @deprecated since Moodle 3.1
+ * @category event
+ * @param string $eventname empty means all
+ * @return int number of dispatched events
+ */
+function events_cron($eventname='') {
+    global $DB;
+
+    $failed = array();
+    $processed = 0;
+
+    if ($eventname) {
+        $sql = "SELECT qh.*
+                  FROM {events_queue_handlers} qh, {events_handlers} h
+                 WHERE qh.handlerid = h.id AND h.eventname=?
+              ORDER BY qh.id";
+        $params = array($eventname);
+    } else {
+        $sql = "SELECT *
+                  FROM {events_queue_handlers}
+              ORDER BY id";
+        $params = array();
+    }
+
+    $rs = $DB->get_recordset_sql($sql, $params);
+    if ($rs->valid()) {
+        debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.', DEBUG_DEVELOPER);
+    }
+
+    foreach ($rs as $qhandler) {
+        if (isset($failed[$qhandler->handlerid])) {
+            // do not try to dispatch any later events when one already asked for retry or ended with exception
+            continue;
+        }
+        $status = events_process_queued_handler($qhandler);
+        if ($status === false) {
+            // handler is asking for retry, do not send other events to this handler now
+            $failed[$qhandler->handlerid] = $qhandler->handlerid;
+        } else if ($status === NULL) {
+            // means completely broken handler, event data was purged
+            $failed[$qhandler->handlerid] = $qhandler->handlerid;
+        } else {
+            $processed++;
+        }
+    }
+    $rs->close();
+
+    // remove events that do not have any handlers waiting
+    $sql = "SELECT eq.id
+              FROM {events_queue} eq
+              LEFT JOIN {events_queue_handlers} qh ON qh.queuedeventid = eq.id
+             WHERE qh.id IS NULL";
+    $rs = $DB->get_recordset_sql($sql);
+    foreach ($rs as $event) {
+        //debugging('Purging stale event '.$event->id);
+        $DB->delete_records('events_queue', array('id'=>$event->id));
+    }
+    $rs->close();
+
+    return $processed;
+}
+
+/**
+ * Do not call directly, this is intended to be used from new event base only.
+ *
+ * @private
+ * @deprecated since Moodle 3.1
+ * @param string $eventname name of the event
+ * @param mixed $eventdata event data object
+ * @return int number of failed events
+ */
+function events_trigger_legacy($eventname, $eventdata) {
+    global $CFG, $USER, $DB;
+
+    $failedcount = 0; // number of failed events.
+
+    // pull out all registered event handlers
+    if ($handlers = events_get_handlers($eventname)) {
+        foreach ($handlers as $handler) {
+            $errormessage = '';
+
+            if ($handler->schedule === 'instant') {
+                if ($handler->status) {
+                    //check if previous pending events processed
+                    if (!$DB->record_exists('events_queue_handlers', array('handlerid'=>$handler->id))) {
+                        // ok, queue is empty, lets reset the status back to 0 == ok
+                        $handler->status = 0;
+                        $DB->set_field('events_handlers', 'status', 0, array('id'=>$handler->id));
+                        // reset static handler cache
+                        events_get_handlers('reset');
+                    }
+                }
+
+                // dispatch the event only if instant schedule and status ok
+                if ($handler->status or (!$handler->internal and $DB->is_transaction_started())) {
+                    // increment the error status counter
+                    $handler->status++;
+                    $DB->set_field('events_handlers', 'status', $handler->status, array('id'=>$handler->id));
+                    // reset static handler cache
+                    events_get_handlers('reset');
+
+                } else {
+                    $errormessage = 'Unknown error';
+                    $result = events_dispatch($handler, $eventdata, $errormessage);
+                    if ($result === true) {
+                        // everything is fine - event dispatched
+                        continue;
+                    } else if ($result === false) {
+                        // retry later - set error count to 1 == send next instant into cron queue
+                        $DB->set_field('events_handlers', 'status', 1, array('id'=>$handler->id));
+                        // reset static handler cache
+                        events_get_handlers('reset');
+                    } else {
+                        // internal problem - ignore the event completely
+                        $failedcount ++;
+                        continue;
+                    }
+                }
+
+                // update the failed counter
+                $failedcount ++;
+
+            } else if ($handler->schedule === 'cron') {
+                //ok - use queueing of events only
+
+            } else {
+                // unknown schedule - ignore event completely
+                debugging("Unknown handler schedule type: $handler->schedule");
+                $failedcount ++;
+                continue;
+            }
+
+            // if even type is not instant, or dispatch asked for retry, queue it
+            $event = new stdClass();
+            $event->userid      = $USER->id;
+            $event->eventdata   = base64_encode(serialize($eventdata));
+            $event->timecreated = time();
+            if (debugging()) {
+                $dump = '';
+                $callers = debug_backtrace();
+                foreach ($callers as $caller) {
+                    if (!isset($caller['line'])) {
+                        $caller['line'] = '?';
+                    }
+                    if (!isset($caller['file'])) {
+                        $caller['file'] = '?';
+                    }
+                    $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1);
+                    if (isset($caller['function'])) {
+                        $dump .= ': call to ';
+                        if (isset($caller['class'])) {
+                            $dump .= $caller['class'] . $caller['type'];
+                        }
+                        $dump .= $caller['function'] . '()';
+                    }
+                    $dump .= "\n";
+                }
+                $event->stackdump = $dump;
+            } else {
+                $event->stackdump = '';
+            }
+            $event->id = $DB->insert_record('events_queue', $event);
+            events_queue_handler($handler, $event, $errormessage);
+        }
+    } else {
+        // No handler found for this event name - this is ok!
+    }
+
+    return $failedcount;
+}
+
+/**
+ * checks if an event is registered for this component
+ *
+ * @access public Part of the public API
+ * @deprecated since Moodle 3.1
+ * @param string $eventname name of the event
+ * @param string $component component name, can be mod/data or moodle
+ * @return bool
+ */
+function events_is_registered($eventname, $component) {
+    global $DB;
+
+    debugging('events_is_registered() has been deprecated along with all Events 1 API in favour of Events 2 API,' .
+        ' please use it instead.', DEBUG_DEVELOPER);
+
+    return $DB->record_exists('events_handlers', array('component'=>$component, 'eventname'=>$eventname));
+}
+
+/**
+ * checks if an event is queued for processing - either cron handlers attached or failed instant handlers
+ *
+ * @access public Part of the public API
+ * @deprecated since Moodle 3.1
+ * @param string $eventname name of the event
+ * @return int number of queued events
+ */
+function events_pending_count($eventname) {
+    global $DB;
+
+    debugging('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2 API,' .
+        ' please use it instead.', DEBUG_DEVELOPER);
+
+    $sql = "SELECT COUNT('x')
+              FROM {events_queue_handlers} qh
+              JOIN {events_handlers} h ON h.id = qh.handlerid
+             WHERE h.eventname = ?";
+
+    return $DB->count_records_sql($sql, array($eventname));
+}
index e089c1c..1fe8ee2 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..75083cd 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..f158f16 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..ad5e77f 100644 (file)
@@ -97,10 +97,15 @@ EditorSelection.prototype = {
         // 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);
+
+            // Ignore the event if the target is not part of the current editor.
+            if (!this._wrapper.contains(e.target._node)) {
+                return;
+            }
+            Y.soon(Y.bind(this._hasSelectionChanged, this, e));
+        }, {
+            standAlone: true
+        }, this);
 
         return this;
     },
index 567cc6c..2474aa1 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-/**
- * Loads the events definitions for the component (from file). If no
- * events are defined for the component, we simply return an empty array.
- *
- * @access protected To be used from eventslib only
- *
- * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
- * @return array Array of capabilities or empty array if not exists
- */
-function events_load_def($component) {
-    global $CFG;
-    if ($component === 'unittest') {
-        $defpath = $CFG->dirroot.'/lib/tests/fixtures/events.php';
-    } else {
-        $defpath = core_component::get_component_directory($component).'/db/events.php';
-    }
-
-    $handlers = array();
-
-    if (file_exists($defpath)) {
-        require($defpath);
-    }
-
-    // make sure the definitions are valid and complete; tell devs what is wrong
-    foreach ($handlers as $eventname => $handler) {
-        if ($eventname === 'reset') {
-            debugging("'reset' can not be used as event name.");
-            unset($handlers['reset']);
-            continue;
-        }
-        if (!is_array($handler)) {
-            debugging("Handler of '$eventname' must be specified as array'");
-            unset($handlers[$eventname]);
-            continue;
-        }
-        if (!isset($handler['handlerfile'])) {
-            debugging("Handler of '$eventname' must include 'handlerfile' key'");
-            unset($handlers[$eventname]);
-            continue;
-        }
-        if (!isset($handler['handlerfunction'])) {
-            debugging("Handler of '$eventname' must include 'handlerfunction' key'");
-            unset($handlers[$eventname]);
-            continue;
-        }
-        if (!isset($handler['schedule'])) {
-            $handler['schedule'] = 'instant';
-        }
-        if ($handler['schedule'] !== 'instant' and $handler['schedule'] !== 'cron') {
-            debugging("Handler of '$eventname' must include valid 'schedule' type (instant or cron)'");
-            unset($handlers[$eventname]);
-            continue;
-        }
-        if (!isset($handler['internal'])) {
-            $handler['internal'] = 1;
-        }
-        $handlers[$eventname] = $handler;
-    }
-
-    return $handlers;
-}
-
 /**
  * Gets the capabilities that have been cached in the database for this
  * component.
@@ -116,83 +54,6 @@ function events_get_cached($component) {
     return $cachedhandlers;
 }
 
-/**
- * Updates all of the event definitions within the database.
- *
- * Unfortunately this isn't as simple as removing them all and then readding
- * the updated event definitions. Chances are queued items are referencing the
- * existing definitions.
- *
- * Note that the absence of the db/events.php event definition file
- * will cause any queued events for the component to be removed from
- * the database.
- *
- * @category event
- * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
- * @return boolean always returns true
- */
-function events_update_definition($component='moodle') {
-    global $DB;
-
-    // load event definition from events.php
-    $filehandlers = events_load_def($component);
-
-    // load event definitions from db tables
-    // if we detect an event being already stored, we discard from this array later
-    // the remaining needs to be removed
-    $cachedhandlers = events_get_cached($component);
-
-    foreach ($filehandlers as $eventname => $filehandler) {
-        if (!empty($cachedhandlers[$eventname])) {
-            if ($cachedhandlers[$eventname]['handlerfile'] === $filehandler['handlerfile'] &&
-                $cachedhandlers[$eventname]['handlerfunction'] === serialize($filehandler['handlerfunction']) &&
-                $cachedhandlers[$eventname]['schedule'] === $filehandler['schedule'] &&
-                $cachedhandlers[$eventname]['internal'] == $filehandler['internal']) {
-                // exact same event handler already present in db, ignore this entry
-
-                unset($cachedhandlers[$eventname]);
-                continue;
-
-            } else {
-                // same event name matches, this event has been updated, update the datebase
-                $handler = new stdClass();
-                $handler->id              = $cachedhandlers[$eventname]['id'];
-                $handler->handlerfile     = $filehandler['handlerfile'];
-                $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
-                $handler->schedule        = $filehandler['schedule'];
-                $handler->internal        = $filehandler['internal'];
-
-                $DB->update_record('events_handlers', $handler);
-
-                unset($cachedhandlers[$eventname]);
-                continue;
-            }
-
-        } else {
-            // if we are here, this event handler is not present in db (new)
-            // add it
-            $handler = new stdClass();
-            $handler->eventname       = $eventname;
-            $handler->component       = $component;
-            $handler->handlerfile     = $filehandler['handlerfile'];
-            $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
-            $handler->schedule        = $filehandler['schedule'];
-            $handler->status          = 0;
-            $handler->internal        = $filehandler['internal'];
-
-            $DB->insert_record('events_handlers', $handler);
-        }
-    }
-
-    // clean up the left overs, the entries in cached events array at this points are deprecated event handlers
-    // and should be removed, delete from db
-    events_cleanup($component, $cachedhandlers);
-
-    events_get_handlers('reset');
-
-    return true;
-}
-
 /**
  * Remove all event handlers and queued events
  *
@@ -233,141 +94,6 @@ function events_cleanup($component, $cachedhandlers) {
     return $deletecount;
 }
 
-/****************** End of Events handler Definition code *******************/
-
-/**
- * Puts a handler on queue
- *
- * @access protected To be used from eventslib only
- *
- * @param stdClass $handler event handler object from db
- * @param stdClass $event event data object
- * @param string $errormessage The error message indicating the problem
- * @return int id number of new queue handler
- */
-function events_queue_handler($handler, $event, $errormessage) {
-    global $DB;
-
-    if ($qhandler = $DB->get_record('events_queue_handlers', array('queuedeventid'=>$event->id, 'handlerid'=>$handler->id))) {
-        debugging("Please check code: Event id $event->id is already queued in handler id $qhandler->id");
-        return $qhandler->id;
-    }
-
-    // make a new queue handler
-    $qhandler = new stdClass();
-    $qhandler->queuedeventid  = $event->id;
-    $qhandler->handlerid      = $handler->id;
-    $qhandler->errormessage   = $errormessage;
-    $qhandler->timemodified   = time();
-    if ($handler->schedule === 'instant' and $handler->status == 1) {
-        $qhandler->status     = 1; //already one failed attempt to dispatch this event
-    } else {
-        $qhandler->status     = 0;
-    }
-
-    return $DB->insert_record('events_queue_handlers', $qhandler);
-}
-
-/**
- * trigger a single event with a specified handler
- *
- * @access protected To be used from eventslib only
- *
- * @param stdClass $handler This shoudl be a row from the events_handlers table.
- * @param stdClass $eventdata An object containing information about the event
- * @param string $errormessage error message indicating problem
- * @return bool|null True means event processed, false means retry event later; may throw exception, NULL means internal error
- */
-function events_dispatch($handler, $eventdata, &$errormessage) {
-    global $CFG;
-
-    $function = unserialize($handler->handlerfunction);
-
-    if (is_callable($function)) {
-        // oki, no need for includes
-
-    } else if (file_exists($CFG->dirroot.$handler->handlerfile)) {
-        include_once($CFG->dirroot.$handler->handlerfile);
-
-    } else {
-        $errormessage = "Handler file of component $handler->component: $handler->handlerfile can not be found!";
-        return null;
-    }
-
-    // checks for handler validity
-    if (is_callable($function)) {
-        $result = call_user_func($function, $eventdata);
-        if ($result === false) {
-            $errormessage = "Handler function of component $handler->component: $handler->handlerfunction requested resending of event!";
-            return false;
-        }
-        return true;
-
-    } else {
-        $errormessage = "Handler function of component $handler->component: $handler->handlerfunction not callable function or class method!";
-        return null;
-    }
-}
-
-/**
- * given a queued handler, call the respective event handler to process the event
- *
- * @access protected To be used from eventslib only
- *
- * @param stdClass $qhandler events_queued_handler row from db
- * @return boolean true means event processed, false means retry later, NULL means fatal failure
- */
-function events_process_queued_handler($qhandler) {
-    global $DB;
-
-    // get handler
-    if (!$handler = $DB->get_record('events_handlers', array('id'=>$qhandler->handlerid))) {
-        debugging("Error processing queue handler $qhandler->id, missing handler id: $qhandler->handlerid");
-        //irrecoverable error, remove broken queue handler
-        events_dequeue($qhandler);
-        return NULL;
-    }
-
-    // get event object
-    if (!$event = $DB->get_record('events_queue', array('id'=>$qhandler->queuedeventid))) {
-        // can't proceed with no event object - might happen when two crons running at the same time
-        debugging("Error processing queue handler $qhandler->id, missing event id: $qhandler->queuedeventid");
-        //irrecoverable error, remove broken queue handler
-        events_dequeue($qhandler);
-        return NULL;
-    }
-
-    // call the function specified by the handler
-    try {
-        $errormessage = 'Unknown error';
-        if (events_dispatch($handler, unserialize(base64_decode($event->eventdata)), $errormessage)) {
-            //everything ok
-            events_dequeue($qhandler);
-            return true;
-        }
-    } catch (Exception $e) {
-        // the problem here is that we do not want one broken handler to stop all others,
-        // cron handlers are very tricky because the needed data might have been deleted before the cron execution
-        $errormessage = "Handler function of component $handler->component: $handler->handlerfunction threw exception :" .
-                $e->getMessage() . "\n" . format_backtrace($e->getTrace(), true);
-        if (!empty($e->debuginfo)) {
-            $errormessage .= $e->debuginfo;
-        }
-    }
-
-    //dispatching failed
-    $qh = new stdClass();
-    $qh->id           = $qhandler->id;
-    $qh->errormessage = $errormessage;
-    $qh->timemodified = time();
-    $qh->status       = $qhandler->status + 1;
-    $DB->update_record('events_queue_handlers', $qh);
-
-    debugging($errormessage);
-
-    return false;
-}
-
 /**
  * Removes this queued handler from the events_queued_handler table
  *
@@ -413,205 +139,3 @@ function events_get_handlers($eventname) {
 
     return $handlers[$eventname];
 }
-
-/**
- * Events cron will try to empty the events queue by processing all the queued events handlers
- *
- * @access public Part of the public API
- * @category event
- * @param string $eventname empty means all
- * @return int number of dispatched events
- */
-function events_cron($eventname='') {
-    global $DB;
-
-    $failed = array();
-    $processed = 0;
-
-    if ($eventname) {
-        $sql = "SELECT qh.*
-                  FROM {events_queue_handlers} qh, {events_handlers} h
-                 WHERE qh.handlerid = h.id AND h.eventname=?
-              ORDER BY qh.id";
-        $params = array($eventname);
-    } else {
-        $sql = "SELECT *
-                  FROM {events_queue_handlers}
-              ORDER BY id";
-        $params = array();
-    }
-
-    $rs = $DB->get_recordset_sql($sql, $params);
-    foreach ($rs as $qhandler) {
-        if (isset($failed[$qhandler->handlerid])) {
-            // do not try to dispatch any later events when one already asked for retry or ended with exception
-            continue;
-        }
-        $status = events_process_queued_handler($qhandler);
-        if ($status === false) {
-            // handler is asking for retry, do not send other events to this handler now
-            $failed[$qhandler->handlerid] = $qhandler->handlerid;
-        } else if ($status === NULL) {
-            // means completely broken handler, event data was purged
-            $failed[$qhandler->handlerid] = $qhandler->handlerid;
-        } else {
-            $processed++;
-        }
-    }
-    $rs->close();
-
-    // remove events that do not have any handlers waiting
-    $sql = "SELECT eq.id
-              FROM {events_queue} eq
-              LEFT JOIN {events_queue_handlers} qh ON qh.queuedeventid = eq.id
-             WHERE qh.id IS NULL";
-    $rs = $DB->get_recordset_sql($sql);
-    foreach ($rs as $event) {
-        //debugging('Purging stale event '.$event->id);
-        $DB->delete_records('events_queue', array('id'=>$event->id));
-    }
-    $rs->close();
-
-    return $processed;
-}
-
-/**
- * Do not call directly, this is intended to be used from new event base only.
- *
- * @private
- * @param string $eventname name of the event
- * @param mixed $eventdata event data object
- * @return int number of failed events
- */
-function events_trigger_legacy($eventname, $eventdata) {
-    global $CFG, $USER, $DB;
-
-    $failedcount = 0; // number of failed events.
-
-    // pull out all registered event handlers
-    if ($handlers = events_get_handlers($eventname)) {
-        foreach ($handlers as $handler) {
-            $errormessage = '';
-
-            if ($handler->schedule === 'instant') {
-                if ($handler->status) {
-                    //check if previous pending events processed
-                    if (!$DB->record_exists('events_queue_handlers', array('handlerid'=>$handler->id))) {
-                        // ok, queue is empty, lets reset the status back to 0 == ok
-                        $handler->status = 0;
-                        $DB->set_field('events_handlers', 'status', 0, array('id'=>$handler->id));
-                        // reset static handler cache
-                        events_get_handlers('reset');
-                    }
-                }
-
-                // dispatch the event only if instant schedule and status ok
-                if ($handler->status or (!$handler->internal and $DB->is_transaction_started())) {
-                    // increment the error status counter
-                    $handler->status++;
-                    $DB->set_field('events_handlers', 'status', $handler->status, array('id'=>$handler->id));
-                    // reset static handler cache
-                    events_get_handlers('reset');
-
-                } else {
-                    $errormessage = 'Unknown error';
-                    $result = events_dispatch($handler, $eventdata, $errormessage);
-                    if ($result === true) {
-                        // everything is fine - event dispatched
-                        continue;
-                    } else if ($result === false) {
-                        // retry later - set error count to 1 == send next instant into cron queue
-                        $DB->set_field('events_handlers', 'status', 1, array('id'=>$handler->id));
-                        // reset static handler cache
-                        events_get_handlers('reset');
-                    } else {
-                        // internal problem - ignore the event completely
-                        $failedcount ++;
-                        continue;
-                    }
-                }
-
-                // update the failed counter
-                $failedcount ++;
-
-            } else if ($handler->schedule === 'cron') {
-                //ok - use queueing of events only
-
-            } else {
-                // unknown schedule - ignore event completely
-                debugging("Unknown handler schedule type: $handler->schedule");
-                $failedcount ++;
-                continue;
-            }
-
-            // if even type is not instant, or dispatch asked for retry, queue it
-            $event = new stdClass();
-            $event->userid      = $USER->id;
-            $event->eventdata   = base64_encode(serialize($eventdata));
-            $event->timecreated = time();
-            if (debugging()) {
-                $dump = '';
-                $callers = debug_backtrace();
-                foreach ($callers as $caller) {
-                    if (!isset($caller['line'])) {
-                        $caller['line'] = '?';
-                    }
-                    if (!isset($caller['file'])) {
-                        $caller['file'] = '?';
-                    }
-                    $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1);
-                    if (isset($caller['function'])) {
-                        $dump .= ': call to ';
-                        if (isset($caller['class']))&n