Merge branch 'master_MDL-46816' of https://github.com/danmarsden/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 25 Aug 2014 22:09:51 +0000 (00:09 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 25 Aug 2014 22:09:51 +0000 (00:09 +0200)
59 files changed:
admin/tool/generator/classes/course_backend.php
admin/tool/generator/lang/en/tool_generator.php
availability/classes/info.php
backup/util/ui/tests/behat/behat_backup.php
grade/report/grader/lib.php
grade/report/grader/styles.css
grade/report/history/classes/event/grade_report_viewed.php [new file with mode: 0644]
grade/report/history/classes/filter_form.php [new file with mode: 0644]
grade/report/history/classes/helper.php [new file with mode: 0644]
grade/report/history/classes/output/renderer.php [new file with mode: 0644]
grade/report/history/classes/output/tablelog.php [new file with mode: 0644]
grade/report/history/classes/output/user_button.php [new file with mode: 0644]
grade/report/history/db/access.php [new file with mode: 0644]
grade/report/history/index.php [new file with mode: 0644]
grade/report/history/lang/en/gradereport_history.php [new file with mode: 0644]
grade/report/history/settings.php [new file with mode: 0644]
grade/report/history/styles.css [new file with mode: 0644]
grade/report/history/tests/report_test.php [new file with mode: 0644]
grade/report/history/users_ajax.php [new file with mode: 0644]
grade/report/history/version.php [new file with mode: 0644]
grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-debug.js [new file with mode: 0644]
grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-min.js [new file with mode: 0644]
grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector.js [new file with mode: 0644]
grade/report/history/yui/src/userselector/build.json [new file with mode: 0644]
grade/report/history/yui/src/userselector/js/userselector.js [new file with mode: 0644]
grade/report/history/yui/src/userselector/meta/userselector.json [new file with mode: 0644]
grade/report/user/lib.php
grade/report/user/styles.css
lib/classes/plugin_manager.php
lib/classes/task/manager.php
lib/datalib.php
lib/ddl/database_manager.php
lib/ddl/tests/ddl_test.php
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-debug.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-min.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button.js
lib/editor/atto/plugins/html/yui/src/button/js/button.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/editor.js
lib/tests/behat/behat_general.php
lib/tests/datalib_test.php
lib/tests/scheduled_task_test.php
mod/assign/submission/file/locallib.php
mod/data/styles.css
mod/data/templates.php
mod/forum/classes/task/cron_task.php [new file with mode: 0644]
mod/forum/db/tasks.php [new file with mode: 0644]
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/version.php
question/tests/behat/behat_question.php
question/type/calculated/tests/variablesubstituter_test.php
theme/base/style/grade.css
theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap-debug.js
theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap-min.js
theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap.js
theme/bootstrapbase/yui/src/bootstrap/js/bootstrap.js

