Merge branch 'MDL-52383'
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 20 Jan 2016 00:26:22 +0000 (01:26 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 20 Jan 2016 00:26:22 +0000 (01:26 +0100)
96 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/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]
cohort/externallib.php
cohort/upgrade.txt
course/format/renderer.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
lang/en/admin.php
lib/amd/build/str.min.js
lib/amd/src/str.js
lib/classes/log/sql_internal_reader.php [deleted file]
lib/classes/log/sql_select_reader.php [deleted file]
lib/deprecatedlib.php
lib/eventslib.php
lib/grade/grade_category.php
lib/grade/tests/grade_category_test.php
lib/moodlelib.php
lib/phpunit/classes/advanced_testcase.php
lib/rsslib.php
lib/testing/classes/tests_finder.php
lib/tests/event_test.php
lib/tests/eventslib_test.php
lib/upgrade.txt
lib/weblib.php
lib/wiki_to_markdown.php
message/externallib.php
message/lib.php
message/tests/externallib_test.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/tests/locallib_test.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/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/locallib.php
mod/workshop/form/rubric/styles.css
mod/workshop/styles.css
report/log/classes/table_log.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"
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>
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 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 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 1ecd467..5f81af9 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';
index 33d26c9..d1e7660 100644 (file)
Binary files a/lib/amd/build/str.min.js and b/lib/amd/build/str.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/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 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 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'])) {
-                            $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
- *
- * @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;
-    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
- *
- * @param string $eventname name of the event
- * @return int number of queued events
- */
-function events_pending_count($eventname) {
-    global $DB;
-
-    $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 77b3c94..672fc08 100644 (file)
@@ -838,11 +838,12 @@ class grade_category extends grade_object {
 
             $itemlist['userid'] = $userid;
 
-            $DB->set_field_select('grade_grades',
-                                  'aggregationstatus',
-                                  'novalue',
-                                  "itemid $itemsql AND userid = :userid",
-                                  $itemlist);
+            $sql = "UPDATE {grade_grades}
+                       SET aggregationstatus = 'novalue',
+                           aggregationweight = 0
+                     WHERE itemid $itemsql AND userid = :userid";
+
+            $DB->execute($sql, $itemlist);
         }
 
         // Dropped.
@@ -851,11 +852,12 @@ class grade_category extends grade_object {
 
             $itemlist['userid'] = $userid;
 
-            $DB->set_field_select('grade_grades',
-                                  'aggregationstatus',
-                                  'dropped',
-                                  "itemid $itemsql AND userid = :userid",
-                                  $itemlist);
+            $sql = "UPDATE {grade_grades}
+                       SET aggregationstatus = 'dropped',
+                           aggregationweight = 0
+                     WHERE itemid $itemsql AND userid = :userid";
+
+            $DB->execute($sql, $itemlist);
         }
 
         // Extra credit.
index 5451609..5294b94 100644 (file)
@@ -49,6 +49,7 @@ class core_grade_category_testcase extends grade_base_testcase {
         $this->sub_test_grade_category_load_parent_category();
         $this->sub_test_grade_category_get_parent_category();
         $this->sub_test_grade_category_get_name();
+        $this->sub_test_grade_category_generate_grades_aggregationweight();
         $this->sub_test_grade_category_set_parent();
         $this->sub_test_grade_category_get_final();
         $this->sub_test_grade_category_get_sortorder();
@@ -238,6 +239,64 @@ class core_grade_category_testcase extends grade_base_testcase {
         $this->assertEquals(1, $grade_category->grade_item->needsupdate);
     }
 
+    /**
+     * Tests the setting of the grade_grades aggregationweight column.
+     * Currently, this is only a regression test for MDL-51715.
+     * This must be run before sub_test_grade_category_set_parent(), which alters
+     * the fixture.
+     */
+    protected function sub_test_grade_category_generate_grades_aggregationweight() {
+        global $DB;
+
+        // Start of regression test for MDL-51715.
+        // grade_categories [1] and [2] are child categories of [0]
+        // Ensure that grades have been generated with fixture data.
+        $childcat1 = new grade_category($this->grade_categories[1]);
+        $childcat1itemid = $childcat1->load_grade_item()->id;
+        $childcat1->generate_grades();
+        $childcat2 = new grade_category($this->grade_categories[2]);
+        $childcat2itemid = $childcat2->load_grade_item()->id;
+        $childcat2->generate_grades();
+        $parentcat = new grade_category($this->grade_categories[0]);
+        $parentcat->generate_grades();
+
+        // Drop low and and re-generate to produce 'dropped' aggregation status.
+        $parentcat->droplow = 1;
+        $parentcat->generate_grades();
+
+        $this->assertTrue($DB->record_exists_select(
+                                     'grade_grades',
+                                     "aggregationstatus='dropped' and itemid in (?,?)",
+                                     array($childcat1itemid, $childcat2itemid)));
+        $this->assertFalse($DB->record_exists_select(
+                                     'grade_grades',
+                                     "aggregationstatus='dropped' and aggregationweight > 0.00"),
+                           "aggregationweight should be 0.00 if aggregationstatus=='dropped'");
+
+        // Reset grade data to be consistent with fixture data.
+        $parentcat->droplow = 0;
+        $parentcat->generate_grades();
+
+        // Blank out the final grade for one of the child categories and re-generate
+        // to produce 'novalue' aggregationstatus.  Direct DB update is testing shortcut.
+        $DB->set_field('grade_grades', 'finalgrade', null, array('itemid'=>$childcat1itemid));
+        $parentcat->generate_grades();
+
+        $this->assertTrue($DB->record_exists_select(
+                                     'grade_grades',
+                                     "aggregationstatus='novalue' and itemid = ?",
+                                     array($childcat1itemid)));
+        $this->assertFalse($DB->record_exists_select(
+                                     'grade_grades',
+                                     "aggregationstatus='novalue' and aggregationweight > 0.00"),
+                           "aggregationweight should be 0.00 if aggregationstatus=='novalue'");
+
+        // Re-generate to be consistent with fixture data.
+        $childcat1->generate_grades();
+        $parentcat->generate_grades();
+        // End of regression test for MDL-51715.
+    }
+
     /**
      * Tests the calculation of grades using the various aggregation methods with and without hidden grades
      * This will not work entirely until MDL-11837 is done
index 5638971..813d94b 100644 (file)
@@ -3264,7 +3264,7 @@ function fullname($user, $override=false) {
     $allnames = get_all_user_name_fields();
     if ($CFG->debugdeveloper) {
         foreach ($allnames as $allname) {
-            if (!array_key_exists($allname, $user)) {
+            if (!property_exists($user, $allname)) {
                 // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed.
                 debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER);
                 // Message has been sent, no point in sending the message multiple times.
index 7ee75eb..1c0bd81 100644 (file)
@@ -307,6 +307,44 @@ abstract class advanced_testcase extends base_testcase {
         $this->resetDebugging();
     }
 
+    /**
+     * Asserts how many times debugging has been called.
+     *
+     * @param int $expectedcount The expected number of times
+     * @param array $debugmessages Expected debugging messages, one for each expected message.
+     * @param array $debuglevels Expected debugging levels, one for each expected message.
+     * @param string $message
+     * @return void
+     */
+    public function assertDebuggingCalledCount($expectedcount, $debugmessages = array(), $debuglevels = array(), $message = '') {
+        if (!is_int($expectedcount)) {
+            throw new coding_exception('assertDebuggingCalledCount $expectedcount argument should be an integer.');
+        }
+
+        $debugging = $this->getDebuggingMessages();
+        $this->assertEquals($expectedcount, count($debugging), $message);
+
+        if ($debugmessages) {
+            if (!is_array($debugmessages) || count($debugmessages) != $expectedcount) {
+                throw new coding_exception('assertDebuggingCalledCount $debugmessages should contain ' . $expectedcount . ' messages');
+            }
+            foreach ($debugmessages as $key => $debugmessage) {
+                $this->assertSame($debugmessage, $debugging[$key]->message, $message);
+            }
+        }
+
+        if ($debuglevels) {
+            if (!is_array($debuglevels) || count($debuglevels) != $expectedcount) {
+                throw new coding_exception('assertDebuggingCalledCount $debuglevels should contain ' . $expectedcount . ' messages');
+            }
+            foreach ($debuglevels as $key => $debuglevel) {
+                $this->assertSame($debuglevel, $debugging[$key]->level, $message);
+            }
+        }
+
+        $this->resetDebugging();
+    }
+
     /**
      * Call when no debugging() messages expected.
      * @param string $message
index 74696f8..2be2a5c 100644 (file)
@@ -80,12 +80,12 @@ function rss_get_link($contextid, $userid, $componentname, $id, $tooltiptext='')
  */
 function rss_get_url($contextid, $userid, $componentname, $additionalargs) {
     global $CFG;
-    require_once($CFG->libdir.'/filelib.php');
     if (empty($userid)) {
         $userid = guest_user()->id;
     }
     $usertoken = rss_get_token($userid);
-    return get_file_url($contextid.'/'.$usertoken.'/'.$componentname.'/'.$additionalargs.'/rss.xml', null, 'rssfile');
+    $url = '/rss/file.php';
+    return moodle_url::make_file_url($url, '/'.$contextid.'/'.$usertoken.'/'.$componentname.'/'.$additionalargs.'/rss.xml');
 }
 
 /**
index 666e340..946ec23 100644 (file)
@@ -125,15 +125,30 @@ class tests_finder {
     private static function get_all_directories_with_tests($testtype) {
         global $CFG;
 
+        // List of directories to exclude from test file searching.
+        $excludedir = array('node_modules', 'vendor');
+
+        // Get first level directories in which tests should be searched.
+        $directoriestosearch = array();
+        $alldirs = glob($CFG->dirroot . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR);
+        foreach ($alldirs as $dir) {
+            if (!in_array(basename($dir), $excludedir) && (filetype($dir) != 'link')) {
+                $directoriestosearch[] = $dir;
+            }
+        }
+
+        // Search for tests in valid directories.
         $dirs = array();
-        $dirite = new RecursiveDirectoryIterator($CFG->dirroot);
-        $iteite = new RecursiveIteratorIterator($dirite);
-        $regexp = self::get_regexp($testtype);
-        $regite = new RegexIterator($iteite, $regexp);
-        foreach ($regite as $path => $element) {
-            $key = dirname(dirname($path));
-            $value = trim(str_replace('/', '_', str_replace($CFG->dirroot, '', $key)), '_');
-            $dirs[$key] = $value;
+        foreach ($directoriestosearch as $dir) {
+            $dirite = new RecursiveDirectoryIterator($dir);
+            $iteite = new RecursiveIteratorIterator($dirite);
+            $regexp = self::get_regexp($testtype);
+            $regite = new RegexIterator($iteite, $regexp);
+            foreach ($regite as $path => $element) {
+                $key = dirname(dirname($path));
+                $value = trim(str_replace(DIRECTORY_SEPARATOR, '_', str_replace($CFG->dirroot, '', $key)), '_');
+                $dirs[$key] = $value;
+            }
         }
         ksort($dirs);
         return array_flip($dirs);
index 55cdbf9..f260cc6 100644 (file)
@@ -29,6 +29,8 @@ require_once(__DIR__.'/fixtures/event_fixtures.php');
 
 class core_event_testcase extends advanced_testcase {
 
+    const DEBUGGING_MSG = 'Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.';
+
     public function test_event_properties() {
         global $USER;
 
@@ -591,6 +593,7 @@ class core_event_testcase extends advanced_testcase {
 
         $DB->delete_records('log', array());
         events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
         $DB->delete_records_select('events_handlers', "component <> 'unittest'");
         events_get_handlers('reset');
         $this->assertEquals(3, $DB->count_records('events_handlers'));
@@ -601,10 +604,12 @@ class core_event_testcase extends advanced_testcase {
 
         $event1 = \core_tests\event\unittest_executed::create(array('context'=>\context_system::instance(), 'other'=>array('sample'=>5, 'xx'=>10)));
         $event1->trigger();
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
 
         $event2 = \core_tests\event\unittest_executed::create(array('context'=>\context_system::instance(), 'other'=>array('sample'=>6, 'xx'=>11)));
         $event2->nest = true;
         $event2->trigger();
+        $this->assertDebuggingCalledCount(2, array(self::DEBUGGING_MSG, self::DEBUGGING_MSG), array(DEBUG_DEVELOPER, DEBUG_DEVELOPER));
 
         $this->assertSame(
             array('observe_all-5', 'observe_one-5', 'legacy_handler-0', 'observe_all-nesting-6', 'legacy_handler-0', 'observe_one-6', 'observe_all-666', 'observe_one-666', 'legacy_handler-0'),
index 0428744..c62d5cf 100644 (file)
@@ -29,6 +29,8 @@ defined('MOODLE_INTERNAL') || die();
 
 class core_eventslib_testcase extends advanced_testcase {
 
+    const DEBUGGING_MSG = 'Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.';
+
     /**
      * Create temporary entries in the database for these tests.
      * These tests have to work no matter the data currently in the database
@@ -40,7 +42,6 @@ class core_eventslib_testcase extends advanced_testcase {
         // Set global category settings to -1 (not force).
         eventslib_sample_function_handler('reset');
         eventslib_sample_handler_class::static_method('reset');
-        events_update_definition('unittest');
 
         $this->resetAfterTest();
     }
@@ -51,6 +52,9 @@ class core_eventslib_testcase extends advanced_testcase {
     public function test_events_update_definition__install() {
         global $DB;
 
+        events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
         $dbcount = $DB->count_records('events_handlers', array('component'=>'unittest'));
         $handlers = array();
         require(__DIR__.'/fixtures/events.php');
@@ -63,6 +67,9 @@ class core_eventslib_testcase extends advanced_testcase {
     public function test_events_update_definition__uninstall() {
         global $DB;
 
+        events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
         events_uninstall('unittest');
         $this->assertEquals(0, $DB->count_records('events_handlers', array('component'=>'unittest')), 'All handlers should be uninstalled: %s');
     }
@@ -72,6 +79,10 @@ class core_eventslib_testcase extends advanced_testcase {
      */
     public function test_events_update_definition__update() {
         global $DB;
+
+        events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
         // First modify directly existing handler.
         $handler = $DB->get_record('events_handlers', array('component'=>'unittest', 'eventname'=>'test_instant'));
 
@@ -82,6 +93,7 @@ class core_eventslib_testcase extends advanced_testcase {
 
         // Update the definition, it should revert the handler back.
         events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
         $handler = $DB->get_record('events_handlers', array('component'=>'unittest', 'eventname'=>'test_instant'));
         $this->assertSame($handler->handlerfunction, $original, 'update should sync db with file definition: %s');
     }
@@ -90,15 +102,27 @@ class core_eventslib_testcase extends advanced_testcase {
      * Tests events_trigger_is_registered() function.
      */
     public function test_events_is_registered() {
+
+        events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
         $this->assertTrue(events_is_registered('test_instant', 'unittest'));
+        $this->assertDebuggingCalled('events_is_registered() has been deprecated along with all Events 1 API in favour of Events 2' .
+            ' API, please use it instead.', DEBUG_DEVELOPER);
     }
 
     /**
      * Tests events_trigger_legacy() function.
      */
     public function test_events_trigger_legacy_instant() {
+
+        events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
         $this->assertEquals(0, events_trigger_legacy('test_instant', 'ok'));
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
         $this->assertEquals(0, events_trigger_legacy('test_instant', 'ok'));
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
         $this->assertEquals(2, eventslib_sample_function_handler('status'));
     }
 
@@ -106,9 +130,16 @@ class core_eventslib_testcase extends advanced_testcase {
      * Tests events_trigger_legacy() function.
      */
     public function test_events_trigger__cron() {
+
+        events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
         $this->assertEquals(0, events_trigger_legacy('test_cron', 'ok'));
         $this->assertEquals(0, eventslib_sample_handler_class::static_method('status'));
         events_cron('test_cron');
+        // The events_cron one + one for each triggered event above (triggered in events_dispatch).
+        $this->assertDebuggingCalledCount(2, array(self::DEBUGGING_MSG, self::DEBUGGING_MSG),
+            array(DEBUG_DEVELOPER, DEBUG_DEVELOPER));
         $this->assertEquals(1, eventslib_sample_handler_class::static_method('status'));
     }
 
@@ -116,10 +147,20 @@ class core_eventslib_testcase extends advanced_testcase {
      * Tests events_pending_count() function.
      */
     public function test_events_pending_count() {
+
+        events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
         events_trigger_legacy('test_cron', 'ok');
+        $this->assertDebuggingNotCalled();
         events_trigger_legacy('test_cron', 'ok');
+        $this->assertDebuggingNotCalled();
         events_cron('test_cron');
+        // The events_cron one + one for each triggered event above (triggered in events_dispatch).
+        $this->assertDebuggingCalledCount(3);
         $this->assertEquals(0, events_pending_count('test_cron'), 'all messages should be already dequeued: %s');
+        $this->assertDebuggingCalled('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2' .
+            ' API, please use it instead.', DEBUG_DEVELOPER);
     }
 
     /**
@@ -127,34 +168,64 @@ class core_eventslib_testcase extends advanced_testcase {
      */
     public function test_events_trigger__failed_instant() {
         global $CFG;
+
+        events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
         $olddebug = $CFG->debug;
 
         $this->assertEquals(1, events_trigger_legacy('test_instant', 'fail'), 'fail first event: %s');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
         $this->assertEquals(1, events_trigger_legacy('test_instant', 'ok'), 'this one should fail too: %s');
+        $this->assertDebuggingNotCalled();
 
         $this->assertEquals(0, events_cron('test_instant'), 'all events should stay in queue: %s');
-        $this->assertDebuggingCalled();
+        // events_cron + one for each dispatched event.
+        $this->assertDebuggingCalledCount(3);
 
         $this->assertEquals(2, events_pending_count('test_instant'), 'two events should in queue: %s');
+        $this->assertDebuggingCalled('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2' .
+            ' API, please use it instead.', DEBUG_DEVELOPER);
+
         $this->assertEquals(0, eventslib_sample_function_handler('status'), 'verify no event dispatched yet: %s');
         eventslib_sample_function_handler('ignorefail'); // Ignore "fail" eventdata from now on.
         $this->assertEquals(1, events_trigger_legacy('test_instant', 'ok'), 'this one should go to queue directly: %s');
+        $this->assertDebuggingNotCalled();
+
         $this->assertEquals(3, events_pending_count('test_instant'), 'three events should in queue: %s');
+        $this->assertDebuggingCalled('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2' .
+            ' API, please use it instead.', DEBUG_DEVELOPER);
+
         $this->assertEquals(0, eventslib_sample_function_handler('status'), 'verify previous event was not dispatched: %s');
         $this->assertEquals(3, events_cron('test_instant'), 'all events should be dispatched: %s');
+        // events_cron + one for each dispatched event.
+        $this->assertDebuggingCalledCount(4);
+
         $this->assertEquals(3, eventslib_sample_function_handler('status'), 'verify three events were dispatched: %s');
         $this->assertEquals(0, events_pending_count('test_instant'), 'no events should in queue: %s');
+        $this->assertDebuggingCalled('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2' .
+            ' API, please use it instead.', DEBUG_DEVELOPER);
+
         $this->assertEquals(0, events_trigger_legacy('test_instant', 'ok'), 'this event should be dispatched immediately: %s');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
         $this->assertEquals(4, eventslib_sample_function_handler('status'), 'verify event was dispatched: %s');
         $this->assertEquals(0, events_pending_count('test_instant'), 'no events should in queue: %s');
+        $this->assertDebuggingCalled('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2' .
+            ' API, please use it instead.', DEBUG_DEVELOPER);
     }
 
     /**
      * Tests events_trigger() function.
      */
     public function test_events_trigger_debugging() {
+
+        events_update_definition('unittest');
+        $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
+
         $this->assertEquals(0, events_trigger('test_instant', 'ok'));
-        $this->assertDebuggingCalled();
+        $debugmessages = array('events_trigger() is deprecated, please use new events instead', self::DEBUGGING_MSG);
+        $this->assertDebuggingCalledCount(2, $debugmessages, array(DEBUG_DEVELOPER, DEBUG_DEVELOPER));
     }
 }
 
index 02437f8..c4e9f6f 100644 (file)
@@ -34,6 +34,9 @@ information provided here is intended especially for developers.
 * groups_delete_group_members() $showfeedback parameter has been removed and is no longer
   respected. Users of this function should output their own feedback if required.
 * Number of changes to Tags API, see tag/upgrade.txt for more details
+* The previous events API handlers are being deprecated in favour of events 2 API, debugging messages are being displayed if
+  there are 3rd party plugins using it. Switch to events 2 API please, see https://docs.moodle.org/dev/Event_2#Event_dispatching_and_observers
+  Note than you will need to bump the plugin version so moodle is aware that you removed the plugin's event handlers.
 
 === 3.0 ===
 
index aed1cb4..2f2adf7 100644 (file)
@@ -728,7 +728,7 @@ class moodle_url {
         if ($forcedownload) {
             $params['forcedownload'] = 1;
         }
-
+        $path = rtrim($path, '/');
         $url = new moodle_url($urlbase, $params);
         $url->set_slashargument($path);
         return $url;
index 2950d1f..b7db659 100644 (file)
@@ -298,8 +298,7 @@ class WikiToMarkdown {
     $line = preg_replace("/ ([a-zA-Z]+):([0-9]+)\(([^)]+)\)/i",
        " [\\3](".$CFG->wwwroot."/mod/\\1/view.php?id=\\2) ", $line );
 
-    require_once($CFG->libdir.'/filelib.php');
-    $coursefileurl = get_file_url($this->courseid);
+    $coursefileurl = array(moodle_url::make_legacyfile_url($this->courseid, null));
 
     // Replace picture resource link
     $line = preg_replace("#/([a-zA-Z0-9./_-]+)(png|gif|jpg)\(([^)]+)\)#i",
index 35aef7e..9e4443e 100644 (file)
@@ -749,6 +749,14 @@ class core_message_external extends external_api {
             }
             foreach ($messages as $mid => $message) {
 
+                // Do not return deleted messages.
+                if (($useridto == $USER->id and $message->timeusertodeleted) or
+                        ($useridfrom == $USER->id and $message->timeuserfromdeleted)) {
+
+                    unset($messages[$mid]);
+                    continue;
+                }
+
                 // We need to get the user from the query.
                 if (empty($userfromfullname)) {
                     // Check for non-reply and support users.
index 52d2b77..9ded749 100644 (file)
@@ -2755,6 +2755,7 @@ function message_page_type_list($pagetype, $parentcontext, $currentcontext) {
 
 /**
  * Get messages sent or/and received by the specified users.
+ * Please note that this function return deleted messages too.
  *
  * @param  int      $useridto       the user id who received the message
  * @param  int      $useridfrom     the user id who sent the message. -10 or -20 for no-reply or support user
index dff81e4..f1e555b 100644 (file)
@@ -408,7 +408,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
      * Test get_messages.
      */
     public function test_get_messages() {
-        global $CFG;
+        global $CFG, $DB;
         $this->resetAfterTest(true);
 
         $this->preventResetByRollback();
@@ -434,6 +434,15 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $messages = external_api::clean_returnvalue(core_message_external::get_messages_returns(), $messages);
         $this->assertCount(1, $messages['messages']);
 
+        // Delete the message.
+        $message = array_shift($messages['messages']);
+        $messagetobedeleted = $DB->get_record('message_read', array('id' => $message['id']));
+        message_delete_message($messagetobedeleted, $user1->id);
+
+        $messages = core_message_external::get_messages($user2->id, $user1->id, 'conversations', true, true, 0, 0);
+        $messages = external_api::clean_returnvalue(core_message_external::get_messages_returns(), $messages);
+        $this->assertCount(0, $messages['messages']);
+
         // Get unread conversations from user1 to user2.
         $messages = core_message_external::get_messages($user2->id, $user1->id, 'conversations', false, true, 0, 0);
         $messages = external_api::clean_returnvalue(core_message_external::get_messages_returns(), $messages);
@@ -442,7 +451,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         // Get read messages send from user1.
         $messages = core_message_external::get_messages(0, $user1->id, 'conversations', true, true, 0, 0);
         $messages = external_api::clean_returnvalue(core_message_external::get_messages_returns(), $messages);
-        $this->assertCount(2, $messages['messages']);
+        $this->assertCount(1, $messages['messages']);
 
         $this->setUser($user2);
         // Get read conversations from any user to user2.
@@ -450,6 +459,20 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $messages = external_api::clean_returnvalue(core_message_external::get_messages_returns(), $messages);
         $this->assertCount(2, $messages['messages']);
 
+        // Conversations from user3 to user2.
+        $messages = core_message_external::get_messages($user2->id, $user3->id, 'conversations', true, true, 0, 0);
+        $messages = external_api::clean_returnvalue(core_message_external::get_messages_returns(), $messages);
+        $this->assertCount(1, $messages['messages']);
+
+        // Delete the message.
+        $message = array_shift($messages['messages']);
+        $messagetobedeleted = $DB->get_record('message_read', array('id' => $message['id']));
+        message_delete_message($messagetobedeleted, $user2->id);
+
+        $messages = core_message_external::get_messages($user2->id, $user3->id, 'conversations', true, true, 0, 0);
+        $messages = external_api::clean_returnvalue(core_message_external::get_messages_returns(), $messages);
+        $this->assertCount(0, $messages['messages']);
+
         $this->setUser($user3);
         // Get read notifications received by user3.
         $messages = core_message_external::get_messages($user3->id, 0, 'notifications', true, true, 0, 0);
index ca436bb..1d5ec06 100644 (file)
@@ -273,6 +273,14 @@ class assign_grading_table extends table_sql implements renderable {
             $columns[] = 'fullname';
             $headers[] = get_string('fullname');
 
+            // Participant # details if can view real identities.
+            if ($this->assignment->is_blind_marking()) {
+                if (!$this->is_downloading()) {
+                    $columns[] = 'recordid';
+                    $headers[] = get_string('recordid', 'assign');
+                }
+            }
+
             foreach ($extrauserfields as $extrafield) {
                 $columns[] = $extrafield;
                 $headers[] = get_user_field_name($extrafield);
index 44f9f80..c55c0df 100644 (file)
@@ -99,6 +99,7 @@ $string['batchoperationreverttodraft'] = 'revert submissions to draft';
 $string['batchsetallocatedmarker'] = 'Set allocated marker for {$a} selected user(s).';
 $string['batchsetmarkingworkflowstateforusers'] = 'Set marking workflow state for {$a} selected user(s).';
 $string['blindmarking'] = 'Blind marking';
+$string['blindmarkingenabledwarning'] = 'Blind marking is enabled on this activity.';
 $string['blindmarking_help'] = 'Blind marking hides the identity of students from markers. Blind marking settings will be locked once a submission or grade has been made in relation to this assignment.';
 $string['changegradewarning'] = 'This assignment has graded submissions and changing the grade will not automatically re-calculate existing submission grades. You must re-grade all existing submissions, if you wish to change the grade.';
 $string['choosegradingaction'] = 'Grading action';
index 42fbba3..a4c961b 100644 (file)
@@ -3426,6 +3426,10 @@ class assign {
             $o .= plagiarism_update_status($this->get_course(), $this->get_course_module());
         }
 
+        if ($this->is_blind_marking() && has_capability('mod/assign:viewblinddetails', $this->get_context())) {
+            $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage');
+        }
+
         // Load and print the table of submissions.
         if ($showquickgrading && $quickgrading) {
             $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, true);
index ad92370..dee37fa 100644 (file)
@@ -2278,8 +2278,8 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $this->setUser($manager);
         $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
         $output = $assign->get_renderer()->render($gradingtable);
-        $this->assertEquals(false, strpos($output, get_string('hiddenuser', 'assign')));
-        $this->assertEquals(true, strpos($output, fullname($student)));    //students full name doesn't appear.
+        $this->assertEquals(true, strpos($output, get_string('hiddenuser', 'assign')));
+        $this->assertEquals(true, strpos($output, fullname($student)));
     }
 
     /**
index a4f610a..1e105c4 100644 (file)
@@ -302,25 +302,48 @@ function lesson_delete_course($course, $feedback=true) {
  * @return object
  */
 function lesson_user_outline($course, $user, $mod, $lesson) {
-    global $CFG;
+    global $CFG, $DB;
 
     require_once("$CFG->libdir/gradelib.php");
     $grades = grade_get_grades($course->id, 'mod', 'lesson', $lesson->id, $user->id);
-
     $return = new stdClass();
+
     if (empty($grades->items[0]->grades)) {
-        $return->info = get_string("no")." ".get_string("attempts", "lesson");
+        $return->info = get_string("nolessonattempts", "lesson");
     } else {
         $grade = reset($grades->items[0]->grades);
-        $return->info = get_string("grade") . ': ' . $grade->str_long_grade;
+        if (empty($grade->grade)) {
+
+            // Check to see if it an ungraded / incomplete attempt.
+            $sql = "SELECT *
+                      FROM {lesson_timer}
+                     WHERE lessonid = :lessonid
+                       AND userid = :userid
+                  ORDER BY starttime DESC";
+            $params = array('lessonid' => $lesson->id, 'userid' => $user->id);
 
-        //datesubmitted == time created. dategraded == time modified or time overridden
-        //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
-        //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
-        if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
-            $return->time = $grade->dategraded;
+            if ($attempts = $DB->get_records_sql($sql, $params, 0, 1)) {
+                $attempt = reset($attempts);
+                if ($attempt->completed) {
+                    $return->info = get_string("completed", "lesson");
+                } else {
+                    $return->info = get_string("notyetcompleted", "lesson");
+                }
+                $return->time = $attempt->lessontime;
+            } else {
+                $return->info = get_string("nolessonattempts", "lesson");
+            }
         } else {
-            $return->time = $grade->datesubmitted;
+            $return->info = get_string("grade") . ': ' . $grade->str_long_grade;
+
+            // Datesubmitted == time created. dategraded == time modified or time overridden.
+            // If grade was last modified by the user themselves use date graded. Otherwise use date submitted.
+            // TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704.
+            if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
+                $return->time = $grade->dategraded;
+            } else {
+                $return->time = $grade->datesubmitted;
+            }
         }
     }
     return $return;
diff --git a/mod/lesson/reformat.php b/mod/lesson/reformat.php
deleted file mode 100644 (file)
index 14b3084..0000000
+++ /dev/null
@@ -1,221 +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/>.
-
-/**
- * jjg7:8/9/2004
- *
- * @package mod_lesson
- * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
- **/
-
-defined('MOODLE_INTERNAL') || die();
-
-debugging('This file functions are deprecated, please do not use this functions any more.', DEBUG_DEVELOPER);
-
-/**
- * Removes double CRs.
- *
- * @deprecated Since Moodle 2.9 MDL-48901 - please do not use this function any more.
- * @todo MDL-48985 This will be deleted in Moodle 3.1
- * @param string $filename
- * @return void
- */
-function removedoublecr($filename) {
-// This function will adjust a file in roughly Aiken style by replacing extra newlines with <br/> tags
-// so that instructors can have newlines wherever they like as long as the overall format is in Aiken
-
-    $filearray = file($filename);
-    /// Check for Macintosh OS line returns (ie file on one line), and fix
-    if (preg_match("/\r/", $filearray[0]) AND !preg_match("/\n/", $filearray[0])) {
-        $outfile = explode("\r", $filearray[0]);
-    } else {
-        $outfile = $filearray;
-    }
-
-    $outarray = array();
-
-    foreach ($outfile as $line) {
-        // remove leading and trailing whitespace
-        trim($line);
-        // check it's length, if 0 do not output... if it is > 0 output
-        if ($line[0] == "\n" OR strlen($line)==0 ) {
-            if (count($outarray) ) {
-                // get the last item in the outarray
-                $cur_pos = (count($outarray) - 1);
-                $outarray[$cur_pos] = trim($outarray[$cur_pos])."<br/>\n";
-            }
-        }
-        else {
-            $length=strlen($line);
-            if ($length==0) {
-                // don't do anything
-            }
-            else {
-                if ($line[$length-1] == "\n") {
-                    $outarray[] = $line;
-                }
-                else {
-                    $outarray[] = $line."\n";
-                }
-            }
-        }
-    }
-    // output modified file to original
-    if ( is_writable($filename) ) {
-
-        if (! $handle =fopen ($filename ,'w' )) {
-            echo "Cannot open file ($filename)" ;
-            exit;
-        }
-        foreach ($outarray as $outline) {
-            fwrite($handle, $outline);
-        }
-        fclose($handle);
-    }
-    else {
-        // file not writeable
-    }
-}
-
-/**
- * This function converts from Brusca style to Aiken.
- *
- * @deprecated Since Moodle 2.9 MDL-48901 - please do not use this function any more.
- * @todo MDL-48985 This will be deleted in Moodle 3.1
- * @param string $filename
- * @return bool Success.
- */
-function importmodifiedaikenstyle($filename) {
-// This function converts from Brusca style to Aiken
-    $lines = file($filename);
-    $answer_found = 0;
-    $responses = 0;
-    $outlines = array();
-    foreach ($lines as $line) {
-        // strip leading and trailing whitespace
-        $line = trim($line);
-        // add a space at the end, quick hack to make sure words from different lines don't run together
-        $line = $line. ' ';
-
-        // ignore lines less than 2 characters
-        if (strlen($line) < 2) {
-            continue;
-        }
-
-
-        // see if we have the answer line
-        if ($line[0] =='*') {
-            if ($line[0] == '*') {
-                $answer_found = 1;
-                $line[0]="\t";
-                $line = ltrim($line);
-                $answer = $line[0];
-            }
-        }
-
-        $leadin = substr($line, 0,2);
-        if (strpos(".A)B)C)D)E)F)G)H)I)J)a)b)c)d)e)f)g)h)i)j)A.B.C.D.E.F.G.H.I.J.a.b.c.d.e.f.g.h.i.j.", $leadin)>0) {
-
-            // re-add newline to indicate end of previous question/response
-            if (count($outlines)) {
-                $cur_pos = (count($outlines) - 1);
-                $outlines[$cur_pos] = $outlines[$cur_pos]."\n";
-            }
-
-
-            $responses = 1;
-            // make character uppercase
-            $line[0]=strtoupper($line[0]);
-
-            // make entry followed by '.'
-            $line[1]='.';
-        }
-        elseif ( ($responses AND $answer_found) OR (count($outlines)<=1) ) {
-        // we have found responses and an answer and the current line is not an answer
-            switch ($line[0]) {
-                case 1:
-                case 2:
-                case 3:
-                case 4:
-                case 5:
-                case 6:
-                case 7:
-                case 8:
-                case 9:
-
-                    // re-add newline to indicate end of previous question/response
-                    if (count($outlines)) {
-                        $cur_pos = (count($outlines) - 1);
-                        $outlines[$cur_pos] = $outlines[$cur_pos]."\n";
-                    }
-
-                    // this next ugly block is to strip out the numbers at the beginning
-                    $np = 0;
-                    // this probably could be done cleaner... it escapes me at the moment
-                    while ($line[$np] == '0' OR $line[$np] == '1' OR $line[$np] == '2'
-                            OR $line[$np] == '3' OR $line[$np] == '4'  OR $line[$np] == '5'
-                            OR $line[$np] == '6'  OR $line[$np] == '7' OR $line[$np] == '8'
-                            OR $line[$np] == '9' ) {
-                        $np++;
-                    }
-                    // grab everything after '###.'
-                    $line = substr($line, $np+1, strlen($line));
-
-                    if ($responses AND $answer_found) {
-                        $responses = 0;
-                        $answer_found = 0;
-                        $answer = strtoupper($answer);
-                        $outlines[] = "ANSWER: $answer\n\n";
-                    }
-                    break;
-            }
-        }
-        if (substr($line, 0, 14) == 'ANSWER CHOICES') {
-            // don't output this line
-        }
-        else {
-            $outlines[]=$line;
-        }
-    } // close for each line
-
-    // re-add newline to indicate end of previous question/response
-    if (count($outlines)) {
-        $cur_pos = (count($outlines) - 1);
-        $outlines[$cur_pos] = $outlines[$cur_pos]."\n";
-    }
-
-    // output the last answer
-    $answer = strtoupper($answer);
-    $outlines[] = "ANSWER: $answer\n\n";
-
-    // output modified file to original
-    if ( is_writable($filename) ) {
-        if (! $handle =fopen ($filename ,'w' )) {
-            echo "Cannot open file ($filename)" ;
-            exit;
-        }
-        foreach ($outlines as $outline) {
-            fwrite($handle, $outline);
-        }
-        fclose($handle);
-        return true;
-    }
-    else {
-        return false;
-    }
-}
diff --git a/mod/lesson/tests/behat/lesson_outline_report.feature b/mod/lesson/tests/behat/lesson_outline_report.feature
new file mode 100644 (file)
index 0000000..f92d54d
--- /dev/null
@@ -0,0 +1,183 @@
+@mod @mod_lesson
+Feature: Teachers can review student progress on all lessons in a course by viewing the overview report
+  As a Teacher
+  I need to view the overview report for one of my students.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    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 "Lesson" to section "1"
+    And I set the following fields to these values:
+      | Name | Test lesson name |
+      | Description | Test lesson description |
+      | Re-takes allowed | Yes |
+    And I press "Save and return to course"
+    And I follow "Test lesson name"
+
+  Scenario: View student progress for lesson that was never attempted
+    Given I follow "Add a content page"
+    And I set the following fields to these values:
+      | Page title | First page name |
+      | Page contents | First page contents |
+      | id_answer_editor_0 | Next page |
+      | id_jumpto_0 | Next page |
+    And I press "Save page"
+    And I select "Question" from the "qtype" singleselect
+    And I set the field "Select a question type" to "True/false"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title | True/false question 1 |
+      | Page contents | Paper is made from trees. |
+      | id_answer_editor_0 | True |
+      | id_response_editor_0 | Correct |
+      | id_jumpto_0 | Next page |
+      | id_answer_editor_1 | False |
+      | id_response_editor_1 | Wrong |
+      | id_jumpto_1 | This page |
+    And I press "Save page"
+    When I follow "Course 1"
+    And I follow "Participants"
+    And I follow "Student 1"
+    And I follow "Outline report"
+    Then I should see "No attempts have been made on this lesson"
+
+  Scenario: View student progress for an incomplete lesson containing both content and question pages
+    Given I follow "Add a content page"
+    And I set the following fields to these values:
+      | Page title | First page name |
+      | Page contents | First page contents |
+      | id_answer_editor_0 | Next page |
+      | id_jumpto_0 | Next page |
+    And I press "Save page"
+    And I select "Question" from the "qtype" singleselect
+    And I set the field "Select a question type" to "True/false"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title | True/false question 1 |
+      | Page contents | Paper is made from trees. |
+      | id_answer_editor_0 | True |
+      | id_response_editor_0 | Correct |
+      | id_jumpto_0 | Next page |
+      | id_answer_editor_1 | False |
+      | id_response_editor_1 | Wrong |
+      | id_jumpto_1 | This page |
+    And I press "Save page"
+    And I select "Add a content page" from the "qtype" singleselect
+    And I set the following fields to these values:
+      | Page title | Second page name |
+      | Page contents | Second page contents |
+      | id_answer_editor_0 | Previous page |
+      | id_jumpto_0 | Previous page |
+      | id_answer_editor_1 | Next page |
+      | id_jumpto_1 | Next page |
+    And I press "Save page"
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "First page contents"
+    And I press "Next page"
+    And I log out
+    Then I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Participants"
+    And I follow "Student 1"
+    And I follow "Outline report"
+    And I should see "Lesson has been started, but not yet completed"
+
+  Scenario: View student progress for a lesson containing both content and question pages
+    Given I follow "Add a content page"
+    And I set the following fields to these values:
+      | Page title | First page name |
+      | Page contents | First page contents |
+      | id_answer_editor_0 | Next page |
+      | id_jumpto_0 | Next page |
+    And I press "Save page"
+    And I select "Question" from the "qtype" singleselect
+    And I set the field "Select a question type" to "True/false"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title | True/false question 1 |
+      | Page contents | Paper is made from trees. |
+      | id_answer_editor_0 | True |
+      | id_response_editor_0 | Correct |
+      | id_jumpto_0 | Next page |
+      | id_answer_editor_1 | False |
+      | id_response_editor_1 | Wrong |
+      | id_jumpto_1 | This page |
+    And I press "Save page"
+    And I select "Add a content page" from the "qtype" singleselect
+    And I set the following fields to these values:
+      | Page title | Second page name |
+      | Page contents | Second page contents |
+      | id_answer_editor_0 | Previous page |
+      | id_jumpto_0 | Previous page |
+      | id_answer_editor_1 | Next page |
+      | id_jumpto_1 | Next page |
+    And I press "Save page"
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "First page contents"
+    And I press "Next page"
+    And I should see "Second page contents"
+    And I press "Next page"
+    And I should see "Paper is made from trees."
+    And I set the following fields to these values:
+      | True | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I log out
+    Then I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Participants"
+    And I follow "Student 1"
+    And I follow "Outline report"
+    And I should see "Grade: 100.00 / 100.00"
+
+  Scenario: View student attempts in a lesson containing only content pages
+    Given I follow "Add a content page"
+    And I set the following fields to these values:
+      | Page title | First page name |
+      | Page contents | First page contents |
+      | id_answer_editor_0 | Next page |
+      | id_jumpto_0 | Next page |
+    And I press "Save page"
+    And I select "Add a content page" from the "qtype" singleselect
+    And I set the following fields to these values:
+      | Page title | Second page name |
+      | Page contents | Second page contents |
+      | id_answer_editor_0 | Previous page |
+      | id_jumpto_0 | Previous page |
+      | id_answer_editor_1 | End of lesson |
+      | id_jumpto_1 | End of lesson |
+    And I press "Save page"
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "First page contents"
+    And I press "Next page"
+    And I should see "Second page contents"
+    And I press "End of lesson"
+    And I log out
+    Then I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Participants"
+    And I follow "Student 1"
+    And I follow "Outline report"
+    And I should see "Completed"
index d75bea4..3160879 100644 (file)
@@ -1,5 +1,9 @@
 This files describes API changes in the lesson code.
 
+=== 3.1 ===
+* Removed the unused file reformat.php
+* removedoublecr() and importmodifiedaikenstyle() have now been removed.
+
 === 3.0 ===
 * Removed broken high score code.  Use the activity results block instead.
 
index 7a833a9..81208fb 100644 (file)
@@ -368,9 +368,16 @@ class quiz_access_manager {
      * @return mod_quiz_preflight_check_form the form.
      */
     public function get_preflight_check_form(moodle_url $url, $attemptid) {
+        // This form normally wants POST submissins. However, it also needs to
+        // accept GET submissions. Since formslib is strict, we have to detect
+        // which case we are in, and set the form property appropriately.
+        $method = 'post';
+        if (!empty($_GET['_qf__mod_quiz_preflight_check_form'])) {
+            $method = 'get';
+        }
         return new mod_quiz_preflight_check_form($url->out_omit_querystring(),
                 array('rules' => $this->rules, 'quizobj' => $this->quizobj,
-                      'attemptid' => $attemptid, 'hidden' => $url->params()));
+                      'attemptid' => $attemptid, 'hidden' => $url->params()), $method);
     }
 
     /**
index cf7855d..41f29af 100644 (file)
@@ -38,6 +38,7 @@ class mod_quiz_preflight_check_form extends moodleform {
 
     protected function definition() {
         $mform = $this->_form;
+        $this->_form->updateAttributes(array('id' => 'mod_quiz_preflight_form'));
 
         foreach ($this->_customdata['hidden'] as $name => $value) {
             if ($name === 'sesskey') {
@@ -54,7 +55,8 @@ class mod_quiz_preflight_check_form extends moodleform {
             }
         }
 
-        $this->add_action_buttons(true, get_string('continue'));
+        $this->add_action_buttons(true, get_string('startattempt', 'quiz'));
+        $mform->setDisableShortforms();
     }
 
     public function validation($data, $files) {
index f0bee8b..106dfe9 100644 (file)
@@ -27,5 +27,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 
+$string['confirmstartheader'] = 'Timed quiz';
+$string['confirmstart'] = 'The quiz has a time limit of {$a}. Time will count down from the moment you start your attempt and you must submit before it expires. Are you sure that you wish to start now?';
 $string['pluginname'] = 'Time limit quiz access rule';
 $string['quiztimelimit'] = 'Time limit: {$a}';
index 5955840..879b4a6 100644 (file)
@@ -63,6 +63,18 @@ class quizaccess_timelimit extends quiz_access_rule_base {
             return false;
         }
         return $endtime - $timenow;
+    }
+
+    public function is_preflight_check_required($attemptid) {
+        // Warning only required if the attempt is not already started.
+        return $attemptid === null;
+    }
 
+    public function add_preflight_check_form_fields(mod_quiz_preflight_check_form $quizform,
+            MoodleQuickForm $mform, $attemptid) {
+        $mform->addElement('header', 'honestycheckheader',
+                get_string('confirmstartheader', 'quizaccess_timelimit'));
+        $mform->addElement('static', 'honestycheckmessage', '',
+                get_string('confirmstart', 'quizaccess_timelimit', format_time($this->quiz->timelimit)));
     }
 }
diff --git a/mod/quiz/amd/build/preflightcheck.min.js b/mod/quiz/amd/build/preflightcheck.min.js
new file mode 100644 (file)
index 0000000..48befcf
Binary files /dev/null and b/mod/quiz/amd/build/preflightcheck.min.js differ
diff --git a/mod/quiz/amd/src/preflightcheck.js b/mod/quiz/amd/src/preflightcheck.js
new file mode 100644 (file)
index 0000000..e9c9e17
--- /dev/null
@@ -0,0 +1,112 @@
+// 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/>.
+
+/**
+ * This class manages the confirmation pop-up (also called the pre-flight check)
+ * that is sometimes shown when a use clicks the start attempt button.
+ *
+ * This is also responsible for opening the pop-up window, if the quiz requires to be in one.
+ *
+ * @module    mod_quiz/preflightcheck
+ * @class     preflightcheck
+ * @package   mod_quiz
+ * @copyright 2016 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     3.1
+ */
+define(['jquery', 'core/yui'], function($, Y) {
+
+    /**
+     * @alias module:mod_quiz/preflightcheck
+     */
+    var t = {
+        confirmDialogue: null,
+
+        /**
+         * Initialise the start attempt button.
+         *
+         * @param {String} startButtonId the id of the start attempt button that we will be enhancing.
+         * @param {String} confirmationTitle the title of the dialogue.
+         * @param {String} confirmationForm selector for the confirmation form to show in the dialogue.
+         * @param {String} popupoptions If not null, the quiz should be launced in a pop-up.
+         */
+        init: function(startButton, confirmationTitle, confirmationForm, popupoptions) {
+            var finalStartButton = startButton;
+
+            Y.use('moodle-core-notification', 'moodle-core-formchangechecker', 'io-form', function () {
+                if (Y.one(confirmationForm)) {
+                    t.confirmDialogue = new M.core.dialogue({
+                        headerContent: confirmationTitle,
+                        bodyContent: Y.one(confirmationForm),
+                        draggable: true,
+                        visible: false,
+                        center: true,
+                        modal: true,
+                        width: null,
+                        extraClasses: ['mod_quiz_preflight_popup']
+                    });
+
+                    Y.one(startButton).on('click', t.displayDialogue);
+                    Y.one('#id_cancel').on('click', t.hideDialogue);
+
+                    finalStartButton = t.confirmDialogue.get('boundingBox').one('[name="submitbutton"]');
+                }
+
+                if (popupoptions) {
+                    Y.one(finalStartButton).on('click', t.launchQuizPopup, t, popupoptions);
+                }
+            });
+        },
+
+        /**
+         * Display the dialogue.
+         * @param {Y.EventFacade} e the event being responded to, if any.
+         */
+        displayDialogue: function(e) {
+            if (e) {
+                e.halt();
+            }
+            t.confirmDialogue.show();
+        },
+
+        /**
+         * Hide the dialogue.
+         * @param {Y.EventFacade} e the event being responded to, if any.
+         */
+        hideDialogue: function(e) {
+            if (e) {
+                e.halt();
+            }
+            t.confirmDialogue.hide(e);
+        },
+
+        /**
+         * Event handler for the quiz start attempt button.
+         */
+        launchQuizPopup: function(e, popupoptions) {
+            e.halt();
+            M.core_formchangechecker.reset_form_dirty_state();
+            var form = e.target.ancestor('form');
+            window.openpopup(e, {
+                url: form.get('action') + '?' + Y.IO.stringify(form).replace(/\bcancel=/, 'x='),
+                windowname: 'quizpopup',
+                options: popupoptions,
+                fullscreen: true,
+            });
+        }
+    };
+
+    return t;
+});
index 2281986..ed1e9c5 100644 (file)
@@ -371,23 +371,13 @@ class quiz {
     // Bits of content =========================================================
 
     /**
-     * @param bool $unfinished whether there is currently an unfinished attempt active.
-     * @return string if the quiz policies merit it, return a warning string to
-     *      be displayed in a javascript alert on the start attempt button.
+     * @param bool $notused not used.
+     * @return string an empty string.
+     * @deprecated since 3.1. This sort of functionality is now entirely handled by quiz access rules.
      */
-    public function confirm_start_attempt_message($unfinished) {
-        if ($unfinished) {
-            return '';
-        }
-
-        if ($this->quiz->timelimit && $this->quiz->attempts) {
-            return get_string('confirmstartattempttimelimit', 'quiz', $this->quiz->attempts);
-        } else if ($this->quiz->timelimit) {
-            return get_string('confirmstarttimelimit', 'quiz');
-        } else if ($this->quiz->attempts) {
-            return get_string('confirmstartattemptlimit', 'quiz', $this->quiz->attempts);
-        }
-
+    public function confirm_start_attempt_message($notused) {
+        debugging('confirm_start_attempt_message is deprecated. ' .
+                'This sort of functionality is now entirely handled by quiz access rules.');
         return '';
     }
 
index c1961ab..08942de 100644 (file)
@@ -32,6 +32,7 @@ $slot = required_param('slot', PARAM_INT); // The question number in the attempt
 $PAGE->set_url('/mod/quiz/comment.php', array('attempt' => $attemptid, 'slot' => $slot));
 
 $attemptobj = quiz_attempt::create($attemptid);
+$student = $DB->get_record('user', array('id' => $attemptobj->get_userid()));
 
 // Can only grade finished attempts.
 if (!$attemptobj->is_finished()) {
@@ -44,6 +45,9 @@ $attemptobj->require_capability('mod/quiz:grade');
 
 // Print the page header.
 $PAGE->set_pagelayout('popup');
+$PAGE->set_title(get_string('manualgradequestion', 'quiz', array(
+        'question' => format_string($attemptobj->get_question_name($slot)),
+        'quiz' => format_string($attemptobj->get_quiz_name()), 'user' => fullname($student))));
 $PAGE->set_heading($attemptobj->get_course()->fullname);
 $output = $PAGE->get_renderer('mod_quiz');
 echo $output->header();
@@ -51,6 +55,16 @@ echo $output->header();
 // Prepare summary information about this question attempt.
 $summarydata = array();
 
+// Student name.
+$userpicture = new user_picture($student);
+$userpicture->courseid = $attemptobj->get_courseid();
+$summarydata['user'] = array(
+    'title'   => $userpicture,
+    'content' => new action_link(new moodle_url('/user/view.php', array(
+            'id' => $student->id, 'course' => $attemptobj->get_courseid())),
+            fullname($student, true)),
+);
+
 // Quiz name.
 $summarydata['quizname'] = array(
     'title'   => get_string('modulename', 'quiz'),
index f119782..b8ff44b 100644 (file)
@@ -172,7 +172,7 @@ foreach ($quizzes as $quiz) {
         // Grade and feedback.
         $attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'all');
         list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(
-                $quiz, $attempts, $context);
+                $quiz, $attempts);
 
         $grade = '';
         $feedback = '';
index 7f5f084..ca75002 100644 (file)
@@ -202,9 +202,6 @@ $string['confirmclose'] = 'Once you submit, you will no longer be able to change
 $string['confirmremovequestion'] = 'Are you sure you want to remove this {$a} question?';
 $string['confirmremovesectionheading'] = 'Are you sure you want to remove the \'{$a}\' section heading?';
 $string['confirmserverdelete'] = 'Are you sure you want to remove the server <b>{$a}</b> from the list?';
-$string['confirmstartattemptlimit'] = 'Number of attempts allowed:  {$a}. You are about to start a new attempt.  Do you wish to proceed?';
-$string['confirmstartattempttimelimit'] = 'This quiz has a time limit and is limited to {$a} attempt(s). You are about to start a new attempt.  Do you wish to proceed?';
-$string['confirmstarttimelimit'] = 'The quiz has a time limit. Are you sure that you wish to start?';
 $string['connectionok'] = 'Network connection restored. You may continue safely.';
 $string['connectionerror'] = 'Network connection lost. (Autosave failed).
 
@@ -458,6 +455,7 @@ $string['loadingquestionsfailed'] = 'Loading questions failed: {$a}';
 $string['makecopy'] = 'Save as new question';
 $string['managetypes'] = 'Manage question types and servers';
 $string['manualgrading'] = 'Grading';
+$string['manualgradequestion'] = 'Manually grade question {$a->question} in {$a->quiz} by {$a->user}';
 $string['mark'] = 'Submit';
 $string['markall'] = 'Submit page';
 $string['marks'] = 'Marks';
@@ -498,6 +496,8 @@ $string['multichoice'] = 'Multiple choice';
 $string['multipleanswers'] = 'Choose at least one answer.';
 $string['mustbesubmittedby'] = 'This attempt must be submitted by {$a}.';
 $string['name'] = 'Name';
+$string['navigatenext'] = 'Next page';
+$string['navigateprevious'] = 'Previous page';
 $string['navmethod'] = 'Navigation method';
 $string['navmethod_free'] = 'Free';
 $string['navmethod_help'] = 'When sequential navigation is enabled a student must progress through the quiz in order and may not return to previous pages nor skip ahead.';
@@ -771,6 +771,7 @@ $string['reviewimmediately'] = 'Immediately after the attempt';
 $string['reviewnever'] = 'Never allow review';
 $string['reviewofattempt'] = 'Review of attempt {$a}';
 $string['reviewofpreview'] = 'Review of preview';
+$string['reviewofquestion'] = 'Review of question {$a->question} in {$a->quiz} by {$a->user}';
 $string['reviewopen'] = 'Later, while the quiz is still open';
 $string['reviewoptions'] = 'Students may review';
 $string['reviewoptionsheading'] = 'Review options';
index cf54fbb..fea6e99 100644 (file)
@@ -1416,8 +1416,6 @@ function quiz_get_review_options($quiz, $attempt, $context) {
  *
  * @param object $quiz the quiz instance.
  * @param array $attempts an array of attempt objects.
- * @param $context the roles and permissions context,
- *          normally the context for the quiz module instance.
  *
  * @return array of two options objects, one showing which options are true for
  *          at least one of the attempts, the other showing which options are true
index aafb48c..aaa3926 100644 (file)
@@ -272,23 +272,6 @@ M.mod_quiz.secure_window = {
         e.halt();
     },
 
-    /**
-     * Event handler for the quiz start attempt button.
-     */
-    start_attempt_action: function(e, args) {
-        if (args.startattemptwarning == '') {
-            openpopup(e, args);
-        } else {
-            M.util.show_confirm_dialog(e, {
-                message: args.startattemptwarning,
-                callback: function() {
-                    openpopup(e, args);
-                },
-                continuelabel: M.util.get_string('startattempt', 'quiz')
-            });
-        }
-    },
-
     init_close_button: function(Y, url) {
         Y.on('click', function(e) {
             M.mod_quiz.secure_window.close(url, 0)
index 1b5070e..a604711 100644 (file)
@@ -39,6 +39,7 @@ $timenow = time();
 $attemptid     = required_param('attempt',  PARAM_INT);
 $thispage      = optional_param('thispage', 0, PARAM_INT);
 $nextpage      = optional_param('nextpage', 0, PARAM_INT);
+$previous      = optional_param('previous',      false, PARAM_BOOL);
 $next          = optional_param('next',          false, PARAM_BOOL);
 $finishattempt = optional_param('finishattempt', false, PARAM_BOOL);
 $timeup        = optional_param('timeup',        0,      PARAM_BOOL); // True if form was submitted by timer.
@@ -50,6 +51,8 @@ $attemptobj = quiz_attempt::create($attemptid);
 // Set $nexturl now.
 if ($next) {
     $page = $nextpage;
+} else if ($previous && $thispage > 0) {
+    $page = $thispage - 1;
 } else {
     $page = $thispage;
 }
index b11ed81..855a7e9 100644 (file)
@@ -92,10 +92,11 @@ class mod_quiz_renderer extends plugin_renderer_base {
     /**
      * Renders the review question pop-up.
      *
+     * @param quiz_attempt $attemptobj an instance of quiz_attempt.
      * @param string $message Why the review is not allowed.
      * @return string html to output.
      */
-    public function review_question_not_allowed($message) {
+    public function review_question_not_allowed(quiz_attempt $attemptobj, $message) {
         $output = '';
         $output .= $this->header();
         $output .= $this->heading(format_string($attemptobj->get_quiz_name(), true,
@@ -237,25 +238,36 @@ class mod_quiz_renderer extends plugin_renderer_base {
                     array($url), false, quiz_get_js_module());
             return html_writer::empty_tag('input', array('type' => 'button',
                     'value' => get_string('finishreview', 'quiz'),
-                    'id' => 'secureclosebutton'));
+                    'id' => 'secureclosebutton',
+                    'class' => 'mod_quiz-next-nav'));
 
         } else {
-            return html_writer::link($url, get_string('finishreview', 'quiz'));
+            return html_writer::link($url, get_string('finishreview', 'quiz'),
+                    array('class' => 'mod_quiz-next-nav'));
         }
     }
 
     /**
-     * Creates a next page arrow or the finishing link
+     * Creates the navigation links/buttons at the bottom of the reivew attempt page.
+     *
+     * Note, the name of this function is no longer accurate, but when the design
+     * changed, it was decided to keep the old name for backwards compatibility.
      *
      * @param quiz_attempt $attemptobj instance of quiz_attempt
      * @param int $page the current page
      * @param bool $lastpage if true current page is the last page
      */
     public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpage) {
+        $nav = '';
+        if ($page > 0) {
+            $nav .= link_arrow_left(get_string('navigateprevious', 'quiz'),
+                    $attemptobj->review_url(null, $page - 1), false, 'mod_quiz-prev-nav');
+        }
         if ($lastpage) {
-            $nav = $this->finish_review_link($attemptobj);
+            $nav .= $this->finish_review_link($attemptobj);
         } else {
-            $nav = link_arrow_right(get_string('next'), $attemptobj->review_url(null, $page + 1));
+            $nav .= link_arrow_right(get_string('navigatenext', 'quiz'),
+                    $attemptobj->review_url(null, $page + 1), false, 'mod_quiz-next-nav');
         }
         return html_writer::tag('div', $nav, array('class' => 'submitbtns'));
     }
@@ -410,9 +422,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
         $output .= $this->heading(format_string($quizobj->get_quiz_name(), true,
                                   array("context" => $quizobj->get_context())));
         $output .= $this->quiz_intro($quizobj->get_quiz(), $quizobj->get_cm());
-        ob_start();
-        $mform->display();
-        $output .= ob_get_clean();
+        $output .= $mform->render();
         $output .= $this->footer();
         return $output;
     }
@@ -476,10 +486,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
                     $attemptobj->attempt_url($slot, $page), $this);
         }
 
-        $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
-        $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next',
-                'value' => get_string('next')));
-        $output .= html_writer::end_tag('div');
+        $output .= $this->attempt_navigation_buttons($page, $attemptobj->is_last_page($page));
 
         // Some hidden fields to trach what is going on.
         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt',
@@ -510,6 +517,33 @@ class mod_quiz_renderer extends plugin_renderer_base {
         return $output;
     }
 
+    /**
+     * Display the prev/next buttons that go at the bottom of each page of the attempt.
+     *
+     * @param int $page the page number. Starts at 0 for the first page.
+     * @param bool $lastpage is this the last page in the quiz?
+     * @return string HTML fragment.
+     */
+    protected function attempt_navigation_buttons($page, $lastpage) {
+        $output = '';
+
+        $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
+        if ($page > 0) {
+            $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'previous',
+                    'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav'));
+        }
+        if ($lastpage) {
+            $nextlabel = get_string('endtest', 'quiz');
+        } else {
+            $nextlabel = get_string('navigatenext', 'quiz');
+        }
+        $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next',
+                'value' => $nextlabel, 'class' => 'mod_quiz-next-nav'));
+        $output .= html_writer::end_tag('div');
+
+        return $output;
+    }
+
     /**
      * Render a button which allows students to redo a question in the attempt.
      *
@@ -762,9 +796,8 @@ class mod_quiz_renderer extends plugin_renderer_base {
 
         if ($viewobj->buttontext) {
             $output .= $this->start_attempt_button($viewobj->buttontext,
-                    $viewobj->startattempturl, $viewobj->startattemptwarning,
+                    $viewobj->startattempturl, $viewobj->preflightcheckform,
                     $viewobj->popuprequired, $viewobj->popupoptions);
-
         }
 
         if ($viewobj->showbacktocourse) {
@@ -779,43 +812,44 @@ class mod_quiz_renderer extends plugin_renderer_base {
     /**
      * Generates the view attempt button
      *
-     * @param int $course The course ID
-     * @param array $quiz Array containging quiz date
-     * @param int $cm The Course Module ID
-     * @param int $context The page Context ID
-     * @param mod_quiz_view_object $viewobj
-     * @param string $buttontext
+     * @param string $buttontext the label to display on the button.
+     * @param moodle_url $url The URL to POST to in order to start the attempt.
+     * @param mod_quiz_preflight_check_form $preflightcheckform deprecated.
+     * @param bool $popuprequired whether the attempt needs to be opened in a pop-up.
+     * @param array $popupoptions the options to use if we are opening a popup.
+     * @return string HTML fragment.
      */
     public function start_attempt_button($buttontext, moodle_url $url,
-            $startattemptwarning, $popuprequired, $popupoptions) {
+            mod_quiz_preflight_check_form $preflightcheckform = null,
+            $popuprequired = false, $popupoptions = null) {
+
+        if (is_string($preflightcheckform)) {
+            // Calling code was not updated since the API change.
+            debugging('The third argument to start_attempt_button should now be the ' .
+                    'mod_quiz_preflight_check_form from ' .
+                    'quiz_access_manager::get_preflight_check_form, not a warning message string.');
+        }
 
         $button = new single_button($url, $buttontext);
         $button->class .= ' quizstartbuttondiv';
 
-        $warning = '';
-        if ($popuprequired) {
-            $this->page->requires->js_module(quiz_get_js_module());
-            $this->page->requires->js('/mod/quiz/module.js');
-            $popupaction = new popup_action('click', $url, 'quizpopup', $popupoptions);
-
-            $button->class .= ' quizsecuremoderequired';
-            $button->add_action(new component_action('click',
-                    'M.mod_quiz.secure_window.start_attempt_action', array(
-                        'url' => $url->out(false),
-                        'windowname' => 'quizpopup',
-                        'options' => $popupaction->get_js_options(),
-                        'fullscreen' => true,
-                        'startattemptwarning' => $startattemptwarning,
-                    )));
-
-            $warning = html_writer::tag('noscript', $this->heading(get_string('noscript', 'quiz')));
-
-        } else if ($startattemptwarning) {
-            $button->add_action(new confirm_action($startattemptwarning, null,
-                    get_string('startattempt', 'quiz')));
+        $popupjsoptions = null;
+        if ($popuprequired && $popupoptions) {
+            $action = new popup_action('click', $url, 'popup', $popupoptions);
+            $popupjsoptions = $action->get_js_options();
         }
 
-        return $this->render($button) . $warning;
+        if ($preflightcheckform) {
+            $checkform = $preflightcheckform->render();
+        } else {
+            $checkform = null;
+        }
+
+        $this->page->requires->js_call_amd('mod_quiz/preflightcheck', 'init',
+                array('.quizstartbuttondiv input[type=submit]', get_string('startattempt', 'quiz'),
+                       '#mod_quiz_preflight_form', $popupjsoptions));
+
+        return $this->render($button) . $checkform;
     }
 
     /**
@@ -1280,10 +1314,11 @@ class mod_quiz_view_object {
     /** @var string $buttontext caption for the start attempt button. If this is null, show no
      *      button, or if it is '' show a back to the course button. */
     public $buttontext;
-    /** @var string $startattemptwarning alert to show the user before starting an attempt. */
-    public $startattemptwarning;
     /** @var moodle_url $startattempturl URL to start an attempt. */
     public $startattempturl;
+    /** @var moodleform|null $preflightcheckform confirmation form that must be
+     *       submitted before an attempt is started, if required. */
+    public $preflightcheckform;
     /** @var moodle_url $startattempturl URL for any Back to the course button. */
     public $backtocourseurl;
     /** @var bool $showbacktocourse should we show a back to the course button? */
@@ -1294,4 +1329,16 @@ class mod_quiz_view_object {
     public $popupoptions;
     /** @var bool $quizhasquestions whether the quiz has any questions. */
     public $quizhasquestions;
+
+    public function __get($field) {
+        switch ($field) {
+            case 'startattemptwarning':
+                debugging('startattemptwarning has been deprecated. It is now always blank.');
+                return '';
+
+            default:
+                debugging('Unknown property ' . $field);
+                return null;
+        }
+    }
 }
index eebab6b..be91bd6 100644 (file)
@@ -134,10 +134,10 @@ $summarydata = array();
 if (!$attemptobj->get_quiz()->showuserpicture && $attemptobj->get_userid() != $USER->id) {
     // If showuserpicture is true, the picture is shown elsewhere, so don't repeat it.
     $student = $DB->get_record('user', array('id' => $attemptobj->get_userid()));
-    $usrepicture = new user_picture($student);
-    $usrepicture->courseid = $attemptobj->get_courseid();
+    $userpicture = new user_picture($student);
+    $userpicture->courseid = $attemptobj->get_courseid();
     $summarydata['user'] = array(
-        'title'   => $usrepicture,
+        'title'   => $userpicture,
         'content' => new action_link(new moodle_url('/user/view.php', array(
                                 'id' => $student->id, 'course' => $attemptobj->get_courseid())),
                           fullname($student, true)),
index e7bcf91..7a6e2a1 100644 (file)
@@ -44,11 +44,15 @@ $attemptobj = quiz_attempt::create($attemptid);
 // Check login.
 require_login($attemptobj->get_course(), false, $attemptobj->get_cm());
 $attemptobj->check_review_capability();
+$student = $DB->get_record('user', array('id' => $attemptobj->get_userid()));
 
 $accessmanager = $attemptobj->get_access_manager(time());
 $options = $attemptobj->get_display_options(true);
 
 $PAGE->set_pagelayout('popup');
+$PAGE->set_title(get_string('reviewofquestion', 'quiz', array(
+        'question' => format_string($attemptobj->get_question_name($slot)),
+        'quiz' => format_string($attemptobj->get_quiz_name()), 'user' => fullname($student))));
 $PAGE->set_heading($attemptobj->get_course()->fullname);
 $output = $PAGE->get_renderer('mod_quiz');
 
@@ -56,10 +60,10 @@ $output = $PAGE->get_renderer('mod_quiz');
 // quiz_attempt::check_file_access. If you change on, change them all.
 if ($attemptobj->is_own_attempt()) {
     if (!$attemptobj->is_finished()) {
-        echo $output->review_question_not_allowed(get_string('cannotreviewopen', 'quiz'));
+        echo $output->review_question_not_allowed($attemptobj, get_string('cannotreviewopen', 'quiz'));
         die();
     } else if (!$options->attempt) {
-        echo $output->review_question_not_allowed(
+        echo $output->review_question_not_allowed($attemptobj,
                 $attemptobj->cannot_review_message());
         die();
     }
@@ -71,6 +75,16 @@ if ($attemptobj->is_own_attempt()) {
 // Prepare summary informat about this question attempt.
 $summarydata = array();
 
+// Student name.
+$userpicture = new user_picture($student);
+$userpicture->courseid = $attemptobj->get_courseid();
+$summarydata['user'] = array(
+    'title'   => $userpicture,
+    'content' => new action_link(new moodle_url('/user/view.php', array(
+            'id' => $student->id, 'course' => $attemptobj->get_courseid())),
+            fullname($student, true)),
+);
+
 // Quiz name.
 $summarydata['quizname'] = array(
     'title'   => get_string('modulename', 'quiz'),
index ae60ae6..9b625ec 100644 (file)
@@ -125,8 +125,8 @@ if ($lastattempt && ($lastattempt->state == quiz_attempt::IN_PROGRESS ||
 }
 
 // Check access.
-$output = $PAGE->get_renderer('mod_quiz');
 if (!$quizobj->is_preview_user() && $messages) {
+    $output = $PAGE->get_renderer('mod_quiz');
     print_error('attempterror', 'quiz', $quizobj->view_url(),
             $output->access_messages($messages));
 }
@@ -137,7 +137,7 @@ if ($accessmanager->is_preflight_check_required($currentattemptid)) {
             $quizobj->start_attempt_url($page), $currentattemptid);
 
     if ($mform->is_cancelled()) {
-        $accessmanager->back_to_view_page($output);
+        $accessmanager->back_to_view_page($PAGE->get_renderer('mod_quiz'));
 
     } else if (!$mform->get_data()) {
 
@@ -145,6 +145,7 @@ if ($accessmanager->is_preflight_check_required($currentattemptid)) {
         $PAGE->set_url($quizobj->start_attempt_url($page));
         $PAGE->set_title($quizobj->get_quiz_name());
         $accessmanager->setup_attempt_page($PAGE);
+        $output = $PAGE->get_renderer('mod_quiz');
         if (empty($quizobj->get_quiz()->showblocks)) {
             $PAGE->blocks->show_only_fake_blocks();
         }
index c648195..f98ea29 100644 (file)
     text-align: left;
     padding-top: 1.5em;
 }
-
 #page-mod-quiz-attempt.dir-rtl .submitbtns,
 #page-mod-quiz-review.dir-rtl .submitbtns {
     text-align: right;
 }
 
+#page-mod-quiz-attempt .submitbtns .mod_quiz-next-nav,
+#page-mod-quiz-review .submitbtns .mod_quiz-next-nav {
+    float: right;
+}
+#page-mod-quiz-attempt.dir-rtl .submitbtns .mod_quiz-next-nav,
+#page-mod-quiz-review.dir-rtl .submitbtns .mod_quiz-next-nav {
+    float: left;
+}
+
 .path-mod-quiz .mod_quiz-redo_question_button {
     margin: 0;
 }
@@ -337,6 +345,47 @@ table.quizattemptsummary .noreviewmessage {
 .jsenabled .quizstartbuttondiv.quizsecuremoderequired input {
     display: inline;
 }
+.quizattempt #mod_quiz_preflight_form {
+    display: none;
+}
+
+#mod_quiz_preflight_form .femptylabel .fitemtitle {
+    display: none;
+}
+#mod_quiz_preflight_form .femptylabel .felement {
+    margin: 0;
+    padding: 0;
+}
+
+.moodle-dialogue-base .moodle-dialogue.mod_quiz_preflight_popup {
+    width: 600px;
+}
+.moodle-dialogue-base .moodle-dialogue.mod_quiz_preflight_popup .moodle-dialogue-wrap {
+    overflow: hidden;
+}
+.moodle-dialogue-base .moodle-dialogue.mod_quiz_preflight_popup .moodle-dialogue-bd {
+    padding: 0;
+}
+.moodle-dialogue-base .moodle-dialogue.mod_quiz_preflight_popup .moodle-dialogue-bd #mod_quiz_preflight_form legend {
+    padding: 0 10px;
+    margin: 0;
+    border: 0 none;
+}
+.moodle-dialogue-base .moodle-dialogue.mod_quiz_preflight_popup .moodle-dialogue-bd #mod_quiz_preflight_form .fitem {
+    margin-left: 10px;
+}
+.moodle-dialogue-base .moodle-dialogue.mod_quiz_preflight_popup .moodle-dialogue-bd #mod_quiz_preflight_form #fgroup_id_buttonar {
+    padding: 10px 0 0;
+    margin: 0;
+}
+.moodle-dialogue-base .moodle-dialogue.mod_quiz_preflight_popup .moodle-dialogue-content .moodle-dialogue-ft {
+    margin: 0;
+}
+/* Standard Moodle rule that needs to be more specific here. */
+.moodle-dialogue-bd #mod_quiz_preflight_form fieldset.hidden {
+    display: inherit;
+    visibility: inherit;
+}
 
 body.path-mod-quiz .gradedattempt,
 body.path-mod-quiz table tbody tr.gradedattempt > td {
index f54b30e..b9f32fa 100644 (file)
@@ -37,7 +37,7 @@ Feature: Add a quiz
     Then I should see "Question 1"
     And I should see "Answer the first question"
     And I set the field "True" to "1"
-    And I press "Next"
+    And I press "Finish attempt ..."
     And I should see "Answer saved"
     And I press "Submit all and finish"
 
index 5d28ca5..f8ff46b 100644 (file)
@@ -37,7 +37,7 @@ Feature: Attemp a quiz where some questions require that the previous question h
     And I press "Attempt quiz now"
     And I click on "True" "radio" in the "First question" "question"
     And I click on "False" "radio" in the "Second question" "question"
-    And I press "Next"
+    And I press "Finish attempt ..."
     And I press "Submit all and finish"
     And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
     Then I should see "25.00 out of 100.00"
@@ -99,3 +99,36 @@ Feature: Attemp a quiz where some questions require that the previous question h
     And I should see question "4" in section "Section 2" in the quiz navigation
     And I should see question "5" in section "Section 3" in the quiz navigation
     And I should see question "6" in section "Section 3" in the quiz navigation
+
+  @javascript
+  Scenario: Next and previous navigation
+    Given the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext                |
+      | Test questions   | truefalse   | TF1   | Text of the first question  |
+      | Test questions   | truefalse   | TF2   | Text of the second question |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page |
+      | TF1      | 1    |
+      | TF2      | 2    |
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    Then I should see "Text of the first question"
+    And I press "Next page"
+    And I should see "Text of the second question"
+    And I click on "Finish attempt ..." "button" in the "region-main" "region"
+    And I should see "Summary of attempt"
+    And I press "Return to attempt"
+    And I should see "Text of the second question"
+    And I press "Previous page"
+    And I should see "Text of the first question"
+    And I follow "Finish attempt ..."
+    And I press "Submit all and finish"
+    And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
+    And I follow "Show one page at a time"
+    And I should see "Text of the first question"
+    And I follow "Next page"
+    And I should see "Text of the second question"
+    And I follow "Previous page"
+    And I should see "Text of the first question"
diff --git a/mod/quiz/tests/behat/attempt_begin.feature b/mod/quiz/tests/behat/attempt_begin.feature
new file mode 100644 (file)
index 0000000..a2987a8
--- /dev/null
@@ -0,0 +1,115 @@
+@mod @mod_quiz
+Feature: The various checks that may happen when an attept is started
+  As a student
+  In order to start a quiz with confidence
+  I need to be waned if there is a time limit, or various similar things
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email               |
+      | student  | Student   | One      | student@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role    |
+      | student  | C1     | student |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext               |
+      | Test questions   | truefalse   | TF1   | Text of the first question |
+
+  @javascript
+  Scenario: Start a quiz with no time limit
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page |
+      | TF1      | 1    |
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    Then I should see "Text of the first question"
+
+  @javascript
+  Scenario: Start a quiz with time limit and password
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | timelimit | quizpassword |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | 3600      | Frog         |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page |
+      | TF1      | 1    |
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    Then I should see "To attempt this quiz you need to know the quiz password" in the "Start attempt" "dialogue"
+    And I should see "The quiz has a time limit of 1 hour. Time will " in the "Start attempt" "dialogue"
+    And I set the field "Quiz password" to "Frog"
+    And I click on "Start attempt" "button" in the "Start attempt" "dialogue"
+    And I should see "Text of the first question"
+
+  @javascript
+  Scenario: Cancel starting a quiz with time limit and password
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | timelimit | quizpassword |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | 3600      | Frog         |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page |
+      | TF1      | 1    |
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I click on "Cancel" "button" in the "Start attempt" "dialogue"
+    Then I should see "Quiz 1 description"
+    And "Attempt quiz now" "button" should be visible
+
+  @javascript
+  Scenario: Start a quiz with time limit and password, get the password wrong first time
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | timelimit | quizpassword |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | 3600      | Frog         |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page |
+      | TF1      | 1    |
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I set the field "Quiz password" to "Toad"
+    And I click on "Start attempt" "button" in the "Start attempt" "dialogue"
+    Then I should see "Quiz 1 description"
+    And I should see "To attempt this quiz you need to know the quiz password"
+    And I should see "The quiz has a time limit of 1 hour. Time will "
+    And I should see "The password entered was incorrect"
+    And I set the field "Quiz password" to "Frog"
+    And I press "Start attempt"
+    And I should see "Text of the first question"
+
+  @javascript
+  Scenario: Start a quiz with time limit and password, get the password wrong first time then cancel
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | timelimit | quizpassword |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | 3600      | Frog         |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page |
+      | TF1      | 1    |
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I set the field "Quiz password" to "Toad"
+    And I click on "Start attempt" "button" in the "Start attempt" "dialogue"
+    And I should see "Quiz 1 description"
+    And I should see "To attempt this quiz you need to know the quiz password"
+    And I should see "The quiz has a time limit of 1 hour. Time will "
+    And I should see "The password entered was incorrect"
+    And I set the field "Quiz password" to "Frog"
+    And I press "Cancel"
+    Then I should see "Quiz 1 description"
+    And "Attempt quiz now" "button" should be visible
index ce6cf09..2760fa1 100644 (file)
@@ -63,7 +63,7 @@ Feature: Allow students to redo questions in a practice quiz, without starting a
     And I press "Attempt quiz now"
     And I click on "False" "radio" in the "First question" "question"
     And I click on "Check" "button" in the "First question" "question"
-    And I press "Next"
+    And I press "Finish attempt ..."
     And I press "Submit all and finish"
     And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
     Then "Redo question" "button" should not exist
@@ -75,7 +75,7 @@ Feature: Allow students to redo questions in a practice quiz, without starting a
     And I click on "False" "radio" in the "First question" "question"
     And I click on "Check" "button" in the "First question" "question"
     And I press "Redo question"
-    And I press "Next"
+    And I press "Finish attempt ..."
     And I press "Submit all and finish"
     And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
     And I log out
index 5470f8b..e5b37f1 100644 (file)
@@ -99,7 +99,7 @@ Feature: Attemp a quiz where some questions require that the previous question h
     And I follow "Course 1"
     And I follow "Quiz 1"
     And I press "Attempt quiz now"
-    And I press "Next"
+    And I press "Finish attempt ..."
     And I press "Submit all and finish"
     And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
 
@@ -177,7 +177,7 @@ Feature: Attemp a quiz where some questions require that the previous question h
     And I follow "Course 1"
     And I follow "Quiz 1"
     And I press "Attempt quiz now"
-    And I press "Next"
+    And I press "Next page"
 
     Then I should see "Second question"
     And I should not see "This question cannot be attempted until the previous question has been completed."
index 330606d..c364400 100644 (file)
@@ -38,14 +38,14 @@ Feature: Set a quiz to be marked complete when the student uses all attempts all
     And I follow "Test quiz name"
     And I press "Attempt quiz now"
     And I set the field "False" to "1"
-    And I press "Next"
+    And I press "Finish attempt ..."
     And I press "Submit all and finish"
     And I follow "C1"
     And the "Test quiz name" "quiz" activity with "auto" completion should be marked as not complete
     And I follow "Test quiz name"
     And I press "Re-attempt quiz"
     And I set the field "False" to "1"
-    And I press "Next"
+    And I press "Finish attempt ..."
     And I press "Submit all and finish"
     And I follow "C1"
     Then "//img[contains(@alt, 'Completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element"
index 3312cca..b47457e 100644 (file)
@@ -38,7 +38,7 @@ Feature: Set a quiz to be marked complete when the student passes
     And I follow "Test quiz name"
     And I press "Attempt quiz now"
     And I set the field "True" to "1"
-    And I press "Next"
+    And I press "Finish attempt ..."
     And I press "Submit all and finish"
     And I follow "C1"
     Then "//img[contains(@alt, 'Completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element"
index c591eba..3953efd 100644 (file)
@@ -39,7 +39,7 @@ Feature: Quiz reset
     And I follow "Test quiz name"
     And I press "Attempt quiz now"
     And I set the field "True" to "1"
-    And I press "Next"
+    And I press "Finish attempt ..."
     And I press "Submit all and finish"
     And I log out
     And I log in as "teacher1"
index 78bee87..d3828d1 100644 (file)
@@ -1,9 +1,18 @@
 This files describes API changes in the quiz code.
 
 === 3.1 ===
+
 * quiz_attempt::question_print_comment_fields() has been removed. It was broken
   since at least Moodle 2.0.
 
+* quiz::confirm_start_attempt_message and mod_quiz_view_object::$startattemptwarning
+  have been deprecated. This functionality is now entirely handled within the
+  quiz access rule plugins.
+
+* The third argument to mod_quiz_renderer::start_attempt_button has been changed
+  from a warning string to a mod_quiz_preflight_check_form.
+
+
 === 2.9 ===
 
 * There have been changes in classes/output/edit_renderer.php for MDL-40990.
@@ -100,6 +109,7 @@ This files describes API changes in the quiz code.
   trigger the event outside this function. Note that the appropriate start event is
   fired automatically by the quiz_attempt_save_started function.
 
+
 === 2.7 ===
 
 * The old quiz.questions database column (comma-separated list of question ids)
@@ -130,6 +140,7 @@ This files describes API changes in the quiz code.
       quiz_delete_empty_page: has had its arguments changed to $quiz and $pagenumber.
       quiz_has_question_use: now takes $quiz and $slot, not $questionid.
 
+
 === 2.6 ===
 
 * As part of improving the page usability and accessibility, we updated the
index a99f825..db0c149 100644 (file)
@@ -93,6 +93,7 @@ $viewobj->canreviewmine = $canreviewmine;
 $attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true);
 $lastfinishedattempt = end($attempts);
 $unfinished = false;
+$unfinishedattemptid = null;
 if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
     $attempts[] = $unfinishedattempt;
 
@@ -105,6 +106,7 @@ if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id))
     if (!$unfinished) {
         $lastfinishedattempt = $unfinishedattempt;
     }
+    $unfinishedattemptid = $unfinishedattempt->id;
     $unfinishedattempt = null; // To make it clear we do not use this again.
 }
 $numattempts = count($attempts);
@@ -153,7 +155,7 @@ $output = $PAGE->get_renderer('mod_quiz');
 // Print table with existing attempts.
 if ($attempts) {
     // Work out which columns we need, taking account what data is available in each attempt.
-    list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts, $context);
+    list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
 
     $viewobj->attemptcolumn  = $quiz->attempts != 1;
 
@@ -177,7 +179,11 @@ $viewobj->canedit = has_capability('mod/quiz:manage', $context);
 $viewobj->editurl = new moodle_url('/mod/quiz/edit.php', array('cmid' => $cm->id));
 $viewobj->backtocourseurl = new moodle_url('/course/view.php', array('id' => $course->id));
 $viewobj->startattempturl = $quizobj->start_attempt_url();
-$viewobj->startattemptwarning = $quizobj->confirm_start_attempt_message($unfinished);
+
+if ($accessmanager->is_preflight_check_required($unfinishedattemptid)) {
+    $viewobj->preflightcheckform = $accessmanager->get_preflight_check_form(
+            $viewobj->startattempturl, $unfinishedattemptid);
+}
 $viewobj->popuprequired = $accessmanager->attempt_must_be_in_popup();
 $viewobj->popupoptions = $accessmanager->get_popup_options();
 
index 2ccf618..17c662e 100644 (file)
@@ -1444,7 +1444,7 @@ function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='n
 
     if (!empty($organizationsco)) {
         $result[0] = $organizationsco;
-        $result[0]->isvisible = true;
+        $result[0]->isvisible = 'true';
         $result[0]->statusicon = '';
         $result[0]->url = '';
     }
@@ -1464,7 +1464,7 @@ function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='n
         }
         foreach ($scoes as $sco) {
             if (!isset($sco->isvisible)) {
-                $sco->isvisible = true;
+                $sco->isvisible = 'true';
             }
 
             if (empty($sco->title)) {
@@ -1477,7 +1477,7 @@ function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='n
                 $sco->prereq = empty($sco->prerequisites) || scorm_eval_prerequisites($sco->prerequisites, $usertracks);
             }
 
-            if ($sco->isvisible) {
+            if ($sco->isvisible === 'true') {
                 if (!empty($sco->launch)) {
                     if (empty($scoid) && ($mode != 'normal')) {
                         $scoid = $sco->id;
@@ -1667,88 +1667,86 @@ function scorm_format_toc_for_treeview($user, $scorm, $scoes, $usertracks, $cmid
     $prevsco = '';
     if (!empty($scoes)) {
         foreach ($scoes as $sco) {
+
+            if ($sco->isvisible === 'false') {
+                continue;
+            }
+
             $result->toc .= html_writer::start_tag('li');
             $scoid = $sco->id;
 
-            $sco->isvisible = true;
-
-            if ($sco->isvisible) {
-                $score = '';
+            $score = '';
 
-                if (isset($usertracks[$sco->identifier])) {
-                    $viewscore = has_capability('mod/scorm:viewscores', context_module::instance($cmid));
-                    if (isset($usertracks[$sco->identifier]->score_raw) && $viewscore) {
-                        if ($usertracks[$sco->identifier]->score_raw != '') {
-                            $score = '('.get_string('score', 'scorm').':&nbsp;'.$usertracks[$sco->identifier]->score_raw.')';
-                        }
+            if (isset($usertracks[$sco->identifier])) {
+                $viewscore = has_capability('mod/scorm:viewscores', context_module::instance($cmid));
+                if (isset($usertracks[$sco->identifier]->score_raw) && $viewscore) {
+                    if ($usertracks[$sco->identifier]->score_raw != '') {
+                        $score = '('.get_string('score', 'scorm').':&nbsp;'.$usertracks[$sco->identifier]->score_raw.')';
                     }
                 }
+            }
 
-                if (!empty($sco->prereq)) {
-                    if ($sco->id == $scoid) {
-                        $result->prerequisites = true;
-                    }
+            if (!empty($sco->prereq)) {
+                if ($sco->id == $scoid) {
+                    $result->prerequisites = true;
+                }
 
-                    if (!empty($prevsco) && scorm_version_check($scorm->version, SCORM_13) && !empty($prevsco->hidecontinue)) {
+                if (!empty($prevsco) && scorm_version_check($scorm->version, SCORM_13) && !empty($prevsco->hidecontinue)) {
+                    if ($sco->scormtype == 'sco') {
+                        $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
+                    } else {
+                        $result->toc .= html_writer::span('&nbsp;'.format_string($sco->title));
+                    }
+                } else if ($toclink == TOCFULLURL) {
+                    $url = $CFG->wwwroot.'/mod/scorm/player.php?'.$sco->url;
+                    if (!empty($sco->launch)) {
                         if ($sco->scormtype == 'sco') {
-                            $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
+                            $result->toc .= $sco->statusicon.'&nbsp;';
+                            $result->toc .= html_writer::link($url, format_string($sco->title)).$score;
                         } else {
-                            $result->toc .= html_writer::span('&nbsp;'.format_string($sco->title));
-                        }
-                    } else if ($toclink == TOCFULLURL) {
-                        $url = $CFG->wwwroot.'/mod/scorm/player.php?'.$sco->url;
-                        if (!empty($sco->launch)) {
-                            if ($sco->scormtype == 'sco') {
-                                $result->toc .= $sco->statusicon.'&nbsp;';
-                                $result->toc .= html_writer::link($url, format_string($sco->title)).$score;
-                            } else {
-                                $result->toc .= '&nbsp;'.html_writer::link($url, format_string($sco->title),
-                                                                            array('data-scoid' => $sco->id)).$score;
-                            }
-                        } else {
-                            if ($sco->scormtype == 'sco') {
-                                $result->toc .= $sco->statusicon.'&nbsp;'.format_string($sco->title).$score;
-                            } else {
-                                $result->toc .= '&nbsp;'.format_string($sco->title).$score;
-                            }
+                            $result->toc .= '&nbsp;'.html_writer::link($url, format_string($sco->title),
+                                                                        array('data-scoid' => $sco->id)).$score;
                         }
                     } else {
-                        if (!empty($sco->launch)) {
-                            if ($sco->scormtype == 'sco') {
-                                $result->toc .= html_writer::tag('a', $sco->statusicon.'&nbsp;'.
-                                                                    format_string($sco->title).'&nbsp;'.$score,
-                                                                    array('data-scoid' => $sco->id, 'title' => $sco->url));
-                            } else {
-                                $result->toc .= html_writer::tag('a', '&nbsp;'.format_string($sco->title).'&nbsp;'.$score,
-                                                                    array('data-scoid' => $sco->id, 'title' => $sco->url));
-                            }
+                        if ($sco->scormtype == 'sco') {
+                            $result->toc .= $sco->statusicon.'&nbsp;'.format_string($sco->title).$score;
                         } else {
-                            if ($sco->scormtype == 'sco') {
-                                $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
-                            } else {
-                                $result->toc .= html_writer::span('&nbsp;'.format_string($sco->title));
-                            }
+                            $result->toc .= '&nbsp;'.format_string($sco->title).$score;
                         }
                     }
-
                 } else {
-                    if ($play) {
+                    if (!empty($sco->launch)) {
                         if ($sco->scormtype == 'sco') {
-                            $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
+                            $result->toc .= html_writer::tag('a', $sco->statusicon.'&nbsp;'.
+                                                                format_string($sco->title).'&nbsp;'.$score,
+                                                                array('data-scoid' => $sco->id, 'title' => $sco->url));
                         } else {
-                            $result->toc .= '&nbsp;'.format_string($sco->title).html_writer::end_span();
+                            $result->toc .= html_writer::tag('a', '&nbsp;'.format_string($sco->title).'&nbsp;'.$score,
+                                                                array('data-scoid' => $sco->id, 'title' => $sco->url));
                         }
                     } else {
                         if ($sco->scormtype == 'sco') {
-                            $result->toc .= $sco->statusicon.'&nbsp;'.format_string($sco->title);
+                            $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
                         } else {
-                            $result->toc .= '&nbsp;'.format_string($sco->title);
+                            $result->toc .= html_writer::span('&nbsp;'.format_string($sco->title));
                         }
                     }
                 }
 
             } else {
-                $result->toc .= "&nbsp;".format_string($sco->title);
+                if ($play) {
+                    if ($sco->scormtype == 'sco') {
+                        $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
+                    } else {
+                        $result->toc .= '&nbsp;'.format_string($sco->title).html_writer::end_span();
+                    }
+                } else {
+                    if ($sco->scormtype == 'sco') {
+                        $result->toc .= $sco->statusicon.'&nbsp;'.format_string($sco->title);
+                    } else {
+                        $result->toc .= '&nbsp;'.format_string($sco->title);
+                    }
+                }
             }
 
             if (!empty($sco->children)) {
@@ -2169,7 +2167,7 @@ function scorm_get_sco_and_launch_url($scorm, $scoid, $context) {
         // This SCORM content sits in a repository that allows relative links.
         $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/imsmanifest/$scorm->revision/$launcher";
     } else if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
-        // Note: do not convert this to use get_file_url() or moodle_url()
+        // Note: do not convert this to use moodle_url().
         // SCORM does not work without slasharguments and moodle_url() encodes querystring vars.
         $scolaunchurl = "$CFG->wwwr