index bd2cf9f..554530e 100644 (file)
@@ -36,6 +36,10 @@ class tool_generator_course_backend extends tool_generator_backend {
      * @var array Number of sections in course
      */
     private static $paramsections = array(1, 10, 100, 500, 1000, 2000);
+    /**
+     * @var array Number of assignments in course
+     */
+    private static $paramassignments = array(1, 10, 100, 500, 1000, 2000);
     /**
      * @var array Number of Page activities in course
      */
@@ -179,6 +183,7 @@ class tool_generator_course_backend extends tool_generator_backend {
         // Make course.
         $this->course = $this->create_course();
         $this->create_users();
+        $this->create_assignments();
         $this->create_pages();
         $this->create_small_files();
         $this->create_big_files();
@@ -318,6 +323,26 @@ class tool_generator_course_backend extends tool_generator_backend {
         $this->end_log();
     }
 
+    /**
+     * Creates a number of Assignment activities.
+     */
+    private function create_assignments() {
+        // Set up generator.
+        $assigngenerator = $this->generator->get_plugin_generator('mod_assign');
+
+        // Create assignments.
+        $number = self::$paramassignments[$this->size];
+        $this->log('createassignments', $number, true);
+        for ($i = 0; $i < $number; $i++) {
+            $record = array('course' => $this->course);
+            $options = array('section' => $this->get_target_section());
+            $assigngenerator->create_instance($record, $options);
+            $this->dot($i, $number);
+        }
+
+        $this->end_log();
+    }
+
     /**
      * Creates a number of Page activities.
      */
index 83964c8..edaab03 100644 (file)
@@ -71,6 +71,7 @@ $string['pluginname'] = 'Development data generator';
 $string['progress_checkaccounts'] = 'Checking user accounts ({$a})';
 $string['progress_coursecompleted'] = 'Course completed ({$a}s)';
 $string['progress_createaccounts'] = 'Creating user accounts ({$a->from} - {$a->to})';
+$string['progress_createassignments'] = 'Creating assignments ({$a})';
 $string['progress_createbigfiles'] = 'Creating big files ({$a})';
 $string['progress_createcourse'] = 'Creating course {$a}';
 $string['progress_createforum'] = 'Creating forum ({$a} posts)';
index 884151b..b7c6d3d 100644 (file)
@@ -613,7 +613,12 @@ abstract class info {
         $info = preg_replace_callback('~<AVAILABILITY_CMNAME_([0-9]+)/>~',
                 function($matches) use($modinfo, $context) {
                     $cm = $modinfo->get_cm($matches[1]);
-                    return format_string($cm->name, true, array('context' => $context));
+                    if ($cm->has_view() and $cm->uservisible) {
+                        // Help student by providing a link to the module which is preventing availability.
+                        return \html_writer::link($cm->url, format_string($cm->name, true, array('context' => $context)));
+                    } else {
+                        return format_string($cm->name, true, array('context' => $context));
+                    }
                 }, $info);
 
         return $info;
index 282a470..9a230ed 100644 (file)
@@ -27,6 +27,7 @@
 
 require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
 require_once(__DIR__ . '/../../../../../lib/behat/behat_field_manager.php');
+require_once(__DIR__ . '/../../../../../lib/tests/behat/behat_navigation.php');
 
 use Behat\Gherkin\Node\TableNode as TableNode,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
@@ -42,6 +43,19 @@ use Behat\Gherkin\Node\TableNode as TableNode,
  */
 class behat_backup extends behat_base {
 
+    /**
+     * Follow a link like 'Backup' or 'Import', where the link name comes from
+     * a language string, in the settings nav block of a course.
+     * @param string $langstring the lang string to look for. E.g. 'backup' or 'import'.
+     * @param string $component (optional) second argument to {@link get_string}.
+     */
+    protected function navigate_to_course_settings_link($langstring, $component = '') {
+        $behatnavigation = new behat_navigation();
+        $behatnavigation->setMink($this->getMink());
+        $behatnavigation->i_navigate_to_node_in(get_string($langstring, $component),
+                get_string('courseadministration'));
+    }
+
     /**
      * Backups the specified course using the provided options. If you are interested in restoring this backup would be useful to provide a 'Filename' option.
      *
@@ -60,7 +74,7 @@ class behat_backup extends behat_base {
         $this->find_link($backupcourse)->click();
 
         // Click the backup link.
-        $this->find_link(get_string('backup'))->click();
+        $this->navigate_to_course_settings_link('backup');
         $this->wait();
 
         // Initial settings.
@@ -110,7 +124,7 @@ class behat_backup extends behat_base {
         $this->wait();
 
         // Click the import link.
-        $this->find_link(get_string('import'))->click();
+        $this->navigate_to_course_settings_link('import');
         $this->wait();
 
         // Select the course.
index 948fb50..0c6e692 100644 (file)
@@ -939,7 +939,7 @@ class grade_report_grader extends grade_report {
 
                 $hidden = '';
                 if ($grade->is_hidden()) {
-                    $hidden = ' hidden ';
+                    $hidden = ' dimmed_text ';
                 }
 
                 $gradepass = ' gradefail ';
@@ -1284,7 +1284,7 @@ class grade_report_grader extends grade_report {
 
                 $hidden = '';
                 if ($item->is_hidden()) {
-                    $hidden = ' hidden ';
+                    $hidden = ' dimmed_text ';
                 }
 
                 $formattedrange = $item->get_formatted_range($rangesdisplaytype, $rangesdecimalpoints);
index f5e4296..fcf9f2e 100644 (file)
@@ -77,7 +77,7 @@ table#user-grades {
     background-color: transparent;
     border-style: solid;
     border-width: 1px;
-    margin: 20px 0 0;
+    margin: 0;
 }
 .path-grade-report-grader #overDiv table {
     margin: 0;
@@ -343,7 +343,6 @@ table#user-grades th.fixedcolumn {
 .path-grade-report-grader .left_scroller {
     float: left;
     clear: none;
-    padding-top: 22px;
 }
 .path-grade-report-grader.dir-rtl .left_scroller {
     float: right;
@@ -617,13 +616,6 @@ table#user-grades th.category {
 .path-grade-report-grader .gradeparent {
     border-top: 1px solid #cecece;
 }
-#page-grade-report-grader-index .topscrollcontent {
-    background-color: #cecece;
-    height: 1px;
-}
-#page-grade-report-grader-index #user-grades {
-    margin-top: 4px;
-}
 .path-grade-report-grader div.left_scroller tr,
 .path-grade-report-grader div.right_scroller tr,
 .path-grade-report-grader div.left_scroller td,
@@ -635,3 +627,18 @@ table#user-grades th.category {
 .path-grade-report-grader td.grade.overridden {
     line-height: 20px;
 }
+
+/** MDL-46812 **/
+#page-grade-report-grader-index.jsenabled .right_scroller.topscroll {
+    background-color: #ececec;
+    height: 20px;
+    margin: 0;
+    padding: 0;
+}
+#page-grade-report-grader-index.jsenabled .topscrollcontent {
+    background-color: #ececec;
+    height: 20px;
+}
+.jsenabled.path-grade-report-grader .left_scroller {
+    border-top: 20px solid #ececec;
+}
diff --git a/grade/report/history/classes/event/grade_report_viewed.php b/grade/report/history/classes/event/grade_report_viewed.php
new file mode 100644 (file)
index 0000000..c9a38c1
--- /dev/null
@@ -0,0 +1,47 @@
+<?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/>.
+
+/**
+ * Grade history report viewed event.
+ *
+ * @package    gradereport_history
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradereport_history\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grade history report viewed event class.
+ *
+ * @package    gradereport_history
+ * @since      Moodle 2.8
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class grade_report_viewed extends \core\event\grade_report_viewed {
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventgradereportviewed', 'gradereport_history');
+    }
+}
diff --git a/grade/report/history/classes/filter_form.php b/grade/report/history/classes/filter_form.php
new file mode 100644 (file)
index 0000000..0e51229
--- /dev/null
@@ -0,0 +1,94 @@
+<?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/>.
+
+/**
+ * Form for grade history filters
+ *
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradereport_history;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Form for grade history filters
+ *
+ * @since      Moodle 2.8
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class filter_form extends \moodleform {
+
+    /**
+     * Definition of the Mform for filters displayed in the report.
+     */
+    public function definition() {
+
+        $mform    = $this->_form;
+        $course   = $this->_customdata['course'];
+        $itemids  = $this->_customdata['itemids'];
+        $graders  = $this->_customdata['graders'];
+        $userbutton = $this->_customdata['userbutton'];
+        $names = \html_writer::span('', 'selectednames');
+
+        $mform->addElement('static', 'userselect', get_string('selectusers', 'gradereport_history'), $userbutton);
+        $mform->addElement('static', 'selectednames', get_string('selectedusers', 'gradereport_history'), $names);
+
+        $mform->addElement('select', 'itemid', get_string('gradeitem', 'grades'), $itemids);
+        $mform->setType('itemid', PARAM_INT);
+
+        $mform->addElement('select', 'grader', get_string('grader', 'gradereport_history'), $graders);
+        $mform->setType('grader', PARAM_INT);
+
+        $mform->addElement('date_selector', 'datefrom', get_string('datefrom', 'gradereport_history'), array('optional' => true));
+        $mform->addElement('date_selector', 'datetill', get_string('datetill', 'gradereport_history'), array('optional' => true));
+
+        $mform->addElement('checkbox', 'revisedonly', get_string('revisedonly', 'gradereport_history'));
+        $mform->addHelpButton('revisedonly', 'revisedonly', 'gradereport_history');
+
+        $mform->addElement('hidden', 'id', $course->id);
+        $mform->setType('id', PARAM_INT);
+
+        $mform->addElement('hidden', 'userids');
+        $mform->setType('userids', PARAM_SEQUENCE);
+
+        $mform->addElement('hidden', 'userfullnames');
+        $mform->setType('userfullnames', PARAM_TEXT);
+
+        // Add a submit button.
+        $mform->addElement('submit', 'submitbutton', get_string('submit'));
+    }
+
+    /**
+     * This method implements changes to the form that need to be made once the form data is set.
+     */
+    public function definition_after_data() {
+        $mform = $this->_form;
+
+        if ($userfullnames = $mform->getElementValue('userfullnames')) {
+            $mform->getElement('selectednames')->setValue(\html_writer::span($userfullnames, 'selectednames'));
+        }
+    }
+
+}
diff --git a/grade/report/history/classes/helper.php b/grade/report/history/classes/helper.php
new file mode 100644 (file)
index 0000000..b0a3ff9
--- /dev/null
@@ -0,0 +1,187 @@
+<?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/>.
+
+/**
+ * Helper class for gradehistory report.
+ *
+ * @package    gradereport_history
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradereport_history;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Helper class for gradehistory report.
+ *
+ * @since      Moodle 2.8
+ * @package    gradereport_history
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+
+    /**
+     * Initialise the js to handle the user selection {@link gradereport_history_user_button}.
+     *
+     * @param int $courseid       course id.
+     * @param array $currentusers List of currently selected users.
+     *
+     * @return output\user_button the user select button.
+     */
+    public static function init_js($courseid, array $currentusers = null) {
+        global $PAGE;
+
+        // Load the strings for js.
+        $PAGE->requires->strings_for_js(array(
+            'errajaxsearch',
+            'finishselectingusers',
+            'foundoneuser',
+            'foundnusers',
+            'loadmoreusers',
+            'selectusers',
+        ), 'gradereport_history');
+        $PAGE->requires->strings_for_js(array(
+            'loading'
+        ), 'admin');
+        $PAGE->requires->strings_for_js(array(
+            'noresults',
+            'search'
+        ), 'moodle');
+
+        $arguments = array(
+            'courseid'            => $courseid,
+            'ajaxurl'             => '/grade/report/history/users_ajax.php',
+            'url'                 => $PAGE->url->out(false),
+            'selectedUsers'       => $currentusers,
+        );
+
+        // Load the yui module.
+        $PAGE->requires->yui_module(
+            'moodle-gradereport_history-userselector',
+            'Y.M.gradereport_history.UserSelector.init',
+            array($arguments)
+        );
+    }
+
+    /**
+     * Retrieve a list of users.
+     *
+     * We're interested in anyone that had a grade history in this course. This api returns a list of such users based on various
+     * criteria passed.
+     *
+     * @param \context $context Context of the page where the results would be shown.
+     * @param string $search the text to search for (empty string = find all).
+     * @param int $page page number, defaults to 0.
+     * @param int $perpage Number of entries to display per page, defaults to 0.
+     *
+     * @return array list of users.
+     */
+    public static function get_users($context, $search = '', $page = 0, $perpage = 25) {
+        global $DB;
+
+        list($sql, $params) = self::get_users_sql_and_params($context, $search);
+        $limitfrom = $page * $perpage;
+        $limitto = $limitfrom + $perpage;
+        $users = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
+        return $users;
+    }
+
+    /**
+     * Get total number of users present for the given search criteria.
+     *
+     * @param \context $context Context of the page where the results would be shown.
+     * @param string $search the text to search for (empty string = find all).
+     *
+     * @return int number of users found.
+     */
+    public static function get_users_count($context, $search = '') {
+        global $DB;
+
+        list($sql, $params) = self::get_users_sql_and_params($context, $search, true);
+        return $DB->count_records_sql($sql, $params);
+
+    }
+
+    /**
+     * Get sql and params to use to get list of users.
+     *
+     * @param \context $context Context of the page where the results would be shown.
+     * @param string $search the text to search for (empty string = find all).
+     * @param bool $count setting this to true, returns an sql to get count only instead of the complete data records.
+     *
+     * @return array sql and params list
+     */
+    protected static function get_users_sql_and_params($context, $search = '', $count = false) {
+
+        // Fields we need from the user table.
+        $extrafields = get_extra_user_fields($context);
+        $params = array();
+        if (!empty($search)) {
+            list($filtersql, $params) = users_search_sql($search, 'u', true, $extrafields);
+            $filtersql .= ' AND ';
+        } else {
+            $filtersql = '';
+        }
+
+        $ufields = \user_picture::fields('u', $extrafields).',u.username';
+        if ($count) {
+            $select = "SELECT COUNT(DISTINCT u.id) ";
+            $orderby = "";
+        } else {
+            $select = "SELECT DISTINCT $ufields ";
+            $orderby = " ORDER BY u.lastname ASC, u.firstname ASC";
+        }
+        $sql = "$select
+                 FROM {user} u
+                 JOIN {grade_grades_history} ggh ON u.id = ggh.userid
+                 JOIN {grade_items} gi ON gi.id = ggh.itemid
+                WHERE $filtersql gi.courseid = :courseid";
+        $sql .= $orderby;
+        $params['courseid'] = $context->instanceid;
+
+        return array($sql, $params);
+    }
+
+    /**
+     * Get a list of graders.
+     *
+     * @param int $courseid Id of course for which we need to fetch graders.
+     *
+     * @return array list of graders.
+     */
+    public static function get_graders($courseid) {
+        global $DB;
+
+        $ufields = get_all_user_name_fields(true, 'u');
+        $sql = "SELECT u.id, $ufields
+                  FROM {user} u
+                  JOIN {grade_grades_history} ggh ON ggh.usermodified = u.id
+                  JOIN {grade_items} gi ON gi.id = ggh.itemid
+                 WHERE gi.courseid = :courseid
+              GROUP BY u.id, $ufields
+              ORDER BY u.lastname ASC, u.firstname ASC";
+
+        $graders = $DB->get_records_sql($sql, array('courseid' => $courseid));
+        $return = array(0 => get_string('allgraders', 'gradereport_history'));
+        foreach ($graders as $grader) {
+            $return[$grader->id] = fullname($grader);
+        }
+        return $return;
+    }
+}
diff --git a/grade/report/history/classes/output/renderer.php b/grade/report/history/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..384822a
--- /dev/null
@@ -0,0 +1,112 @@
+<?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/>.
+
+/**
+ * Renderer for history grade report.
+ *
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradereport_history\output;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Renderer for history grade report.
+ *
+ * @since      Moodle 2.8
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+    /**
+     * Render for the select user button.
+     *
+     * @param user_button $button instance of  gradereport_history_user_button to render
+     *
+     * @return string HTML to display
+     */
+    protected function render_user_button(user_button $button) {
+        $attributes = array('type'     => 'button',
+                            'class'    => 'selectortrigger',
+                            'value'    => $button->label,
+                            'disabled' => $button->disabled ? 'disabled' : null,
+                            'title'    => $button->tooltip);
+
+        if ($button->actions) {
+            $id = \html_writer::random_id('single_button');
+            $attributes['id'] = $id;
+            foreach ($button->actions as $action) {
+                $this->add_action_handler($action, $id);
+            }
+        }
+        // First the input element.
+        $output = \html_writer::empty_tag('input', $attributes);
+
+        // Then hidden fields.
+        $params = $button->url->params();
+        if ($button->method === 'post') {
+            $params['sesskey'] = sesskey();
+        }
+        foreach ($params as $var => $val) {
+            $output .= \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $var, 'value' => $val));
+        }
+
+        // Then div wrapper for xhtml strictness.
+        $output = \html_writer::tag('div', $output);
+
+        // Now the form itself around it.
+        if ($button->method === 'get') {
+            $url = $button->url->out_omit_querystring(true); // Url without params, the anchor part allowed.
+        } else {
+            $url = $button->url->out_omit_querystring();     // Url without params, the anchor part not allowed.
+        }
+        if ($url === '') {
+            $url = '#'; // There has to be always some action.
+        }
+        $attributes = array('method' => $button->method,
+                            'action' => $url,
+                            'id'     => $button->formid);
+        $output = \html_writer::tag('div', $output, $attributes);
+
+        // Finally one more wrapper with class.
+        return \html_writer::tag('div', $output, array('class' => $button->class));
+    }
+
+    /**
+     * Get the html for the table.
+     *
+     * @param tablelog $tablelog table object.
+     *
+     * @return string table html
+     */
+    protected function render_tablelog(tablelog $tablelog) {
+        $o = '';
+        ob_start();
+        $tablelog->out($tablelog->pagesize, false);
+        $o = ob_get_contents();
+        ob_end_clean();
+
+        return $o;
+    }
+
+}
diff --git a/grade/report/history/classes/output/tablelog.php b/grade/report/history/classes/output/tablelog.php
new file mode 100644 (file)
index 0000000..0132406
--- /dev/null
@@ -0,0 +1,433 @@
+<?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/>.
+
+/**
+ * Renderable class for gradehistory report.
+ *
+ * @package    gradereport_history
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradereport_history\output;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/tablelib.php');
+
+/**
+ * Renderable class for gradehistory report.
+ *
+ * @since      Moodle 2.8
+ * @package    gradereport_history
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tablelog extends \table_sql implements \renderable {
+
+    /**
+     * @var int course id.
+     */
+    protected $courseid;
+
+    /**
+     * @var \context context of the page to be rendered.
+     */
+    protected $context;
+
+    /**
+     * @var \stdClass A list of filters to be applied to the sql query.
+     */
+    protected $filters;
+
+    /**
+     * @var array A list of grade items present in the course.
+     */
+    protected $gradeitems = array();
+
+    /**
+     * @var \course_modinfo|null A list of cm instances in course.
+     */
+    protected $cms;
+
+    /**
+     * Sets up the table_log parameters.
+     *
+     * @param string $uniqueid unique id of table.
+     * @param \context_course $context Context of the report.
+     * @param \moodle_url $url url of the page where this table would be displayed.
+     * @param array $filters options are:
+     *                          userids : limit to specific users (default: none)
+     *                          itemid : limit to specific grade item (default: all)
+     *                          grader : limit to specific graders (default: all)
+     *                          datefrom : start of date range
+     *                          datetill : end of date range
+     *                          revisedonly : only show revised grades (default: false)
+     *                          format : page | csv | excel (default: page)
+     * @param string $download Represents download format, pass '' no download at this time.
+     * @param int $page The current page being displayed.
+     * @param int $perpage Number of rules to display per page.
+     */
+    public function __construct($uniqueid, \context_course $context, $url, $filters = array(), $download = '', $page = 0,
+                                $perpage = 100) {
+        parent::__construct($uniqueid);
+
+        $this->set_attribute('class', 'gradereport_history generaltable generalbox');
+
+        // Set protected properties.
+        $this->context = $context;
+        $this->courseid = $this->context->instanceid;
+        $this->pagesize = $perpage;
+        $this->page = $page;
+        $this->filters = (object)$filters;
+        $this->gradeitems = \grade_item::fetch_all(array('courseid' => $this->courseid));
+        $this->cms = get_fast_modinfo($this->courseid);
+        $this->useridfield = 'userid';
+
+        // Define columns in the table.
+        $this->define_table_columns();
+
+        // Define configs.
+        $this->define_table_configs($url);
+
+        // Set download satus.
+        $this->is_downloading($download);
+    }
+
+    /**
+     * Define table configs.
+     *
+     * @param \moodle_url $url url of the page where this table would be displayed.
+     */
+    protected function define_table_configs(\moodle_url $url) {
+
+        // Set table url.
+        $urlparams = (array)$this->filters;
+        unset($urlparams['submitbutton']);
+        unset($urlparams['userfullnames']);
+        $url->params($urlparams);
+        $this->define_baseurl($url);
+
+        // Set table configs.
+        $this->collapsible(true);
+        $this->sortable(true, 'timemodified', SORT_DESC);
+        $this->pageable(true);
+        $this->no_sorting('grader');
+    }
+
+    /**
+     * Setup the headers for the html table.
+     */
+    protected function define_table_columns() {
+        $extrafields = get_extra_user_fields($this->context);
+
+        // Define headers and columns.
+        $cols = array(
+            'timemodified' => get_string('datetime', 'gradereport_history'),
+            'fullname' => get_string('name')
+        );
+
+        // Add headers for extra user fields.
+        foreach ($extrafields as $field) {
+            if (get_string_manager()->string_exists($field, 'moodle')) {
+                $cols[$field] = get_string($field);
+            } else {
+                $cols[$field] = $field;
+            }
+        }
+
+        // Add remaining headers.
+        $cols = array_merge($cols, array(
+            'itemname' => get_string('gradeitem', 'grades'),
+            'prevgrade' => get_string('gradeold', 'gradereport_history'),
+            'finalgrade' => get_string('gradenew', 'gradereport_history'),
+            'grader' => get_string('grader', 'gradereport_history'),
+            'source' => get_string('source', 'gradereport_history'),
+            'overridden' => get_string('overridden', 'grades'),
+            'locked' => get_string('locked', 'grades'),
+            'excluded' => get_string('excluded', 'gradereport_history'),
+            'feedback' => get_string('feedbacktext', 'gradereport_history')
+            )
+        );
+
+        $this->define_columns(array_keys($cols));
+        $this->define_headers(array_values($cols));
+    }
+
+    /**
+     * Method to display column timemodifed.
+     *
+     * @param \stdClass $history an entry of history record.
+     *
+     * @return string HTML to display
+     */
+    public function col_timemodified(\stdClass $history) {
+        return userdate($history->timemodified);
+    }
+
+    /**
+     * Method to display column itemname.
+     *
+     * @param \stdClass $history an entry of history record.
+     *
+     * @return string HTML to display
+     */
+    public function col_itemname(\stdClass $history) {
+        // Make sure grade item is still present and link it to the module if possible.
+        $itemid = $history->itemid;
+        if (!empty($this->gradeitems[$itemid])) {
+            if ($history->itemtype === 'mod' && !$this->is_downloading()) {
+                if (!empty($this->cms->instances[$history->itemmodule][$history->iteminstance])) {
+                    $cm = $this->cms->instances[$history->itemmodule][$history->iteminstance];
+                    $url = new \moodle_url('/mod/' . $history->itemmodule . '/view.php', array('id' => $cm->id));
+                    return \html_writer::link($url, $this->gradeitems[$itemid]->get_name());
+                }
+            }
+            return $this->gradeitems[$itemid]->get_name();
+        }
+        return get_string('deleteditemid', 'gradereport_history', $history->itemid);
+    }
+
+    /**
+     * Method to display column grader.
+     *
+     * @param \stdClass $history an entry of history record.
+     *
+     * @return string HTML to display
+     */
+    public function col_grader(\stdClass $history) {
+        $grader = new \stdClass();
+        $grader = username_load_fields_from_object($grader, $history, 'grader');
+        $name = fullname($grader);
+
+        if ($this->download) {
+            return $name;
+        }
+
+        $userid = $history->usermodified;
+        $profileurl = new \moodle_url('/user/view.php', array('id' => $userid, 'course' => $this->courseid));
+
+        return \html_writer::link($profileurl, $name);
+    }
+
+    /**
+     * Method to display column overridden.
+     *
+     * @param \stdClass $history an entry of history record.
+     *
+     * @return string HTML to display
+     */
+    public function col_overridden(\stdClass $history) {
+        return $history->overridden ? get_string('yes') : get_string('no');
+    }
+
+    /**
+     * Method to display column locked.
+     *
+     * @param \stdClass $history an entry of history record.
+     *
+     * @return string HTML to display
+     */
+    public function col_locked(\stdClass $history) {
+        return $history->locked ? get_string('yes') : get_string('no');
+    }
+
+    /**
+     * Method to display column excluded.
+     *
+     * @param \stdClass $history an entry of history record.
+     *
+     * @return string HTML to display
+     */
+    public function col_excluded(\stdClass $history) {
+        return $history->excluded ? get_string('yes') : get_string('no');
+    }
+
+    /**
+     * Method to display column feedback.
+     *
+     * @param \stdClass $history an entry of history record.
+     *
+     * @return string HTML to display
+     */
+    public function col_feedback(\stdClass $history) {
+        if ($this->is_downloading()) {
+            return $history->feedback;
+        } else {
+            return format_text($history->feedback, $history->feedbackformat, array('context' => $this->context));
+        }
+    }
+
+    /**
+     * Builds the sql and param list needed, based on the user selected filters.
+     *
+     * @return array containing sql to use and an array of params.
+     */
+    protected function get_filters_sql_and_params() {
+        global $DB;
+
+        $coursecontext = $this->context;
+        $filter = 'gi.courseid = :courseid';
+        $params = array(
+            'courseid' => $coursecontext->instanceid,
+        );
+
+        if (!empty($this->filters->itemid)) {
+            $filter .= ' AND ggh.itemid = :itemid';
+            $params['itemid'] = $this->filters->itemid;
+        }
+        if (!empty($this->filters->userids)) {
+            $list = explode(',', $this->filters->userids);
+            list($insql, $plist) = $DB->get_in_or_equal($list, SQL_PARAMS_NAMED);
+            $filter .= " AND ggh.userid $insql";
+            $params += $plist;
+        }
+        if (!empty($this->filters->datefrom)) {
+            $filter .= " AND ggh.timemodified >= :datefrom";
+            $params += array('datefrom' => $this->filters->datefrom);
+        }
+        if (!empty($this->filters->datetill)) {
+            $filter .= " AND ggh.timemodified <= :datetill";
+            $params += array('datetill' => $this->filters->datetill);
+        }
+        if (!empty($this->filters->grader)) {
+            $filter .= " AND ggh.usermodified = :grader";
+            $params += array('grader' => $this->filters->grader);
+        }
+
+        return array($filter, $params);
+    }
+
+    /**
+     * Builds the complete sql with all the joins to get the grade history data.
+     *
+     * @param bool $count setting this to true, returns an sql to get count only instead of the complete data records.
+     *
+     * @return array containing sql to use and an array of params.
+     */
+    protected function get_sql_and_params($count = false) {
+        $fields = 'ggh.id, ggh.timemodified, ggh.itemid, ggh.userid, ggh.finalgrade, ggh.usermodified,
+                   ggh.source, ggh.overridden, ggh.locked, ggh.excluded, ggh.feedback, ggh.feedbackformat,
+                   gi.itemtype, gi.itemmodule, gi.iteminstance, gi.itemnumber, ';
+
+        // Add extra user fields that we need for the graded user.
+        $extrafields = get_extra_user_fields($this->context);
+        foreach ($extrafields as $field) {
+            $fields .= 'u.' . $field . ', ';
+        }
+        $gradeduserfields = get_all_user_name_fields(true, 'u');
+        $fields .= $gradeduserfields . ', ';
+        $groupby = $fields;
+
+        // Add extra user fields that we need for the grader user.
+        $fields .= get_all_user_name_fields(true, 'ug', '', 'grader');
+        $groupby .= get_all_user_name_fields(true, 'ug');
+
+        // Filtering on revised grades only.
+        $revisedonly = !empty($this->filters->revisedonly);
+
+        if ($count && !$revisedonly) {
+            // We can only directly use count when not using the filter revised only.
+            $select = "COUNT(1)";
+        } else {
+            // Fetching the previous grade. We use MAX() to ensure that we only get one result if
+            // more than one histories happened at the same second.
+            $prevgrade = "SELECT MAX(finalgrade)
+                            FROM {grade_grades_history} h
+                           WHERE h.itemid = ggh.itemid
+                             AND h.userid = ggh.userid
+                             AND h.timemodified < ggh.timemodified
+                             AND NOT EXISTS (
+                              SELECT 1
+                                FROM {grade_grades_history} h2
+                               WHERE h2.itemid = ggh.itemid
+                                 AND h2.userid = ggh.userid
+                                 AND h2.timemodified < ggh.timemodified
+                                 AND h.timemodified < h2.timemodified)";
+
+            $select = "$fields, ($prevgrade) AS prevgrade,
+                      CASE WHEN gi.itemname IS NULL THEN gi.itemtype ELSE gi.itemname END AS itemname";
+        }
+
+        list($where, $params) = $this->get_filters_sql_and_params();
+
+        $sql =  "SELECT $select
+                   FROM {grade_grades_history} ggh
+              LEFT JOIN {grade_items} gi ON gi.id = ggh.itemid
+                   JOIN {user} u ON u.id = ggh.userid
+                   JOIN {user} ug ON ug.id = ggh.usermodified
+                  WHERE $where";
+
+        // As prevgrade is a dynamic field, we need to wrap the query. This is the only filtering
+        // that should be defined outside the method self::get_filters_sql_and_params().
+        if ($revisedonly) {
+            $allorcount = $count ? 'COUNT(1)' : '*';
+            $sql = "SELECT $allorcount FROM ($sql) pg
+                     WHERE pg.finalgrade != pg.prevgrade
+                        OR (pg.prevgrade IS NULL AND pg.finalgrade IS NOT NULL)
+                        OR (pg.prevgrade IS NOT NULL AND pg.finalgrade IS NULL)";
+        }
+
+        // Add order by if needed.
+        if (!$count && $this->get_sql_sort()) {
+            $sql .= " ORDER BY " . $this->get_sql_sort();
+        }
+
+        return array($sql, $params);
+    }
+
+    /**
+     * Query the reader. Store results in the object for use by build_table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar.
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+        global $DB;
+
+        list($countsql, $countparams) = $this->get_sql_and_params(true);
+        list($sql, $params) = $this->get_sql_and_params();
+        $total = $DB->count_records_sql($countsql, $countparams);
+        $this->pagesize($pagesize, $total);
+        $histories = $DB->get_records_sql($sql, $params, $this->pagesize * $this->page, $this->pagesize);
+        foreach ($histories as $history) {
+            $this->rawdata[] = $history;
+        }
+        // Set initial bars.
+        if ($useinitialsbar) {
+            $this->initialbars($total > $pagesize);
+        }
+    }
+
+    /**
+     * Returns a list of selected users.
+     *
+     * @return array returns an array in the format $userid => $userid
+     */
+    public function get_selected_users() {
+        global $DB;
+        $idlist = array();
+        if (!empty($this->filters->userids)) {
+
+            $idlist = explode(',', $this->filters->userids);
+            list($where, $params) = $DB->get_in_or_equal($idlist);
+            return $DB->get_records_select('user', "id $where", $params);
+
+        }
+        return $idlist;
+    }
+
+}
diff --git a/grade/report/history/classes/output/user_button.php b/grade/report/history/classes/output/user_button.php
new file mode 100644 (file)
index 0000000..a5c8324
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * User button. Adapted from core_select_user_button.
+ *
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradereport_history\output;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * A button that is used to select users for a form.
+ *
+ * @since      Moodle 2.8
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_button extends \single_button implements \renderable {
+    /**
+     * Initialises the new select_user_button.
+     *
+     * @param \moodle_url $url
+     * @param string $label The text to display in the button
+     * @param string $method Either post or get
+     */
+    public function __construct(\moodle_url $url, $label, $method = 'post') {
+        parent::__construct($url, $label, $method);
+        $this->class = 'singlebutton selectusersbutton gradereport_history_plugin';
+        $this->formid = \html_writer::random_id('selectusersbutton');
+    }
+}
diff --git a/grade/report/history/db/access.php b/grade/report/history/db/access.php
new file mode 100644 (file)
index 0000000..0d1bd5d
--- /dev/null
@@ -0,0 +1,41 @@
+<?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/>.
+
+/**
+ * Capability definition for the gradebook grader report
+ *
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+
+    'gradereport/history:view' => array(
+        'riskbitmask' => RISK_PERSONAL,
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'teacher' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW
+        ),
+        'clonepermissionsfrom' => 'gradereport/grader:view'
+    )
+);
diff --git a/grade/report/history/index.php b/grade/report/history/index.php
new file mode 100644 (file)
index 0000000..3813c77
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The gradebook grade history report
+ *
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir.'/gradelib.php');
+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.
+
+$PAGE->set_pagelayout('report');
+$url = new moodle_url('/grade/report/history/index.php', array('id' => $courseid));
+$PAGE->set_url($url);
+
+$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+require_login($course);
+$context = context_course::instance($course->id);
+
+require_capability('gradereport/history:view', $context);
+require_capability('moodle/grade:viewall', $context);
+
+// Last selected report session tracking.
+if (!isset($USER->grade_last_report)) {
+    $USER->grade_last_report = array();
+}
+$USER->grade_last_report[$course->id] = 'history';
+
+$select = "itemtype != 'course' AND itemname != '' AND courseid = :courseid";
+$itemids = $DB->get_records_select_menu('grade_items', $select, array('courseid' => $course->id), 'itemname ASC', 'id, itemname');
+$itemids = array(0 => get_string('allgradeitems', 'gradereport_history')) + $itemids;
+
+$output = $PAGE->get_renderer('gradereport_history');
+$graders = \gradereport_history\helper::get_graders($course->id);
+$params = array('course' => $course, 'itemids' => $itemids, 'graders' => $graders, 'userbutton' => null);
+$mform = new \gradereport_history\filter_form(null, $params);
+$filters = array();
+if ($data = $mform->get_data()) {
+    $filters = (array)$data;
+
+    if (!empty($filters['datetill'])) {
+        $filters['datetill'] += DAYSECS - 1; // Set to end of the chosen day.
+    }
+} else {
+    $filters = array(
+        'id' => $courseid,
+        'userids' => optional_param('userids', '', PARAM_SEQUENCE),
+        'itemid' => optional_param('itemid', 0, PARAM_INT),
+        'grader' => optional_param('grader', 0, PARAM_INT),
+        'datefrom' => optional_param('datefrom', 0, PARAM_INT),
+        'datetill' => optional_param('datetill', 0, PARAM_INT),
+        'revisedonly' => optional_param('revisedonly', 0, PARAM_INT),
+    );
+}
+
+$table = new \gradereport_history\output\tablelog('gradereport_history', $context, $url, $filters, $download, $page);
+
+$names = array();
+foreach ($table->get_selected_users() as $key => $user) {
+    $names[$key] = fullname($user);
+}
+$filters['userfullnames'] = implode(',', $names);
+
+// Set up js.
+\gradereport_history\helper::init_js($course->id, $names);
+
+// Now that we have the names, reinitialise the button so its able to control them.
+$button = new \gradereport_history\output\user_button($PAGE->url, get_string('selectusers', 'gradereport_history'), 'get');
+
+$userbutton = $output->render($button);
+$params = array('course' => $course, 'itemids' => $itemids, 'graders' => $graders, 'userbutton' => $userbutton);
+$mform = new \gradereport_history\filter_form(null, $params);
+$mform->set_data($filters);
+
+if ($table->is_downloading()) {
+    // Download file and exit.
+    echo $output->render($table);
+    die();
+}
+
+// Print header.
+print_grade_page_head($COURSE->id, 'report', 'history', get_string('pluginname', 'gradereport_history'), false, '');
+$mform->display();
+
+// Render table.
+echo $output->render($table);
+
+$event = \gradereport_history\event\grade_report_viewed::create(
+    array(
+        'context' => $context,
+        'courseid' => $courseid
+    )
+);
+$event->trigger();
+
+echo $OUTPUT->footer();
diff --git a/grade/report/history/lang/en/gradereport_history.php b/grade/report/history/lang/en/gradereport_history.php
new file mode 100644 (file)
index 0000000..651df25
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * Strings for component 'gradereport_history', language 'en'
+ *
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['allgradeitems'] = 'All grade items';
+$string['allgraders'] = 'All graders';
+$string['datefrom'] = 'Date from';
+$string['datetill'] = 'Date till';
+$string['datetime'] = 'Date and time';
+$string['deleteditemid'] = 'Delete item with id {$a}';
+$string['errajaxsearch'] = 'Error when searching users';
+$string['eventgradereportviewed'] = 'Grade history report viewed';
+$string['excluded'] = 'Excluded from calculations';
+$string['foundoneuser'] = '1 user found';
+$string['foundnusers'] = '{$a} users found';
+$string['feedbacktext'] = 'Feedback text';
+$string['finishselectingusers'] = 'Finish selecting users';
+$string['gradenew'] = 'Revised grade';
+$string['gradeold'] = 'Original grade';
+$string['grader'] = 'Grader';
+$string['history:view'] = 'View the grade history';
+$string['historyperpage'] = 'History entries per page';
+$string['historyperpage_help'] = 'This setting determines the number of history entries displayed per page in the history report.';
+$string['loadmoreusers'] = 'Load more users...';
+$string['pluginname'] = 'Grade history';
+$string['preferences'] = 'Grade history preferences';
+$string['revisedonly'] = 'Revised grades only';
+$string['revisedonly_help'] = 'Only show grades which have been revised.
+
+This means only entries which result in the grade changing are listed.';
+$string['selectuser'] = 'Select user';
+$string['selectusers'] = 'Select users';
+$string['selectedusers'] = 'Selected users';
+$string['source'] = 'Source';
+$string['useractivitygrade'] = '{$a} grade';
+$string['useractivityfeedback'] = '{$a} feedback';
diff --git a/grade/report/history/settings.php b/grade/report/history/settings.php
new file mode 100644 (file)
index 0000000..c841c80
--- /dev/null
@@ -0,0 +1,36 @@
+<?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/>.
+
+/**
+ * Defines site config settings for the grade history report
+ *
+ * @package    gradereport_history
+ * @copyright  2014 NetSpot Pty Ltd
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($ADMIN->fulltree) {
+
+    // Add settings for this module to the $settings object (it's already defined).
+    $settings->add(new admin_setting_configtext('grade_report_historyperpage',
+        new lang_string('historyperpage', 'gradereport_history'),
+        new lang_string('historyperpage_help', 'gradereport_history'),
+        50
+    ));
+
+}
diff --git a/grade/report/history/styles.css b/grade/report/history/styles.css
new file mode 100644 (file)
index 0000000..2e48c70
--- /dev/null
@@ -0,0 +1,105 @@
+/* History */
+
+.path-grade-report-history div.gradeparent {
+    overflow-x: scroll;
+}
+
+.path-grade-report-history .singlebutton div,
+.path-grade-report-history .singlebutton div input[type="button"] {
+    margin: 0;
+}
+
+/* User Selector */
+.yui3-gradereport_history_usp-hidden {
+    display:none;
+}
+
+.gradereport_history_usp .usp-content {
+    position: relative;
+}
+.gradereport_history_usp .usp-ajax-content {
+    overflow: auto;
+    border-top: 1px solid #ccc;
+    border-bottom: 1px solid #ccc;
+}
+.gradereport_history_usp .usp-ajax-content,
+.gradereport_history_usp .usp-loading-lightbox {
+    height: 375px;
+}
+.gradereport_history_usp .usp-loading-lightbox {
+    background-color: #fff;
+    opacity: .5;
+    position: absolute;
+    text-align: center;
+    width: 100%;
+    top: 0;
+    left: 0;
+}
+.gradereport_history_usp .usp-loading-lightbox img {
+    margin-top: 100px;
+    opacity: 1;
+}
+
+.gradereport_history_usp .usp-search {
+    text-align: center;
+}
+.gradereport_history_usp .usp-user {
+    width: 100%;
+    text-align: left;
+    border-top: 1px solid #eee;
+}
+.gradereport_history_usp .usp-user:nth-child(odd) {
+    background-color: #f9f9f9;
+}
+.gradereport_history_usp .usp-first-added {
+    border-top: 1px solid #bbb;
+}
+.gradereport_history_usp .usp-checkbox {
+    text-align: center;
+    float: left;
+    padding: 11px 6px 0 6px;
+}
+.gradereport_history_usp .usp-checkbox input[type=checkbox] {
+    margin: 0;
+}
+.gradereport_history_usp .usp-picture {
+    margin: 6px 3px 0 3px;
+    float: left;
+}
+.gradereport_history_usp .usp-userpicture{
+    cursor: pointer;
+}
+.gradereport_history_usp .usp-user .details {
+    margin-left: 67px;
+    padding: 3px 6px 0 6px;
+    word-wrap: break-word;
+}
+.gradereport_history_usp .usp-user .details label {
+    margin: 0;
+}
+.gradereport_history_usp .usp-more-results {
+    padding: 5px;
+    border-top: 1px solid #bbb;
+}
+.gradereport_history_usp .usp-finish {
+    padding-top: 1em;
+    text-align: center;
+}
+.gradereport_history_usp .usp-finish input {
+    margin: 0;
+}
+
+.dir-rtl .gradereport_history_usp .usp-search-results .usp-user {
+    text-align: right;
+}
+.dir-rtl .gradereport_history_usp .usp-picture,
+.dir-rtl .gradereport_history_usp .usp-checkbox {
+    float: right;
+}
+.dir-rtl .gradereport_history_usp .usp-user .details {
+    margin-right: 67px;
+    margin-left: 0;
+}
+.dir-rtl .gradereport_history_usp input.usp-search-btn {
+    margin-right: 5px;
+}
diff --git a/grade/report/history/tests/report_test.php b/grade/report/history/tests/report_test.php
new file mode 100644 (file)
index 0000000..7a3959d
--- /dev/null
@@ -0,0 +1,356 @@
+<?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/>.
+
+/**
+ * Grade history report test.
+ *
+ * @package    gradereport_history
+ * @copyright  2014 Frédéric Massart - FMCorz.net
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grade history report test class.
+ *
+ * @package    gradereport_history
+ * @copyright  2014 Frédéric Massart - FMCorz.net
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradereport_history_report_testcase extends advanced_testcase {
+
+    /**
+     * Create some grades.
+     */
+    public function test_query_db() {
+        $this->resetAfterTest();
+
+        // Making the setup.
+        $c1 = $this->getDataGenerator()->create_course();
+        $c2 = $this->getDataGenerator()->create_course();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+
+        // Users.
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+        $u4 = $this->getDataGenerator()->create_user();
+        $u5 = $this->getDataGenerator()->create_user();
+        $grader1 = $this->getDataGenerator()->create_user();
+        $grader2 = $this->getDataGenerator()->create_user();
+
+        // Modules.
+        $c1m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
+        $c1m2 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
+        $c1m3 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
+        $c2m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c2));
+        $c2m2 = $this->getDataGenerator()->create_module('assign', array('course' => $c2));
+
+        // Creating fake history data.
+        $giparams = array('itemtype' => 'mod', 'itemmodule' => 'assign');
+        $grades = array();
+
+        $gi = grade_item::fetch($giparams + array('iteminstance' => $c1m1->id));
+        $grades['c1m1u1'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
+                'timemodified' => time() - 3600));
+        $grades['c1m1u2'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u2->id,
+                'timemodified' => time() + 3600));
+        $grades['c1m1u3'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u3->id));
+        $grades['c1m1u4'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u4->id));
+        $grades['c1m1u5'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u5->id));
+
+        $gi = grade_item::fetch($giparams + array('iteminstance' => $c1m2->id));
+        $grades['c1m2u1'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id));
+        $grades['c1m2u2'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u2->id));
+
+        $gi = grade_item::fetch($giparams + array('iteminstance' => $c1m3->id));
+        $grades['c1m3u1'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id));
+
+        $gi = grade_item::fetch($giparams + array('iteminstance' => $c2m1->id));
+        $grades['c2m1u1'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
+            'usermodified' => $grader1->id));
+        $grades['c2m1u2'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u2->id,
+            'usermodified' => $grader1->id));
+        $grades['c2m1u3'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u3->id,
+            'usermodified' => $grader1->id));
+        $grades['c2m1u4'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u4->id,
+            'usermodified' => $grader2->id));
+
+        // Histories where grades have not been revised..
+        $grades['c2m1u5a'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u5->id,
+            'timemodified' => time() - 60));
+        $grades['c2m1u5b'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u5->id,
+            'timemodified' => time()));
+        $grades['c2m1u5c'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u5->id,
+            'timemodified' => time() + 60));
+
+        // Histories where grades have been revised and not revised.
+        $now = time();
+        $gi = grade_item::fetch($giparams + array('iteminstance' => $c2m2->id));
+        $grades['c2m2u1a'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
+            'timemodified' => $now - 60, 'finalgrade' => 50));
+        $grades['c2m2u1b'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
+            'timemodified' => $now - 50, 'finalgrade' => 50));      // Not revised.
+        $grades['c2m2u1c'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
+            'timemodified' => $now, 'finalgrade' => 75));
+        $grades['c2m2u1d'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
+            'timemodified' => $now + 10, 'finalgrade' => 75));      // Not revised.
+        $grades['c2m2u1e'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
+            'timemodified' => $now + 60, 'finalgrade' => 25));
+        $grades['c2m2u1f'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
+            'timemodified' => $now + 70, 'finalgrade' => 25));      // Not revised.
+
+        // TODO MDL-46736 Handle deleted/non-existing grade items.
+        // Histories with missing grade items, considered as deleted.
+        // $grades['c2x1u5'] = $this->create_grade_history($giparams + array('itemid' => -1, 'userid' => $u5->id, 'courseid' => $c1->id));
+        // $grades['c2x2u5'] = $this->create_grade_history($giparams + array('itemid' => 999999, 'userid' => $u5->id, 'courseid' => $c1->id));
+
+        // Basic filtering based on course id.
+        $this->assertEquals(8, $this->get_tablelog_results($c1ctx, array(), true));
+        $this->assertEquals(13, $this->get_tablelog_results($c2ctx, array(), true));
+
+        // Filtering on 1 user.
+        $this->assertEquals(3, $this->get_tablelog_results($c1ctx, array('userids' => $u1->id), true));
+
+        // Filtering on more users.
+        $this->assertEquals(4, $this->get_tablelog_results($c1ctx, array('userids' => "$u1->id,$u3->id"), true));
+
+        // Filtering based on one grade item.
+        $gi = grade_item::fetch($giparams + array('iteminstance' => $c1m1->id));
+        $this->assertEquals(5, $this->get_tablelog_results($c1ctx, array('itemid' => $gi->id), true));
+        $gi = grade_item::fetch($giparams + array('iteminstance' => $c1m3->id));
+        $this->assertEquals(1, $this->get_tablelog_results($c1ctx, array('itemid' => $gi->id), true));
+
+        // Filtering based on the grader.
+        $this->assertEquals(3, $this->get_tablelog_results($c2ctx, array('grader' => $grader1->id), true));
+        $this->assertEquals(1, $this->get_tablelog_results($c2ctx, array('grader' => $grader2->id), true));
+
+        // Filtering based on date.
+        $results = $this->get_tablelog_results($c1ctx, array('datefrom' => time() + 1800));
+        $this->assertGradeHistoryIds(array($grades['c1m1u2']->id), $results);
+        $results = $this->get_tablelog_results($c1ctx, array('datetill' => time() - 1800));
+        $this->assertGradeHistoryIds(array($grades['c1m1u1']->id), $results);
+        $results = $this->get_tablelog_results($c1ctx, array('datefrom' => time() - 1800, 'datetill' => time() + 1800));
+        $this->assertGradeHistoryIds(array($grades['c1m1u3']->id, $grades['c1m1u4']->id, $grades['c1m1u5']->id,
+            $grades['c1m2u1']->id, $grades['c1m2u2']->id, $grades['c1m3u1']->id), $results);
+
+        // Filtering based on revised only.
+        $this->assertEquals(3, $this->get_tablelog_results($c2ctx, array('userids' => $u5->id), true));
+        $this->assertEquals(1, $this->get_tablelog_results($c2ctx, array('userids' => $u5->id, 'revisedonly' => true), true));
+
+        // More filtering based on revised only.
+        $gi = grade_item::fetch($giparams + array('iteminstance' => $c2m2->id));
+        $this->assertEquals(6, $this->get_tablelog_results($c2ctx, array('userids' => $u1->id, 'itemid' => $gi->id), true));
+        $results = $this->get_tablelog_results($c2ctx, array('userids' => $u1->id, 'itemid' => $gi->id, 'revisedonly' => true));
+        $this->assertGradeHistoryIds(array($grades['c2m2u1a']->id, $grades['c2m2u1c']->id, $grades['c2m2u1e']->id), $results);
+
+        // Checking the value of the previous grade.
+        $this->assertEquals(null, $results[$grades['c2m2u1a']->id]->prevgrade);
+        $this->assertEquals($grades['c2m2u1a']->finalgrade, $results[$grades['c2m2u1c']->id]->prevgrade);
+        $this->assertEquals($grades['c2m2u1c']->finalgrade, $results[$grades['c2m2u1e']->id]->prevgrade);
+    }
+
+    /**
+     * Test the get users helper method.
+     */
+    public function test_get_users() {
+        $this->resetAfterTest();
+
+        // Making the setup.
+        $c1 = $this->getDataGenerator()->create_course();
+        $c2 = $this->getDataGenerator()->create_course();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+
+        $c1m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
+        $c2m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c2));
+
+        // Users.
+        $u1 = $this->getDataGenerator()->create_user(array('firstname' => 'Eric', 'lastname' => 'Cartman'));
+        $u2 = $this->getDataGenerator()->create_user(array('firstname' => 'Stan', 'lastname' => 'Marsh'));
+        $u3 = $this->getDataGenerator()->create_user(array('firstname' => 'Kyle', 'lastname' => 'Broflovski'));
+        $u4 = $this->getDataGenerator()->create_user(array('firstname' => 'Kenny', 'lastname' => 'McCormick'));
+
+        // Creating grade history for some users.
+        $gi = grade_item::fetch(array('iteminstance' => $c1m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
+        $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id));
+        $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u2->id));
+        $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u3->id));
+
+        $gi = grade_item::fetch(array('iteminstance' => $c2m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
+        $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u4->id));
+
+        // Checking fetching some users.
+        $users = \gradereport_history\helper::get_users($c1ctx);
+        $this->assertCount(3, $users);
+        $this->assertArrayHasKey($u3->id, $users);
+        $users = \gradereport_history\helper::get_users($c2ctx);
+        $this->assertCount(1, $users);
+        $this->assertArrayHasKey($u4->id, $users);
+        $users = \gradereport_history\helper::get_users($c1ctx, 'c');
+        $this->assertCount(1, $users);
+        $this->assertArrayHasKey($u1->id, $users);
+        $users = \gradereport_history\helper::get_users($c1ctx, '', 0, 2);
+        $this->assertCount(2, $users);
+        $this->assertArrayHasKey($u3->id, $users);
+        $this->assertArrayHasKey($u1->id, $users);
+        $users = \gradereport_history\helper::get_users($c1ctx, '', 1, 2);
+        $this->assertCount(1, $users);
+        $this->assertArrayHasKey($u2->id, $users);
+
+        // Checking the count of users.
+        $this->assertEquals(3, \gradereport_history\helper::get_users_count($c1ctx));
+        $this->assertEquals(1, \gradereport_history\helper::get_users_count($c2ctx));
+        $this->assertEquals(1, \gradereport_history\helper::get_users_count($c1ctx, 'c'));
+    }
+
+    /**
+     * Test the get graders helper method.
+     */
+    public function test_graders() {
+        $this->resetAfterTest();
+
+        // Making the setup.
+        $c1 = $this->getDataGenerator()->create_course();
+        $c2 = $this->getDataGenerator()->create_course();
+
+        $c1m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
+        $c2m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c2));
+
+        // Users.
+        $u1 = $this->getDataGenerator()->create_user(array('firstname' => 'Eric', 'lastname' => 'Cartman'));
+        $u2 = $this->getDataGenerator()->create_user(array('firstname' => 'Stan', 'lastname' => 'Marsh'));
+        $u3 = $this->getDataGenerator()->create_user(array('firstname' => 'Kyle', 'lastname' => 'Broflovski'));
+        $u4 = $this->getDataGenerator()->create_user(array('firstname' => 'Kenny', 'lastname' => 'McCormick'));
+
+        // Creating grade history for some users.
+        $gi = grade_item::fetch(array('iteminstance' => $c1m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
+        $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u1->id));
+        $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u2->id));
+        $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u3->id));
+
+        $gi = grade_item::fetch(array('iteminstance' => $c2m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
+        $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u4->id));
+
+        // Checking fetching some users.
+        $graders = \gradereport_history\helper::get_graders($c1->id);
+        $this->assertCount(4, $graders); // Including "all graders" .
+        $this->assertArrayHasKey($u1->id, $graders);
+        $this->assertArrayHasKey($u2->id, $graders);
+        $this->assertArrayHasKey($u3->id, $graders);
+        $graders = \gradereport_history\helper::get_graders($c2->id);
+        $this->assertCount(2, $graders); // Including "all graders" .
+        $this->assertArrayHasKey($u4->id, $graders);
+    }
+
+    /**
+     * Asserts that the array of grade objects contains exactly the right IDs.
+     *
+     * @param array $expectedids Array of expected IDs.
+     * @param array $objects Array of objects returned by the table.
+     */
+    protected function assertGradeHistoryIds(array $expectedids, array $objects) {
+        $this->assertCount(count($expectedids), $objects);
+        $expectedids = array_flip($expectedids);
+        foreach ($objects as $object) {
+            $this->assertArrayHasKey($object->id, $expectedids);
+            unset($expectedids[$object->id]);
+        }
+        $this->assertCount(0, $expectedids);
+    }
+
+    /**
+     * Create a new grade history entry.
+     *
+     * @param array $params Of values.
+     * @return object The grade object.
+     */
+    protected function create_grade_history($params) {
+        global $DB;
+        $params = (array) $params;
+
+        if (!isset($params['itemid'])) {
+            throw new coding_exception('Missing itemid key.');
+        }
+        if (!isset($params['userid'])) {
+            throw new coding_exception('Missing userid key.');
+        }
+
+        // Default object.
+        $grade = new stdClass();
+        $grade->itemid = 0;
+        $grade->userid = 0;
+        $grade->oldid = 123;
+        $grade->rawgrade = 50;
+        $grade->finalgrade = 50;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '';
+        $grade->informationformat = FORMAT_PLAIN;
+        $grade->feedback = '';
+        $grade->feedbackformat = FORMAT_PLAIN;
+        $grade->usermodified = 2;
+
+        // Merge with data passed.
+        $grade = (object) array_merge((array) $grade, $params);
+
+        // Insert record.
+        $grade->id = $DB->insert_record('grade_grades_history', $grade);
+
+        return $grade;
+    }
+
+    /**
+     * Returns a table log object.
+     *
+     * @param context_course $coursecontext The course context.
+     * @param array $filters An array of filters.
+     * @param boolean $count When true, returns a count rather than an array of objects.
+     * @return mixed Count or array of objects.
+     */
+    protected function get_tablelog_results($coursecontext, $filters = array(), $count = false) {
+        $table = new gradereport_history_tests_tablelog('something', $coursecontext, new moodle_url(''), $filters);
+        return $table->get_test_results($count);
+    }
+
+}
+
+/**
+ * Extended table log class.
+ */
+class gradereport_history_tests_tablelog extends \gradereport_history\output\tablelog {
+
+    /**
+     * Get the test results.
+     *
+     * @param boolean $count Whether or not we want the count.
+     * @return mixed Count or array of objects.
+     */
+    public function get_test_results($count = false) {
+        global $DB;
+        if ($count) {
+            list($sql, $params) = $this->get_sql_and_params(true);
+            return $DB->count_records_sql($sql, $params);
+        } else {
+            $this->setup();
+            list($sql, $params) = $this->get_sql_and_params();
+            return $DB->get_records_sql($sql, $params);
+        }
+    }
+
+}
diff --git a/grade/report/history/users_ajax.php b/grade/report/history/users_ajax.php
new file mode 100644 (file)
index 0000000..387a1aa
--- /dev/null
@@ -0,0 +1,75 @@
+<?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/>.
+
+/**
+ * User searching requests.
+ *
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('AJAX_SCRIPT', true);
+
+require_once(__DIR__ . '/../../../config.php');
+
+$id = required_param('id', PARAM_INT); // Course id.
+$search = optional_param('search', '', PARAM_RAW);
+$page = optional_param('page', 0, PARAM_INT);
+
+$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST);
+$context = context_course::instance($course->id, MUST_EXIST);
+
+if ($course->id == SITEID) {
+    throw new moodle_exception('invalidcourse');
+}
+
+require_sesskey();
+require_login($course);
+require_capability('gradereport/history:view', $context);
+require_capability('moodle/grade:viewall', $context);
+
+$outcome = new stdClass();
+$outcome->success = true;
+$outcome->error = '';
+
+$users = \gradereport_history\helper::get_users($context, $search, $page, 25);
+$outcome->response = array('users' => array());
+$outcome->response['totalusers'] = \gradereport_history\helper::get_users_count($context, $search);;
+
+$extrafields = get_extra_user_fields($context);
+$useroptions = array('link' => false, 'visibletoscreenreaders' => false);
+
+// Format the user record.
+foreach ($users as $user) {
+    $newuser = new stdClass();
+    $newuser->userid = $user->id;
+    $newuser->picture = $OUTPUT->user_picture($user, $useroptions);
+    $newuser->fullname = fullname($user);
+    $fieldvalues = array();
+    foreach ($extrafields as $field) {
+        $fieldvalues[] = s($user->{$field});
+    }
+    $newuser->extrafields = implode(', ', $fieldvalues);
+    $outcome->response['users'][] = $newuser;
+}
+
+$outcome->success = true;
+
+echo $OUTPUT->header();
+echo json_encode($outcome);
+echo $OUTPUT->footer();
diff --git a/grade/report/history/version.php b/grade/report/history/version.php
new file mode 100644 (file)
index 0000000..683cff7
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+/**
+ * Version details for the grade history
+ *
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @author     Adam Olley <adam.olley@netspot.com.au>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2014072900;
+$plugin->requires  = 2014072400;
+$plugin->component = 'gradereport_history';
diff --git a/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-debug.js b/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-debug.js
new file mode 100644 (file)
index 0000000..a305c9a
Binary files /dev/null and b/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-debug.js differ
diff --git a/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-min.js b/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-min.js
new file mode 100644 (file)
index 0000000..6efa157
Binary files /dev/null and b/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-min.js differ
diff --git a/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector.js b/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector.js
new file mode 100644 (file)
index 0000000..3209946
Binary files /dev/null and b/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector.js differ
diff --git a/grade/report/history/yui/src/userselector/build.json b/grade/report/history/yui/src/userselector/build.json
new file mode 100644 (file)
index 0000000..a03222c
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "name": "moodle-gradereport_history-userselector",
+    "builds": {
+        "moodle-gradereport_history-userselector": {
+            "jsfiles": [
+                "userselector.js"
+            ]
+        }
+    }
+}
diff --git a/grade/report/history/yui/src/userselector/js/userselector.js b/grade/report/history/yui/src/userselector/js/userselector.js
new file mode 100644 (file)
index 0000000..38f662e
--- /dev/null
@@ -0,0 +1,851 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The User Selector for the grade history report.
+ *
+ * @module     moodle-gradereport_history-userselector
+ * @package    gradereport_history
+ * @copyright  2013 NetSpot Pty Ltd (https://www.netspot.com.au)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @main       moodle-gradereport_history-userselector
+ */
+
+/**
+ * @module moodle-gradereport_history-userselector
+ */
+
+var COMPONENT = 'gradereport_history';
+var USP = {
+    AJAXURL: 'ajaxurl',
+    BASE: 'base',
+    CHECKBOX_NAME_PREFIX: 'usp-u',
+    COURSEID: 'courseid',
+    DIALOGUE_PREFIX: 'moodle-dialogue',
+    NAME: 'gradereport_history_usp',
+    PAGE: 'page',
+    PARAMS: 'params',
+    PERPAGE: 'perPage',
+    SEARCH: 'search',
+    SEARCHBTN: 'searchbtn',
+    SELECTEDUSERS: 'selectedUsers',
+    URL: 'url',
+    USERCOUNT: 'userCount'
+};
+var CSS = {
+    ACCESSHIDE: 'accesshide',
+    AJAXCONTENT: 'usp-ajax-content',
+    CHECKBOX: 'usp-checkbox',
+    CLOSE: 'close',
+    CLOSEBTN: 'usp-finish',
+    CONTENT: 'usp-content',
+    DETAILS: 'details',
+    EXTRAFIELDS: 'extrafields',
+    FIRSTADDED: 'usp-first-added',
+    FULLNAME: 'fullname',
+    HEADER: 'usp-header',
+    HIDDEN: 'hidden',
+    LIGHTBOX: 'usp-loading-lightbox',
+    LOADINGICON: 'loading-icon',
+    MORERESULTS: 'usp-more-results',
+    OPTIONS: 'options',
+    PICTURE: 'usp-picture',
+    RESULTSCOUNT: 'usp-results-count',
+    SEARCH: 'usp-search',
+    SEARCHBTN: 'usp-search-btn',
+    SEARCHFIELD: 'usp-search-field',
+    SEARCHRESULTS: 'usp-search-results',
+    SELECTED: 'selected',
+    USER: 'usp-user',
+    USERS: 'usp-users',
+    WRAP: 'usp-wrap'
+};
+var SELECTORS = {
+    AJAXCONTENT: '.' + CSS.AJAXCONTENT,
+    FINISHBTN: '.' + CSS.CLOSEBTN + ' input',
+    FIRSTADDED: '.' + CSS.FIRSTADDED,
+    FULLNAME: '.' + CSS.FULLNAME + ' label',
+    LIGHTBOX: '.' + CSS.LIGHTBOX,
+    MORERESULTS: '.' + CSS.MORERESULTS,
+    OPTIONS: '.' + CSS.OPTIONS,
+    PICTURE: '.' + CSS.USER + ' .userpicture',
+    RESULTSCOUNT: '.' + CSS.RESULTSCOUNT,
+    RESULTSUSERS: '.' + CSS.SEARCHRESULTS + ' .' + CSS.USERS,
+    SEARCHBTN: '.' + CSS.SEARCHBTN,
+    SEARCHFIELD: '.' + CSS.SEARCHFIELD,
+    SELECTEDNAMES: '.felement .selectednames',
+    TRIGGER: '.gradereport_history_plugin input.selectortrigger',
+    USER: '.' + CSS.USER,
+    USERFULLNAMES: 'input[name="userfullnames"]',
+    USERIDS: 'input[name="userids"]',
+    USERSELECT: '.' + CSS.CHECKBOX + ' input[type=checkbox]'
+};
+
+/**
+ * User Selector.
+ *
+ * @namespace M.gradereport_history
+ * @class UserSelector
+ * @constructor
+ */
+
+var USERSELECTOR = function() {
+    USERSELECTOR.superclass.constructor.apply(this, arguments);
+};
+Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.core.dialogue, {
+
+    /**
+     * Whether or not this is the first time the user displays the dialogue within that request.
+     *
+     * @property _firstDisplay
+     * @type Boolean
+     * @private
+     */
+    _firstDisplay: true,
+
+    /**
+     * The list of all the users selected while the dialogue is open.
+     *
+     * @type Object
+     * @property _usersBufferList
+     * @private
+     */
+    _usersBufferList: null,
+
+    /**
+     * The Node on which the focus is set.
+     *
+     * @property _userTabFocus
+     * @type Node
+     * @private
+     */
+    _userTabFocus: null,
+
+    /**
+     * Compiled template function for a user node.
+     *
+     * @property _userTemplate
+     * @type Function
+     * @private
+     */
+    _userTemplate: null,
+
+    initializer: function() {
+        var bb = this.get('boundingBox'),
+            content,
+            params,
+            tpl;
+
+        tpl = Y.Handlebars.compile(
+            '<div class="{{CSS.WRAP}}">' +
+                '<div class="{{CSS.HEADER}}">' +
+                    '<div class="{{CSS.SEARCH}}" role="search">' +
+                        '<form>' +
+                            '<input type="text" class="{{CSS.SEARCHFIELD}}" ' +
+                                'aria-label="{{get_string "search" "moodle"}}" value="" />' +
+                            '<input type="submit" class="{{CSS.SEARCHBTN}}"' +
+                                'value="{{get_string "search" "moodle"}}">' +
+                        '</form>' +
+                        '<div aria-live="polite" class="{{CSS.RESULTSCOUNT}}">{{get_string "loading" "admin"}}</div>' +
+                    '</div>' +
+                '</div>' +
+                '<div class="{{CSS.CONTENT}}">' +
+                    '<form>' +
+                        '<div class="{{CSS.AJAXCONTENT}}" aria-live="polite"></div>' +
+                        '<div class="{{CSS.LIGHTBOX}} {{CSS.HIDDEN}}">' +
+                            '<img class="{{CSS.LOADINGICON}}" alt="{{get_string "loading" "admin"}}"' +
+                                'src="{{{loadingIcon}}}">' +
+                        '</div>' +
+                        '<div class="{{CSS.CLOSEBTN}}">' +
+                            '<input type="submit" value="{{get_string "finishselectingusers" COMPONENT}}">' +
+                        '</div>' +
+                    '</form>' +
+                '</div>' +
+            '</div>');
+
+        content = Y.Node.create(
+            tpl({
+                COMPONENT: COMPONENT,
+                CSS: CSS,
+                loadingIcon: M.util.image_url('i/loading', 'moodle')
+            })
+        );
+
+        // Set the title and content.
+        this.getStdModNode(Y.WidgetStdMod.HEADER).prepend(Y.Node.create('<h1>' + this.get('title') + '</h1>'));
+        this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE);
+
+        // Use standard dialogue class name. This removes the default styling of the footer.
+        this.get('boundingBox').one('.moodle-dialogue-wrap').addClass('moodle-dialogue-content');
+
+        // Add the event on the button that opens the dialogue.
+        Y.one(SELECTORS.TRIGGER).on('click', this.show, this);
+
+        // The button to finalize the selection.
+        bb.one(SELECTORS.FINISHBTN).on('click', this.finishSelectingUsers, this);
+
+        // Delegate the keyboard navigation in the users list.
+        bb.delegate('key', this.userKeyboardNavigation, 'down:38,40', SELECTORS.AJAXCONTENT, this);
+
+        // Delegate the action to select a user.
+        Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.USERSELECT, this);
+        Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.PICTURE, this);
+
+        params = this.get(USP.PARAMS);
+        params.id = this.get(USP.COURSEID);
+        this.set(USP.PARAMS, params);
+
+        bb.one(SELECTORS.SEARCHBTN).on('click', this.search, this, false);
+    },
+
+    /**
+     * Display the dialogue.
+     *
+     * @method show
+     */
+    show: function(e) {
+        var bb;
+        this._usersBufferList = Y.clone(this.get(USP.SELECTEDUSERS));
+        if (this._firstDisplay) {
+            // Load the default list of users when the dialogue is loaded for the first time.
+            this._firstDisplay = false;
+            this.search(e, false);
+        } else {
+            // Leave the content as is, but reset the selection.
+            bb = this.get('boundingBox');
+
+            // Remove all the selected users.
+            bb.all(SELECTORS.USER).each(function(node) {
+                this.markUserNode(node, false);
+            }, this);
+
+            // Select the users.
+            Y.Object.each(this._usersBufferList, function(v, k) {
+                var user = bb.one(SELECTORS.USER + '[data-userid="' + k + '"]');
+                if (user) {
+                    this.markUserNode(user, true);
+                }
+            }, this);
+
+            // Reset the tab focus.
+            this.setUserTabFocus(bb.one(SELECTORS.USER));
+        }
+        return Y.namespace('M.gradereport_history.UserSelector').superclass.show.call(this);
+    },
+
+    /**
+     * Search for users.
+     *
+     * @method search
+     * @param {EventFacade} e The event.
+     * @param {Boolean} append Whether we want to append the results to the current results or not.
+     */
+    search: function(e, append) {
+        if (e) {
+            e.preventDefault();
+        }
+        var params;
+        if (append) {
+            this.set(USP.PAGE, this.get(USP.PAGE)+1);
+        } else {
+            this.set(USP.USERCOUNT, 0);
+            this.set(USP.PAGE, 0);
+        }
+        params = this.get(USP.PARAMS);
+        params.sesskey = M.cfg.sesskey;
+        params.action = 'searchusers';
+        params.search = this.get('boundingBox').one(SELECTORS.SEARCHFIELD).get('value');
+        params.page = this.get(USP.PAGE);
+        params.perpage = this.get(USP.PERPAGE);
+
+        Y.io(M.cfg.wwwroot + this.get(USP.AJAXURL), {
+            method:'POST',
+            data:build_querystring(params),
+            on: {
+                start: this.preSearch,
+                complete: this.processSearchResults,
+                end: this.postSearch
+            },
+            context:this,
+            "arguments": {      // Quoted because this is a reserved keyword.
+                append: append
+            }
+        });
+    },
+
+    /**
+     * Pre search callback.
+     *
+     * @method preSearch
+     * @param {String} transactionId The transaction ID.
+     * @param {Object} args The arguments passed from YUI.io()
+     */
+    preSearch: function(unused, args) {
+        var bb = this.get('boundingBox');
+
+        // Display the lightbox.
+        bb.one(SELECTORS.LIGHTBOX).removeClass(CSS.HIDDEN);
+
+        // Set the number of results to 'loading...'.
+        if (!args.append) {
+            bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('loading', 'admin'));
+        }
+    },
+
+    /**
+     * Post search callback.
+     *
+     * @method postSearch
+     * @param {String} transactionId The transaction ID.
+     * @param {Object} args The arguments passed from YUI.io()
+     */
+    postSearch: function(transactionId, args) {
+        var bb = this.get('boundingBox'),
+            firstAdded = bb.one(SELECTORS.FIRSTADDED),
+            firstUser;
+
+        // Hide the lightbox.
+        bb.one(SELECTORS.LIGHTBOX).addClass(CSS.HIDDEN);
+
+        if (args.append && firstAdded) {
+            // Sets the focus on the newly added user if we are appending results.
+            this.setUserTabFocus(firstAdded);
+            firstAdded.one(SELECTORS.USERSELECT).focus();
+        } else {
+            // New search result, set the tab focus on the first user returned.
+            firstUser = bb.one(SELECTORS.USER);
+            if (firstUser) {
+                this.setUserTabFocus(firstUser);
+            }
+        }
+    },
+
+    /**
+     * Process and display the search results.
+     *
+     * @method processSearchResults
+     * @param {String} tid The transaction ID.
+     * @param {Object} outcome The response object.
+     * @param {Object} args The arguments passed from YUI.io().
+     */
+    processSearchResults: function(tid, outcome, args) {
+        var result = false,
+            error = false,
+            bb = this.get('boundingBox'),
+            users,
+            userTemplate,
+            count,
+            selected,
+            i,
+            firstAdded = true,
+            node,
+            content,
+            fetchmore,
+            totalUsers;
+
+        // Decodes the result.
+        try {
+            result = Y.JSON.parse(outcome.responseText);
+            if (!result.success || result.error) {
+                error = true;
+            }
+        } catch (e) {
+            error = true;
+        }
+
+        // There was an error.
+        if (error) {
+            this.setContent('');
+            bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('errajaxsearch', COMPONENT));
+            return;
+        }
+
+        // Create the div containing the users when it is a fresh search.
+        if (!args.append) {
+            users = Y.Node.create('<div role="listbox" aria-activedescendant="" aria-multiselectable="true" class="'+CSS.USERS+'"></div>');
+        } else {
+            users = bb.one(SELECTORS.RESULTSUSERS);
+        }
+
+        // Compile the template for each user node.
+        if (!this._userTemplate) {
+            this._userTemplate = Y.Handlebars.compile(
+                '<div role="option" aria-selected="false" class="{{CSS.USER}} clearfix" ' +
+                        'data-userid="{{userId}}">' +
+                    '<div class="{{CSS.CHECKBOX}}">' +
+                        '<input name="{{USP.CHECKBOX_NAME_PREFIX}}{{userId}}" type="checkbox" tabindex="-1"' +
+                            'id="{{checkboxId}}" aria-describedby="{{checkboxId}} {{extraFieldsId}}"/>' +
+                    '</div>' +
+                    '<div class="{{CSS.PICTURE}}">{{{picture}}}</div>' +
+                    '<div class="{{CSS.DETAILS}}">' +
+                        '<div class="{{CSS.FULLNAME}}">' +
+                            '<label for="{{checkboxId}}">{{fullname}}</label>' +
+                        '</div>' +
+                        '<div id="{{extraFieldsId}}" class="{{CSS.EXTRAFIELDS}}">{{extrafields}}</div>' +
+                    '</div>' +
+                '</div>'
+            );
+        }
+        userTemplate = this._userTemplate;
+
+        // Append the users one by one.
+        count = this.get(USP.USERCOUNT);
+        selected = '';
+        for (i in result.response.users) {
+            count++;
+            user = result.response.users[i];
+
+            // If already selected.
+            if (Y.Object.hasKey(this._usersBufferList, user.userid)) {
+                selected = true;
+            } else {
+                selected = false;
+            }
+
+            node = Y.Node.create(userTemplate({
+                checkboxId: Y.guid(),
+                COMPONENT: COMPONENT,
+                count: count,
+                CSS: CSS,
+                extrafields: user.extrafields,
+                extraFieldsId: Y.guid(),
+                fullname: user.fullname,
+                picture: user.picture,
+                userId: user.userid,
+                USP: USP
+            }));
+
+            this.markUserNode(node, selected);
+
+            // Noting the first user that was when adding more results.
+            if (args.append && firstAdded) {
+                users.all(SELECTORS.FIRSTADDED).removeClass(CSS.FIRSTADDED);
+                node.addClass(CSS.FIRSTADDED);
+                firstAdded = false;
+            }
+            users.append(node);
+        }
+        this.set(USP.USERCOUNT, count);
+
+        // Update the count of users, and add a button to load more if need be.
+        totalUsers = parseInt(result.response.totalusers, 10);
+        if (!args.append) {
+            if (totalUsers === 0) {
+                bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('noresults', 'moodle'));
+                content = '';
+            } else {
+                if (totalUsers === 1) {
+                    bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundoneuser', COMPONENT));
+                } else {
+                    bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundnusers', COMPONENT, totalUsers));
+                }
+
+                content = Y.Node.create('<div class="'+CSS.SEARCHRESULTS+'"></div>')
+                    .append(users);
+                if (result.response.totalusers > (this.get(USP.PAGE)+1)*this.get(USP.PERPAGE)) {
+                    fetchmore = Y.Node.create('<div class="'+CSS.MORERESULTS+'">' +
+                        '<a href="#" role="button">'+M.util.get_string('loadmoreusers', COMPONENT)+'</a></div>');
+                    fetchmore.one('a').on('click', this.search, this, true);
+                    fetchmore.one('a').on('key', this.search, 'space', this, true);
+                    content.append(fetchmore);
+                }
+            }
+            this.setContent(content);
+        } else {
+            if (totalUsers <= (this.get(USP.PAGE)+1)*this.get(USP.PERPAGE)) {
+                bb.one(SELECTORS.MORERESULTS).remove();
+            }
+        }
+    },
+
+    /**
+     * When the user has finished selecting users.
+     *
+     * @method finishSelectingUsers
+     * @param {EventFacade} e The event.
+     */
+    finishSelectingUsers: function(e) {
+        e.preventDefault();
+        this.applySelection();
+        this.hide();
+    },
+
+    /**
+     * Apply the selection made.
+     *
+     * @method applySelection
+     * @param {EventFacade} e The event.
+     */
+    applySelection: function() {
+        var userIds = Y.Object.keys(this._usersBufferList);
+        this.set(USP.SELECTEDUSERS, Y.clone(this._usersBufferList))
+            .setNameDisplay();
+        Y.one(SELECTORS.USERIDS).set('value', userIds.join());
+    },
+
+    /**
+     * Select a user.
+     *
+     * @method SelectUser
+     * @param {EventFacade} e The event.
+     */
+    selectUser: function(e) {
+        var user = e.currentTarget.ancestor(SELECTORS.USER),
+            checkbox = user.one(SELECTORS.USERSELECT),
+            fullname = user.one(SELECTORS.FULLNAME).get('innerHTML'),
+            checked = checkbox.get('checked'),
+            userId = user.getData('userid');
+
+        if (e.currentTarget !== checkbox) {
+            // We triggered the selection from another node, so we need to change the checkbox value.
+            checked = !checked;
+        }
+
+        if (checked) {
+            // Selecting the user.
+            this._usersBufferList[userId] = fullname;
+        } else {
+            // De-selecting the user.
+            delete this._usersBufferList[userId];
+            delete this._usersBufferList[parseInt(userId, 10)]; // Also remove numbered keys.
+        }
+
+        this.markUserNode(user, checked);
+    },
+
+    /**
+     * Mark a user node as selected or not.
+     *
+     * This only takes care of the DOM side of things, not the internal mechanism
+     * storing what users have been selected or not.
+     *
+     * @param {Node} node The user node.
+     * @param {Boolean} selected True to mark as selected.
+     * @chainable
+     */
+    markUserNode: function(node, selected) {
+        if (selected) {
+            node.addClass(CSS.SELECTED)
+                .set('aria-selected', true)
+                .one(SELECTORS.USERSELECT)
+                    .set('checked', true);
+        } else {
+            node.removeClass(CSS.SELECTED)
+                .set('aria-selected', false)
+                .one(SELECTORS.USERSELECT)
+                    .set('checked', false);
+        }
+        return this;
+    },
+
+    /**
+     * Set the content of the dialogue.
+     *
+     * @method setContent
+     * @param {String} content The content.
+     * @chainable
+     */
+    setContent: function(content) {
+        this.get('boundingBox').one(SELECTORS.AJAXCONTENT).setHTML(content);
+        return this;
+    },
+
+    /**
+     * Display the names of the selected users in the form.
+     *
+     * @method setNameDisplay
+     */
+    setNameDisplay: function() {
+        var namelist = Y.Object.values(this.get(USP.SELECTEDUSERS));
+        Y.one(SELECTORS.SELECTEDNAMES).set('innerHTML', namelist.join(', '));
+        Y.one(SELECTORS.USERFULLNAMES).set('value', namelist.join());
+    },
+
+    /**
+     * User keyboard navigation.
+     *
+     * @method userKeyboardNavigation
+     */
+    userKeyboardNavigation: function(e) {
+        var bb = this.get('boundingBox'),
+            users = bb.all(SELECTORS.USER),
+            direction = 1,
+            user,
+            current = e.target.ancestor(SELECTORS.USER, true);
+
+        if (e.keyCode === 38) {
+            direction = -1;
+        }
+
+        user = this.findFocusableUser(users, current, direction);
+        if (user) {
+            e.preventDefault();
+            user.one(SELECTORS.USERSELECT).focus();
+            this.setUserTabFocus(user);
+        }
+    },
+
+    /**
+     * Find the next or previous focusable node.
+     *
+     * @param {NodeList} users The list of users.
+     * @param {Node} user The user to start with.
+     * @param {Number} direction The direction in which to go.
+     * @return {Node|null} A user node, or null if not found.
+     * @method findFocusableUser
+     */
+    findFocusableUser: function(users, user, direction) {
+        var index = users.indexOf(user);
+
+        if (users.size() < 1) {
+            Y.log('The users list is empty', 'debug', COMPONENT);
+            return null;
+        }
+
+        if (index < 0) {
+            Y.log('Unable to find the user in the list of users', 'debug', COMPONENT);
+            return users.item(0);
+        }
+
+        index += direction;
+
+        // Wrap the navigation when reaching the top of the bottom.
+        if (index < 0) {
+            index = users.size() - 1;
+        } else if (index >= users.size()) {
+            index = 0;
+        }
+
+        return users.item(index);
+    },
+
+    /**
+     * Set the user tab focus.
+     *
+     * @param {Node} user The user node.
+     * @method setUserTabFocus
+     */
+    setUserTabFocus: function(user) {
+        if (this._userTabFocus) {
+            this._userTabFocus.setAttribute('tabindex', '-1');
+        }
+
+        this._userTabFocus = user.one(SELECTORS.USERSELECT);
+        this._userTabFocus.setAttribute('tabindex', '0');
+
+        this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID());
+    }
+
+}, {
+    NAME: USP.NAME,
+    CSS_PREFIX: USP.CSS_PREFIX,
+    ATTRS: {
+
+        /**
+         * The header.
+         *
+         * @attribute title
+         * @default selectusers language string.
+         * @type String
+         */
+        title: {
+            validator: Y.Lang.isString,
+            valueFn: function() {
+                return M.util.get_string('selectusers', COMPONENT);
+            }
+        },
+
+        /**
+         * The current page URL.
+         *
+         * @attribute url
+         * @default null
+         * @type String
+         */
+        url: {
+            validator: Y.Lang.isString,
+            value: null
+        },
+
+        /**
+         * The URL to the Ajax file.
+         *
+         * @attribute ajaxurl
+         * @default null
+         * @type String
+         */
+        ajaxurl: {
+            validator: Y.Lang.isString,
+            value: null
+        },
+
+        /**
+         * The names of the selected users.
+         *
+         * The keys are the user IDs, the values are their fullname.
+         *
+         * @attribute selectedUsers
+         * @default null
+         * @type Object
+         */
+        selectedUsers: {
+            validator: Y.Lang.isObject,
+            value: null,
+            getter: function(v) {
+                if (v === null) {
+                    return {};
+                }
+                return v;
+            }
+        },
+
+        /**
+         * The course ID.
+         *
+         * @attribute courseid
+         * @default null
+         * @type Number
+         */
+        courseid: {
+            value: null
+        },
+
+        /**
+         * Array of parameters.
+         *
+         * @attribute params
+         * @default []
+         * @type Array
+         */
+        params: {
+            validator: Y.Lang.isArray,
+            value: []
+        },
+
+        /**
+         * The page we are on.
+         *
+         * @attribute page
+         * @default 0
+         * @type Number
+         */
+        page: {
+            validator: Y.Lang.isNumber,
+            value: 0
+        },
+
+        /**
+         * The number of users displayed.
+         *
+         * @attribute userCount
+         * @default 0
+         * @type Number
+         */
+        userCount: {
+            value: 0,
+            validator: Y.Lang.isNumber
+        },
+
+        /**
+         * The number of results per page.
+         *
+         * @attribute perPage
+         * @default 25
+         * @type Number
+         */
+        perPage: {
+            value: 25,
+            Validator: Y.Lang.isNumber
+        }
+
+    }
+});
+
+Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), {
+
+    /**
+     * List of extra classes.
+     *
+     * @attribute extraClasses
+     * @default ['gradereport_history_usp']
+     * @type Array
+     */
+    extraClasses: {
+        value: [
+            'gradereport_history_usp'
+        ]
+    },
+
+    /**
+     * Whether to focus on the target that caused the Widget to be shown.
+     *
+     * @attribute focusOnPreviousTargetAfterHide
+     * @default true
+     * @type Node
+     */
+    focusOnPreviousTargetAfterHide: {
+        value: true
+    },
+
+    /**
+     *
+     * Width.
+     *
+     * @attribute width
+     * @default '500px'
+     * @type String|Number
+     */
+    width: {
+        value: '500px'
+    },
+
+    /**
+     * Boolean indicating whether or not the Widget is visible.
+     *
+     * @attribute visible
+     * @default false
+     * @type Boolean
+     */
+    visible: {
+        value: false
+    },
+
+   /**
+    * Whether the widget should be modal or not.
+    *
+    * @attribute modal
+    * @type Boolean
+    * @default true
+    */
+    modal: {
+        value: true
+    },
+
+   /**
+    * Whether the widget should be draggable or not.
+    *
+    * @attribute draggable
+    * @type Boolean
+    * @default true
+    */
+    draggable: {
+        value: true
+    }
+
+});
+
+Y.namespace('M.gradereport_history.UserSelector').init = function(cfg) {
+    return new USERSELECTOR(cfg);
+};
diff --git a/grade/report/history/yui/src/userselector/meta/userselector.json b/grade/report/history/yui/src/userselector/meta/userselector.json
new file mode 100644 (file)
index 0000000..b7af13e
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "moodle-gradereport_history-userselector": {
+        "requires": [
+            "escape",
+            "event-delegate",
+            "event-key",
+            "handlebars",
+            "io-base",
+            "json-parse",
+            "moodle-core-notification-dialogue"
+        ]
+    }
+}
index 29eb8b6..966951b 100644 (file)
@@ -380,7 +380,7 @@ class grade_report_user extends grade_report {
 
             /// Hidden Items
             if ($grade_grade->grade_item->is_hidden()) {
-                $hidden = ' hidden';
+                $hidden = ' dimmed_text';
             }
 
             $hide = false;
@@ -461,7 +461,7 @@ class grade_report_user extends grade_report {
                         $data['grade']['content'] = get_string('submittedon', 'grades', userdate($grade_grade->get_datesubmitted(), get_string('strftimedatetimeshort')));
 
                     } elseif ($grade_grade->is_hidden()) {
-                            $data['grade']['class'] = $class.' hidden';
+                            $data['grade']['class'] = $class.' dimmed_text';
                             $data['grade']['content'] = '-';
                     } else {
                         $data['grade']['class'] = $class;
@@ -493,7 +493,7 @@ class grade_report_user extends grade_report {
                         $data['percentage']['class'] = $class.' gradingerror';
                         $data['percentage']['content'] = get_string('error');
                     } else if ($grade_grade->is_hidden()) {
-                        $data['percentage']['class'] = $class.' hidden';
+                        $data['percentage']['class'] = $class.' dimmed_text';
                         $data['percentage']['content'] = '-';
                     } else {
                         $data['percentage']['class'] = $class;
@@ -508,7 +508,7 @@ class grade_report_user extends grade_report {
                         $data['lettergrade']['class'] = $class.' gradingerror';
                         $data['lettergrade']['content'] = get_string('error');
                     } else if ($grade_grade->is_hidden()) {
-                        $data['lettergrade']['class'] = $class.' hidden';
+                        $data['lettergrade']['class'] = $class.' dimmed_text';
                         if (!$this->canviewhidden) {
                             $data['lettergrade']['content'] = '-';
                         } else {
@@ -527,7 +527,7 @@ class grade_report_user extends grade_report {
                         $data['rank']['class'] = $class.' gradingerror';
                         $data['rank']['content'] = get_string('error');
                         } elseif ($grade_grade->is_hidden()) {
-                            $data['rank']['class'] = $class.' hidden';
+                            $data['rank']['class'] = $class.' dimmed_text';
                             $data['rank']['content'] = '-';
                     } else if (is_null($gradeval)) {
                         // no grade, no rank
index 2a958d3..c15f136 100644 (file)
@@ -5,8 +5,6 @@
 #graded_users_selector {float: right;text-align: right;}
 
 /* this must be last if we want to override other category and course item colors */
-.path-grade-report-user .user-grade .hidden,
-.path-grade-report-user .user-grade .hidden a {color:#aaaaaa;}
 .user-grade {border: 1px solid black;margin: auto;padding: 0.25em;font-size: 0.8em;}
 .user-grade td {margin: 1px;padding: 0.25em;min-width: 2em;vertical-align: top;}
 .user-grade thead {border-bottom: 3px double black;}
@@ -53,7 +51,6 @@
 .user-grade td.item,
 .user-grade th.item {border-left: 1px solid gray;border-right: 1px solid gray}
 .user-grade td.excluded {background-color: #666;}
-.user-grade td.hidden {color: #aaa;}
 .user-grade td.feedbacktext {max-width:600px;padding:2px 2px;}
 .pagelayout-report .user-grade .feedbacktext .no-overflow {overflow:auto;padding:0.25em;}
 
index e45f623..69a706a 100644 (file)
@@ -1034,7 +1034,7 @@ class core_plugin_manager {
             ),
 
             'gradereport' => array(
-                'grader', 'outcomes', 'overview', 'user'
+                'grader', 'history', 'outcomes', 'overview', 'user'
             ),
 
             'gradingform' => array(
index d4e09f9..d112172 100644 (file)
@@ -66,8 +66,11 @@ class manager {
         foreach ($tasks as $task) {
             $record = (object) $task;
             $scheduledtask = self::scheduled_task_from_record($record);
-            $scheduledtask->set_component($componentname);
-            $scheduledtasks[] = $scheduledtask;
+            // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
+            if ($scheduledtask) {
+                $scheduledtask->set_component($componentname);
+                $scheduledtasks[] = $scheduledtask;
+            }
         }
 
         return $scheduledtasks;
@@ -237,6 +240,7 @@ class manager {
             $classname = '\\' . $classname;
         }
         if (!class_exists($classname)) {
+            debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
             return false;
         }
         $task = new $classname;
@@ -272,6 +276,7 @@ class manager {
             $classname = '\\' . $classname;
         }
         if (!class_exists($classname)) {
+            debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
             return false;
         }
         /** @var \core\task\scheduled_task $task */
@@ -328,7 +333,10 @@ class manager {
         $records = $DB->get_records('task_scheduled', array('componentname' => $componentname), 'classname', '*', IGNORE_MISSING);
         foreach ($records as $record) {
             $task = self::scheduled_task_from_record($record);
-            $tasks[] = $task;
+            // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
+            if ($task) {
+                $tasks[] = $task;
+            }
         }
 
         return $tasks;
@@ -362,8 +370,12 @@ class manager {
      */
     public static function get_default_scheduled_task($classname) {
         $task = self::get_scheduled_task($classname);
+        $componenttasks = array();
 
-        $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
+        // Safety check in case no task was found for the given classname.
+        if ($task) {
+            $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
+        }
 
         foreach ($componenttasks as $componenttask) {
             if (get_class($componenttask) == get_class($task)) {
@@ -387,7 +399,10 @@ class manager {
 
         foreach ($records as $record) {
             $task = self::scheduled_task_from_record($record);
-            $tasks[] = $task;
+            // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
+            if ($task) {
+                $tasks[] = $task;
+            }
         }
 
         return $tasks;
@@ -418,6 +433,11 @@ class manager {
             if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 10)) {
                 $classname = '\\' . $record->classname;
                 $task = self::adhoc_task_from_record($record);
+                // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
+                if (!$task) {
+                    $lock->release();
+                    continue;
+                }
 
                 $task->set_lock($lock);
                 if (!$task->is_blocking()) {
@@ -463,6 +483,11 @@ class manager {
             if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
                 $classname = '\\' . $record->classname;
                 $task = self::scheduled_task_from_record($record);
+                // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
+                if (!$task) {
+                    $lock->release();
+                    continue;
+                }
 
                 $task->set_lock($lock);
 
index 9d2536d..8fe2f3c 100644 (file)
@@ -1319,6 +1319,10 @@ function get_coursemodule_from_id($modulename, $cmid, $courseid=0, $sectionnum=f
                                                 WHERE cm.id = :cmid", $params, $strictness)) {
             return false;
         }
+    } else {
+        if (!core_component::is_valid_plugin_name('mod', $modulename)) {
+            throw new coding_exception('Invalid modulename parameter');
+        }
     }
 
     $params['modulename'] = $modulename;
@@ -1368,6 +1372,10 @@ function get_coursemodule_from_id($modulename, $cmid, $courseid=0, $sectionnum=f
 function get_coursemodule_from_instance($modulename, $instance, $courseid=0, $sectionnum=false, $strictness=IGNORE_MISSING) {
     global $DB;
 
+    if (!core_component::is_valid_plugin_name('mod', $modulename)) {
+        throw new coding_exception('Invalid modulename parameter');
+    }
+
     $params = array('instance'=>$instance, 'modulename'=>$modulename);
 
     $courseselect = "";
@@ -1406,6 +1414,10 @@ function get_coursemodule_from_instance($modulename, $instance, $courseid=0, $se
 function get_coursemodules_in_course($modulename, $courseid, $extrafields='') {
     global $DB;
 
+    if (!core_component::is_valid_plugin_name('mod', $modulename)) {
+        throw new coding_exception('Invalid modulename parameter');
+    }
+
     if (!empty($extrafields)) {
         $extrafields = ", $extrafields";
     }
@@ -1444,6 +1456,10 @@ function get_coursemodules_in_course($modulename, $courseid, $extrafields='') {
 function get_all_instances_in_courses($modulename, $courses, $userid=NULL, $includeinvisible=false) {
     global $CFG, $DB;
 
+    if (!core_component::is_valid_plugin_name('mod', $modulename)) {
+        throw new coding_exception('Invalid modulename parameter');
+    }
+
     $outputarray = array();
 
     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
index eb94076..a909f33 100644 (file)
@@ -497,7 +497,7 @@ class database_manager {
 
         // Check new table doesn't exist
         if ($this->table_exists($check)) {
-            throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName(), 'can not rename table');
+            throw new ddl_exception('ddltablealreadyexists', $check->getName(), 'can not rename table');
         }
 
         if (!$sqlarr = $this->generator->getRenameTableSQL($xmldb_table, $newname)) {
index 25ec8c3..11e9929 100644 (file)
@@ -563,6 +563,17 @@ class core_ddl_testcase extends database_driver_testcase {
             'secondname' => 'not important',
             'intro'      => 'not important');
         $this->assertSame($insertedrows+1, $DB->insert_record('test_table_cust1', $rec));
+
+        // Verify behavior when target table already exists.
+        $sourcetable = $this->create_deftable('test_table0');
+        $targettable = $this->create_deftable('test_table1');
+        try {
+            $dbman->rename_table($sourcetable, $targettable->getName());
+            $this->fail('Exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('ddl_exception', $e);
+            $this->assertEquals('Table "test_table1" already exists (can not rename table)', $e->getMessage());
+        }
     }
 
     /**
index 8e19cb2..4379be5 100644 (file)
Binary files a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-debug.js and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-debug.js differ
index 0b9d2ea..18931ef 100644 (file)
Binary files a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-min.js and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-min.js differ
index 8e19cb2..4379be5 100644 (file)
Binary files a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button.js and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button.js differ
index 9c65d86..12f1e6a 100644 (file)
@@ -62,7 +62,9 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
      * @private
      */
     _showHTML: function() {
-        var host = this.get('host');
+        var host = this.get('host'),
+            textareaLabel = host.textareaLabel;
+
         if (!this.get('isHTML')) {
             // Unhighlight icon.
             this.unHighlightButtons('html');
@@ -77,6 +79,11 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
             host.textarea.hide();
             this.editor.show();
 
+            if (textareaLabel) {
+                // Update the textarea label.
+                textareaLabel.setAttribute('for', this.editor.getAttribute('id'));
+            }
+
             // Focus on the editor.
             host.focus();
 
@@ -107,6 +114,10 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
             this.editor.hide();
             host.textarea.show();
 
+            if (textareaLabel) {
+                // Update the textarea label.
+                textareaLabel.setAttribute('for', host.textarea.getAttribute('id'));
+            }
 
             // Focus on the textarea.
             host.textarea.focus();
index 40cc593..a1ab9c2 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 068aab8..fafaf0f 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index cb7326d..bab530d 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index e03c03f..7467317 100644 (file)
@@ -183,12 +183,8 @@ Y.extend(Editor, Y.Base, {
             CSS: CSS
         }));
 
-        // Add a labelled-by attribute to the contenteditable.
-        this.textareaLabel = Y.one('[for="' + this.get('elementid') + '"]');
-        if (this.textareaLabel) {
-            this.textareaLabel.generateID();
-            this.editor.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
-        }
+        // Setup the labels and ARIA labelledby.
+        this._setupLabelling();
 
         // Add everything to the wrapper.
         this.setupToolbar();
@@ -375,6 +371,48 @@ Y.extend(Editor, Y.Base, {
      */
     _registerEventHandle: function(handle) {
         this._eventHandles.push(handle);
+    },
+
+    /**
+     * Setup editor labelling. This includes the aria-labelledby attribute
+     * too.
+     *
+     * @method _setupLabelling
+     * @chainable
+     * @private
+     */
+    _setupLabelling: function() {
+        // Copy the ARIA labelledby attribute from the textarea.
+        var labelledby = this.textarea.getAttribute('aria-labelledby'),
+            labels = [];
+
+        if (labelledby) {
+            // The original textarea has a labelledby field, convert the list of IDs into a list.
+            labels = labelledby.split(' ');
+        }
+
+        // Search for any labels for the original textarea.
+        this.textareaLabel = Y.one('[for="' + this.get('elementid') + '"]');
+        if (this.textareaLabel) {
+            // Ensure that the label has an ID.
+            this.textareaLabel.generateID();
+
+            // Add the label to the list of labels. It may be there already, but we can dedupe the list.
+            labels.push(this.textareaLabel.get('id'));
+
+            // Update the label to point to the Atto editor instead of the original textarea.
+            this.textareaLabel.setAttribute('for', this.editor.getAttribute('id'));
+
+            // Register an event handle for the label - labels don't normally focus on non-input elements so we must add
+            // a click handler.
+            // If something changes what the label is for (e.g. an HTML plugin), then this will cease to match and
+            // standard browser behaviour will resume.
+            this._registerEventHandle(Y.delegate('click', this.focus,
+                    Y.config.doc.body, '[for="' + this.editor.getAttribute('id') + '"]', this));
+        }
+
+        // Add the list of labelling attributes to the atto editor.
+        this.editor.setAttribute('aria-labelledby', Y.Array.unique(labels).join(' '));
     }
 
 }, {
index 4c60fc6..d29a9cc 100644 (file)
@@ -1070,7 +1070,7 @@ class behat_general extends behat_base {
      *        | Header 1 | Header 2 | Header 3 |
      *        | Value 1 | Value 2 | Value 3|
      */
-    public function following_should_exit_in_the_table($table, TableNode $data) {
+    public function following_should_exist_in_the_table($table, TableNode $data) {
         $datahash = $data->getHash();
 
         foreach ($datahash as $value) {
@@ -1092,7 +1092,7 @@ class behat_general extends behat_base {
      *        | Header 1 | Header 2 | Header 3 |
      *        | Value 1 | Value 2 | Value 3|
      */
-    public function following_should_not_exit_in_the_table($table, TableNode $data) {
+    public function following_should_not_exist_in_the_table($table, TableNode $data) {
         $datahash = $data->getHash();
 
         foreach ($datahash as $value) {
index fd224bb..be9385b 100644 (file)
@@ -346,4 +346,311 @@ class core_datalib_testcase extends advanced_testcase {
         $this->assertTimeCurrent($record1->cacherev);
         $this->assertEquals($record1->cacherev, $record2->cacherev);
     }
+
+    public function test_get_coursemodule_from_id() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser(); // Some generators have bogus access control.
+
+        $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php");
+        $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php");
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+
+        $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3));
+        $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1));
+        $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1));
+
+        $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2));
+
+        $cm = get_coursemodule_from_id('folder', $folder1a->cmid);
+        $this->assertInstanceOf('stdClass', $cm);
+        $this->assertSame('folder', $cm->modname);
+        $this->assertSame($folder1a->id, $cm->instance);
+        $this->assertSame($folder1a->course, $cm->course);
+        $this->assertObjectNotHasAttribute('sectionnum', $cm);
+
+        $this->assertEquals($cm, get_coursemodule_from_id('', $folder1a->cmid));
+        $this->assertEquals($cm, get_coursemodule_from_id('folder', $folder1a->cmid, $course1->id));
+        $this->assertEquals($cm, get_coursemodule_from_id('folder', $folder1a->cmid, 0));
+        $this->assertFalse(get_coursemodule_from_id('folder', $folder1a->cmid, -10));
+
+        $cm2 = get_coursemodule_from_id('folder', $folder1a->cmid, 0, true);
+        $this->assertEquals(3, $cm2->sectionnum);
+        unset($cm2->sectionnum);
+        $this->assertEquals($cm, $cm2);
+
+        $this->assertFalse(get_coursemodule_from_id('folder', -11));
+
+        try {
+            get_coursemodule_from_id('folder', -11, 0, false, MUST_EXIST);
+            $this->fail('dml_missing_record_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('dml_missing_record_exception', $e);
+        }
+
+        try {
+            get_coursemodule_from_id('', -11, 0, false, MUST_EXIST);
+            $this->fail('dml_missing_record_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('dml_missing_record_exception', $e);
+        }
+
+        try {
+            get_coursemodule_from_id('a b', $folder1a->cmid, 0, false, MUST_EXIST);
+            $this->fail('coding_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        try {
+            get_coursemodule_from_id('abc', $folder1a->cmid, 0, false, MUST_EXIST);
+            $this->fail('dml_read_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('dml_read_exception', $e);
+        }
+    }
+
+    public function test_get_coursemodule_from_instance() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser(); // Some generators have bogus access control.
+
+        $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php");
+        $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php");
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+
+        $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3));
+        $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1));
+
+        $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2));
+
+        $cm = get_coursemodule_from_instance('folder', $folder1a->id);
+        $this->assertInstanceOf('stdClass', $cm);
+        $this->assertSame('folder', $cm->modname);
+        $this->assertSame($folder1a->id, $cm->instance);
+        $this->assertSame($folder1a->course, $cm->course);
+        $this->assertObjectNotHasAttribute('sectionnum', $cm);
+
+        $this->assertEquals($cm, get_coursemodule_from_instance('folder', $folder1a->id, $course1->id));
+        $this->assertEquals($cm, get_coursemodule_from_instance('folder', $folder1a->id, 0));
+        $this->assertFalse(get_coursemodule_from_instance('folder', $folder1a->id, -10));
+
+        $cm2 = get_coursemodule_from_instance('folder', $folder1a->id, 0, true);
+        $this->assertEquals(3, $cm2->sectionnum);
+        unset($cm2->sectionnum);
+        $this->assertEquals($cm, $cm2);
+
+        $this->assertFalse(get_coursemodule_from_instance('folder', -11));
+
+        try {
+            get_coursemodule_from_instance('folder', -11, 0, false, MUST_EXIST);
+            $this->fail('dml_missing_record_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('dml_missing_record_exception', $e);
+        }
+
+        try {
+            get_coursemodule_from_instance('a b', $folder1a->cmid, 0, false, MUST_EXIST);
+            $this->fail('coding_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        try {
+            get_coursemodule_from_instance('', $folder1a->cmid, 0, false, MUST_EXIST);
+            $this->fail('coding_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        try {
+            get_coursemodule_from_instance('abc', $folder1a->cmid, 0, false, MUST_EXIST);
+            $this->fail('dml_read_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('dml_read_exception', $e);
+        }
+    }
+
+    public function test_get_coursemodules_in_course() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser(); // Some generators have bogus access control.
+
+        $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php");
+        $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php");
+        $this->assertFileExists("$CFG->dirroot/mod/label/lib.php");
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+
+        $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3));
+        $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1));
+        $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1));
+
+        $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2));
+        $glossary2a = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
+        $glossary2b = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
+
+        $modules = get_coursemodules_in_course('folder', $course1->id);
+        $this->assertCount(2, $modules);
+
+        $cm = $modules[$folder1a->cmid];
+        $this->assertSame('folder', $cm->modname);
+        $this->assertSame($folder1a->id, $cm->instance);
+        $this->assertSame($folder1a->course, $cm->course);
+        $this->assertObjectNotHasAttribute('sectionnum', $cm);
+        $this->assertObjectNotHasAttribute('revision', $cm);
+        $this->assertObjectNotHasAttribute('display', $cm);
+
+        $cm = $modules[$folder1b->cmid];
+        $this->assertSame('folder', $cm->modname);
+        $this->assertSame($folder1b->id, $cm->instance);
+        $this->assertSame($folder1b->course, $cm->course);
+        $this->assertObjectNotHasAttribute('sectionnum', $cm);
+        $this->assertObjectNotHasAttribute('revision', $cm);
+        $this->assertObjectNotHasAttribute('display', $cm);
+
+        $modules = get_coursemodules_in_course('folder', $course1->id, 'revision, display');
+        $this->assertCount(2, $modules);
+
+        $cm = $modules[$folder1a->cmid];
+        $this->assertSame('folder', $cm->modname);
+        $this->assertSame($folder1a->id, $cm->instance);
+        $this->assertSame($folder1a->course, $cm->course);
+        $this->assertObjectNotHasAttribute('sectionnum', $cm);
+        $this->assertObjectHasAttribute('revision', $cm);
+        $this->assertObjectHasAttribute('display', $cm);
+
+        $modules = get_coursemodules_in_course('label', $course1->id);
+        $this->assertCount(0, $modules);
+
+        try {
+            get_coursemodules_in_course('a b', $course1->id);
+            $this->fail('coding_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        try {
+            get_coursemodules_in_course('abc', $course1->id);
+            $this->fail('dml_read_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('dml_read_exception', $e);
+        }
+    }
+
+    public function test_get_all_instances_in_courses() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser(); // Some generators have bogus access control.
+
+        $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php");
+        $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php");
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+
+        $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3));
+        $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1));
+        $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1));
+
+        $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2));
+        $glossary2a = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
+        $glossary2b = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
+
+        $folder3 = $this->getDataGenerator()->create_module('folder', array('course' => $course3));
+
+        $modules = get_all_instances_in_courses('folder', array($course1->id => $course1, $course2->id => $course2));
+        $this->assertCount(3, $modules);
+
+        foreach ($modules as $cm) {
+            if ($folder1a->cmid == $cm->coursemodule) {
+                $folder = $folder1a;
+            } else if ($folder1b->cmid == $cm->coursemodule) {
+                $folder = $folder1b;
+            } else if ($folder2->cmid == $cm->coursemodule) {
+                $folder = $folder2;
+            } else {
+                $this->fail('Unexpected cm'. $cm->coursemodule);
+            }
+            $this->assertSame($folder->name, $cm->name);
+            $this->assertSame($folder->course, $cm->course);
+        }
+
+        try {
+            get_all_instances_in_courses('a b', array($course1->id => $course1, $course2->id => $course2));
+            $this->fail('coding_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        try {
+            get_all_instances_in_courses('', array($course1->id => $course1, $course2->id => $course2));
+            $this->fail('coding_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+    }
+
+    public function test_get_all_instances_in_course() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser(); // Some generators have bogus access control.
+
+        $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php");
+        $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php");
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+
+        $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3));
+        $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1));
+        $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1));
+
+        $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2));
+        $glossary2a = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
+        $glossary2b = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
+
+        $folder3 = $this->getDataGenerator()->create_module('folder', array('course' => $course3));
+
+        $modules = get_all_instances_in_course('folder', $course1);
+        $this->assertCount(2, $modules);
+
+        foreach ($modules as $cm) {
+            if ($folder1a->cmid == $cm->coursemodule) {
+                $folder = $folder1a;
+            } else if ($folder1b->cmid == $cm->coursemodule) {
+                $folder = $folder1b;
+            } else {
+                $this->fail('Unexpected cm'. $cm->coursemodule);
+            }
+            $this->assertSame($folder->name, $cm->name);
+            $this->assertSame($folder->course, $cm->course);
+        }
+
+        try {
+            get_all_instances_in_course('a b', $course1);
+            $this->fail('coding_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        try {
+            get_all_instances_in_course('', $course1);
+            $this->fail('coding_exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+    }
 }
index c29e7f8..bb81535 100644 (file)
@@ -214,4 +214,32 @@ class core_scheduled_task_testcase extends advanced_testcase {
         $task = \core\task\manager::get_next_scheduled_task($now);
         $this->assertNull($task);
     }
+
+    public function test_get_broken_scheduled_task() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+        // Delete all existing scheduled tasks.
+        $DB->delete_records('task_scheduled');
+        // Add a scheduled task.
+
+        // A broken task that runs all the time.
+        $record = new stdClass();
+        $record->blocking = true;
+        $record->minute = '*';
+        $record->hour = '*';
+        $record->dayofweek = '*';
+        $record->day = '*';
+        $record->month = '*';
+        $record->component = 'test_scheduled_task';
+        $record->classname = '\core\task\scheduled_test_task_broken';
+
+        $DB->insert_record('task_scheduled', $record);
+
+        $now = time();
+        // Should not get any task.
+        $task = \core\task\manager::get_next_scheduled_task($now);
+        $this->assertDebuggingCalled();
+        $this->assertNull($task);
+    }
 }
index e460e3d..31c7efc 100644 (file)
@@ -131,6 +131,10 @@ class assign_submission_file extends assign_submission_plugin {
                                 'maxfiles'=>$this->get_config('maxfilesubmissions'),
                                 'accepted_types'=>'*',
                                 'return_types'=>FILE_INTERNAL);
+        if ($fileoptions['maxbytes'] == 0) {
+            // Use module default.
+            $fileoptions['maxbytes'] = get_config('assignsubmission_file', 'maxbytes');
+        }
         return $fileoptions;
     }
 
index fc6a8ae..955b3cd 100644 (file)
@@ -63,6 +63,7 @@
 #page-mod-data-templates .template_heading {
      text-align:center;
 }
+#page-mod-data-templates #availabletags_wrapper {max-width:250px;}
 
 .dir-rtl .mod-data-default-template .template-field {text-align:left;}
 .dir-rtl .mod-data-default-template .template-token {text-align:right;}
index 236c23b..33fbf5b 100644 (file)
@@ -234,7 +234,7 @@ if ($mode != 'csstemplate' and $mode != 'jstemplate') {
     echo $OUTPUT->help_icon('availabletags', 'data');
     echo '<br />';
 
-
+    echo '<div class="no-overflow" id="availabletags_wrapper">';
     echo '<select name="fields1[]" id="availabletags" size="12" onclick="insert_field_tags(this)">';
 
     $fields = $DB->get_records('data_fields', array('dataid'=>$data->id));
@@ -291,6 +291,7 @@ if ($mode != 'csstemplate' and $mode != 'jstemplate') {
     }
 
     echo '</select>';
+    echo '</div>';
     echo '<br /><br /><br /><br /><input type="submit" name="defaultform" value="'.get_string('resettemplate','data').'" />';
     echo '<br /><br />';
     if ($usehtmleditor) {
diff --git a/mod/forum/classes/task/cron_task.php b/mod/forum/classes/task/cron_task.php
new file mode 100644 (file)
index 0000000..f232c62
--- /dev/null
@@ -0,0 +1,48 @@
+<?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/>.
+
+/**
+ * A scheduled task for forum cron.
+ *
+ * @todo MDL-44734 This job will be split up properly.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_forum\task;
+
+class cron_task extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('crontask', 'mod_forum');
+    }
+
+    /**
+     * Run forum cron.
+     */
+    public function execute() {
+        global $CFG;
+        require_once($CFG->dirroot . '/mod/forum/lib.php');
+        forum_cron();
+    }
+
+}
diff --git a/mod/forum/db/tasks.php b/mod/forum/db/tasks.php
new file mode 100644 (file)
index 0000000..03ad9e0
--- /dev/null
@@ -0,0 +1,38 @@
+<?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/>.
+
+/**
+ * Definition of Forum scheduled tasks.
+ *
+ * @package   mod_forum
+ * @category  task
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+    array(
+        'classname' => 'mod_forum\task\cron_task',
+        'blocking' => 0,
+        'minute' => '*',
+        'hour' => '*',
+        'day' => '*',
+        'month' => '*',
+        'dayofweek' => '*'
+    )
+);
index 1c6804f..4eed8db 100644 (file)
@@ -110,6 +110,7 @@ $string['confirmunsubscribe'] = 'Do you really want to unsubscribe from forum \'
 $string['couldnotadd'] = 'Could not add your post due to an unknown error';
 $string['couldnotdeletereplies'] = 'Sorry, that cannot be deleted as people have already responded to it';
 $string['couldnotupdate'] = 'Could not update your post due to an unknown error';
+$string['crontask'] = 'Forum mailings and maintenance jobs';
 $string['delete'] = 'Delete';
 $string['deleteddiscussion'] = 'The discussion topic has been deleted';
 $string['deletedpost'] = 'The post has been deleted';
index bc9e144..5fdada9 100644 (file)
@@ -434,18 +434,15 @@ function forum_cron_minimise_user_record(stdClass $user) {
 }
 
 /**
- * Function to be run periodically according to the moodle cron
+ * Function to be run periodically according to the scheduled task.
+ *
  * Finds all posts that have yet to be mailed out, and mails them
- * out to all subscribers
+ * out to all subscribers as well as other maintance tasks.
  *
- * @global object
- * @global object
- * @global object
- * @uses CONTEXT_MODULE
- * @uses CONTEXT_COURSE
- * @uses SITEID
- * @uses FORMAT_PLAIN
- * @return void
+ * NOTE: Since 2.7.2 this function is run by scheduled task rather
+ * than standard cron.
+ *
+ * @todo MDL-44734 The function will be split up into seperate tasks.
  */
 function forum_cron() {
     global $CFG, $USER, $DB;
index 12037b7..b554c76 100644 (file)
@@ -24,7 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014081900;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2014082100;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2014050800;       // Requires this Moodle version
 $plugin->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)
-$plugin->cron      = 60;
index 0214f51..caea254 100644 (file)
@@ -75,12 +75,12 @@ class behat_question extends behat_question_base {
 
         // Split in two checkings to give more feedback in case of exception.
         $exception = new ElementNotFoundException($this->getSession(), 'Question "' . $questiondescription . '" ');
-        $questionxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' qtext ')][contains(., {$questiondescriptionliteral})]";
+        $questionxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' que ')]" .
+                "[contains(div[@class='content']/div[@class='formulation'], {$questiondescriptionliteral})]";
         $this->find('xpath', $questionxpath, $exception);
 
         $exception = new ExpectationException('Question "' . $questiondescription . '" state is not "' . $state . '"', $this->getSession());
-        $xpath = $questionxpath . "/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' que ')]" .
-            "/descendant::div[@class='state'][contains(., {$stateliteral})]";
+        $xpath = $questionxpath . "/div[@class='info']/div[@class='state' and contains(., {$stateliteral})]";
         $this->find('xpath', $xpath, $exception);
     }
 }
index f251337..f9be863 100644 (file)
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
 require_once($CFG->dirroot . '/question/type/calculated/question.php');
+require_once($CFG->dirroot . '/question/type/calculated/questiontype.php');
 
 
 /**
index e88099b..d5f096c 100644 (file)
 .gradetreebox #gradetreesubmit {margin-bottom:1em;text-align: center;}
 .gradetreebox .hidden {display: none;}
 
-/*adjustments to grader report (with static student cols) to make things line up in IE and Safari*/
-#page-grade-report-grader-index .right_scroller #user-grades td {padding-top:0;padding-bottom:2px;}
-#page-grade-report-grader-index #fixed_column td {padding-top:0;padding-bottom:2px;}
-
 /** Advanced grading **/
 #page-grade-grading-manage #activemethodselector {text-align:center;margin-bottom:1em;}
 #page-grade-grading-manage #activemethodselector select {margin:0px 1em;}
 #page-grade-edit-outcome-course .courseoutcomes td {
     text-align:center;
 }
-
-#page-grade-report-grader-index .topscrollcontent {
-    height: 1px;
-}
-
-#page-grade-report-grader-index #user-grades {
-    margin-top: 4px;
-}
index c98eef3..c3ca9e6 100644 (file)
Binary files a/theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap-debug.js and b/theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap-debug.js differ
index c3a51d1..5c05647 100644 (file)
Binary files a/theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap-min.js and b/theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap-min.js differ
index e6691f0..927d338 100644 (file)
Binary files a/theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap.js and b/theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap.js differ
index 4de3103..1ab5f55 100644 (file)
@@ -98,6 +98,6 @@ NS.setup_toggle_show = function() {
 NS.toggle_show = function(e) {
     // Toggle the active class on both the clicked .btn-navbar and the .nav-collapse.
     // Our CSS will set the height for these.
-    Y.one(SELECTORS.NAV_COLLAPSE).toggleClass(CSS.ACTIVE);
+    Y.all(SELECTORS.NAV_COLLAPSE).toggleClass(CSS.ACTIVE);
     e.currentTarget.toggleClass(CSS.ACTIVE);
 };