Merge branch '46518-28' of git://github.com/samhemelryk/moodle
authorDan Poltawski <dan@moodle.com>
Fri, 24 Oct 2014 14:16:55 +0000 (15:16 +0100)
committerDan Poltawski <dan@moodle.com>
Fri, 24 Oct 2014 14:16:55 +0000 (15:16 +0100)
177 files changed:
admin/settings/grades.php
admin/tool/langimport/classes/event/langpack_imported.php
admin/tool/langimport/classes/event/langpack_removed.php
admin/tool/langimport/classes/event/langpack_updated.php
admin/tool/monitor/classes/event/rule_created.php [new file with mode: 0644]
admin/tool/monitor/classes/event/rule_deleted.php [new file with mode: 0644]
admin/tool/monitor/classes/event/rule_updated.php [new file with mode: 0644]
admin/tool/monitor/classes/event/subscription_created.php [new file with mode: 0644]
admin/tool/monitor/classes/event/subscription_criteria_met.php [new file with mode: 0644]
admin/tool/monitor/classes/event/subscription_deleted.php [new file with mode: 0644]
admin/tool/monitor/classes/eventobservers.php
admin/tool/monitor/classes/output/managerules/renderable.php
admin/tool/monitor/classes/output/managerules/renderer.php
admin/tool/monitor/classes/output/managesubs/renderer.php
admin/tool/monitor/classes/output/managesubs/rules.php
admin/tool/monitor/classes/rule_manager.php
admin/tool/monitor/classes/subscription_manager.php
admin/tool/monitor/classes/task/clean_events.php [new file with mode: 0644]
admin/tool/monitor/db/tasks.php [new file with mode: 0644]
admin/tool/monitor/index.php
admin/tool/monitor/lang/en/tool_monitor.php
admin/tool/monitor/lib.php
admin/tool/monitor/managerules.php
admin/tool/monitor/tests/behat/subscription.feature
admin/tool/monitor/tests/eventobservers_test.php
admin/tool/monitor/tests/events_test.php [new file with mode: 0644]
admin/tool/monitor/tests/rule_manager_test.php
admin/tool/monitor/tests/task_clean_events_test.php [new file with mode: 0644]
admin/tool/monitor/version.php
admin/tool/uploadcourse/tests/course_test.php
admin/tool/uploadcourse/tests/processor_test.php
auth/db/tests/db_test.php
availability/condition/profile/tests/condition_test.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_test.php
cache/stores/mongodb/lib.php
calendar/lib.php
course/moodleform_mod.php
course/tests/courselib_test.php
course/tests/externallib_test.php
course/view.php
enrol/database/tests/sync_test.php
grade/edit/letter/edit_form.php
grade/edit/letter/index.php
grade/edit/tree/category_form.php
grade/edit/tree/lib.php
grade/edit/tree/outcomeitem_form.php
grade/lib.php
grade/querylib.php
grade/report/grader/index.php
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js
grade/report/grader/yui/src/gradereporttable/js/floatingheaders.js
grade/report/history/classes/output/tablelog.php
grade/report/history/index.php
grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-debug.js
grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-min.js
grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector.js
grade/report/history/yui/src/userselector/js/userselector.js
grade/report/lib.php
grade/report/singleview/classes/local/screen/grade.php
grade/report/singleview/classes/local/screen/user.php
grade/report/singleview/lang/en/gradereport_singleview.php
grade/report/singleview/tests/behat/singleview.feature
grade/report/user/lib.php
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_calculated_weights.feature
grade/tests/behat/grade_contribution_with_extra_credit.feature [new file with mode: 0644]
grade/tests/behat/grade_mingrade.feature
grade/tests/behat/grade_natural_normalisation.feature
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_single_item_scales.feature
grade/tests/report_graderlib_test.php
group/assign.php
install/lang/no/error.php
install/lang/no/install.php
install/lang/ru/error.php
lang/en/grades.php
lang/en/rating.php
lib/adminlib.php
lib/classes/event/calendar_event_updated.php
lib/classes/grades_external.php
lib/classes/task/manager.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/editor/atto/plugins/accessibilitychecker/tests/behat/accessibilitychecker.feature
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/autosave.js
lib/filelib.php
lib/form/modgrade.php
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-debug.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-min.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector.js
lib/form/yui/src/dateselector/js/calendar.js
lib/grade/grade_category.php
lib/grade/grade_grade.php
lib/grade/grade_item.php
lib/grade/tests/fixtures/lib.php
lib/grade/tests/grade_item_test.php
lib/gradelib.php
lib/modinfolib.php
lib/navigationlib.php
lib/testing/classes/util.php
lib/tests/admintree_test.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/event_user_graded_test.php
lib/tests/questionlib_test.php
lib/tests/scheduled_task_test.php
lib/upgrade.txt
lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js
lib/yui/build/moodle-core-dock/moodle-core-dock-min.js
lib/yui/build/moodle-core-dock/moodle-core-dock.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/dock/js/dock.js
lib/yui/src/notification/js/dialogue.js
mod/assign/feedback/editpdf/classes/page_editor.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/feedback/editpdf/tests/editpdf_test.php
mod/assign/tests/locallib_test.php
mod/forum/discuss.php
mod/forum/subscribe_ajax.php
mod/forum/tests/behat/my_forum_posts.feature [new file with mode: 0644]
mod/forum/tests/mail_test.php
mod/lesson/db/access.php
mod/lesson/essay.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/renderer.php
mod/lesson/tabs.php
mod/lesson/tests/behat/lesson_informations_at_end.feature [new file with mode: 0644]
mod/lesson/tests/behat/teacher_grade_essays.feature [new file with mode: 0644]
mod/lesson/version.php
mod/lti/db/upgradelib.php
mod/quiz/attemptlib.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/db/events.php
mod/quiz/locallib.php
mod/quiz/report/responses/first_or_all_responses_table.php
mod/quiz/styles.css
mod/quiz/tests/attempt_walkthrough_test.php
mod/quiz/tests/generator/lib.php
mod/quiz/upgrade.txt
mod/quiz/version.php
mod/scorm/report.php
question/engine/datalib.php
question/engine/questionattempt.php
question/engine/questionattemptstep.php
question/engine/tests/datalib_reporting_queries_test.php [moved from question/engine/tests/question_engine_data_mapper_test.php with 92% similarity]
question/engine/tests/datalib_test.php [new file with mode: 0644]
question/tests/behat/edit_questions.feature
question/tests/behat/preview_question.feature
question/type/questiontypebase.php
question/type/random/questiontype.php
report/participation/index.php
theme/base/style/calendar.css
theme/base/style/core.css
theme/base/style/course.css
theme/base/style/grade.css
theme/bootstrapbase/layout/popup.php
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/style/moodle.css
user/editadvanced.php
user/index.php
user/profile.php
version.php

index 84a5296..15a429e 100644 (file)
@@ -112,8 +112,6 @@ if (has_capability('moodle/grade:manage', $systemcontext)
         $defaults = array('value'=>0, 'forced'=>false, 'adv'=>true);
         $temp->add(new admin_setting_gradecat_combo('grade_aggregateoutcomes', new lang_string('aggregateoutcomes', 'grades'),
                     new lang_string('aggregateoutcomes_help', 'grades'), $defaults, $options));
-        $temp->add(new admin_setting_gradecat_combo('grade_aggregatesubcats', new lang_string('aggregatesubcats', 'grades'),
-                    new lang_string('aggregatesubcats_help', 'grades'), $defaults, $options));
 
         $options = array(0 => new lang_string('none'));
         for ($i=1; $i<=20; $i++) {
index c8bd149..5967929 100644 (file)
@@ -36,6 +36,7 @@ defined('MOODLE_INTERNAL') || die();
  * }
  *
  * @package    tool_langimport
+ * @since      Moodle 2.8
  * @copyright  2014 Dan Poltawski <dan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
index 82e5270..e528da4 100644 (file)
@@ -36,6 +36,7 @@ defined('MOODLE_INTERNAL') || die();
  * }
  *
  * @package    tool_langimport
+ * @since      Moodle 2.8
  * @copyright  2014 Dan Poltawski <dan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
index 4f0528f..b9d026a 100644 (file)
@@ -36,6 +36,7 @@ defined('MOODLE_INTERNAL') || die();
  * }
  *
  * @package    tool_langimport
+ * @since      Moodle 2.8
  * @copyright  2014 Dan Poltawski <dan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
diff --git a/admin/tool/monitor/classes/event/rule_created.php b/admin/tool/monitor/classes/event/rule_created.php
new file mode 100644 (file)
index 0000000..a5d6fe1
--- /dev/null
@@ -0,0 +1,77 @@
+<?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 tool_monitor rule created event.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_monitor rule created event class.
+ *
+ * @package    tool_monitor
+ * @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 rule_created extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_monitor_rules';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventrulecreated', 'tool_monitor');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' created the event monitor rule with id '$this->objectid'.";
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/tool/monitor/edit.php', array('ruleid' => $this->objectid,
+            'courseid' => $this->courseid));
+    }
+}
diff --git a/admin/tool/monitor/classes/event/rule_deleted.php b/admin/tool/monitor/classes/event/rule_deleted.php
new file mode 100644 (file)
index 0000000..988a5b6
--- /dev/null
@@ -0,0 +1,76 @@
+<?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 tool_monitor rule deleted event.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_monitor rule deleted event class.
+ *
+ * @package    tool_monitor
+ * @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 rule_deleted extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_monitor_rules';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventruledeleted', 'tool_monitor');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' deleted the event monitor rule with id '$this->objectid'.";
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/tool/monitor/managerules.php', array('courseid' => $this->courseid));
+    }
+}
diff --git a/admin/tool/monitor/classes/event/rule_updated.php b/admin/tool/monitor/classes/event/rule_updated.php
new file mode 100644 (file)
index 0000000..c17da2d
--- /dev/null
@@ -0,0 +1,77 @@
+<?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 tool_monitor rule updated event.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_monitor rule updated event class.
+ *
+ * @package    tool_monitor
+ * @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 rule_updated extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_monitor_rules';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventruleupdated', 'tool_monitor');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' updated the event monitor rule with id '$this->objectid'.";
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/tool/monitor/edit.php', array('ruleid' => $this->objectid,
+            'courseid' => $this->courseid));
+    }
+}
diff --git a/admin/tool/monitor/classes/event/subscription_created.php b/admin/tool/monitor/classes/event/subscription_created.php
new file mode 100644 (file)
index 0000000..e3d2b81
--- /dev/null
@@ -0,0 +1,67 @@
+<?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 tool_monitor subscription created event.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_monitor subscription created event class.
+ *
+ * @package    tool_monitor
+ * @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 subscription_created extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_monitor_subscriptions';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventsubcreated', 'tool_monitor');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' created the event monitor subscription with id '$this->objectid'.";
+    }
+}
diff --git a/admin/tool/monitor/classes/event/subscription_criteria_met.php b/admin/tool/monitor/classes/event/subscription_criteria_met.php
new file mode 100644 (file)
index 0000000..693bcf4
--- /dev/null
@@ -0,0 +1,86 @@
+<?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 tool_monitor subscription criteria met event.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_monitor subscription criteria met event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string subscriptionid: id of the subscription.
+ * }
+ *
+ * @package    tool_monitor
+ * @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 subscription_criteria_met extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventsubcriteriamet', 'tool_monitor');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The criteria for the subscription with id '{$this->other['subscriptionid']}' was met.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['subscriptionid'])) {
+            throw new \coding_exception('The \'subscriptionid\' value must be set in other.');
+        }
+    }
+}
diff --git a/admin/tool/monitor/classes/event/subscription_deleted.php b/admin/tool/monitor/classes/event/subscription_deleted.php
new file mode 100644 (file)
index 0000000..e4b2bc0
--- /dev/null
@@ -0,0 +1,67 @@
+<?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 tool_monitor subscription deleted event.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_monitor subscription deleted event class.
+ *
+ * @package    tool_monitor
+ * @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 subscription_deleted extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_monitor_subscriptions';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventsubdeleted', 'tool_monitor');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' deleted the event monitor subscription with id '$this->objectid'.";
+    }
+}
index 27d3d29..2ffc4fd 100644 (file)
@@ -54,8 +54,12 @@ class eventobservers {
      */
     public static function course_deleted(\core\event\course_deleted $event) {
         $rules = rule_manager::get_rules_by_courseid($event->courseid);
+        $context = null;
+        if ($event->contextlevel == CONTEXT_COURSE) {
+            $context = $event->get_context();
+        }
         foreach ($rules as $rule) {
-            rule_manager::delete_rule($rule->id);
+            rule_manager::delete_rule($rule->id, $context);
         }
     }
 
@@ -156,6 +160,30 @@ class eventobservers {
                 $count = $DB->count_records_sql($sql, $params);
                 if (!empty($count) && $count >= $subscription->frequency) {
                     $idstosend[] = $subscription->id;
+
+                    // Trigger a subscription_criteria_met event.
+                    // It's possible that the course has been deleted since the criteria was met, so in that case use
+                    // the system context. Set it here and change later if needed.
+                    $context = \context_system::instance();
+                    // We can't perform if (!empty($subscription->courseid)) below as it uses the magic method
+                    // __get to return the variable, which will always result in being empty.
+                    $courseid = $subscription->courseid;
+                    if (!empty($courseid)) {
+                        if ($coursecontext = \context_course::instance($courseid, IGNORE_MISSING)) {
+                            $context = $coursecontext;
+                        }
+                    }
+
+                    $params = array(
+                        'userid' => $subscription->userid,
+                        'courseid' => $subscription->courseid,
+                        'context' => $context,
+                        'other' => array(
+                            'subscriptionid' => $subscription->id
+                        )
+                    );
+                    $event = \tool_monitor\event\subscription_criteria_met::create($params);
+                    $event->trigger();
                 }
             }
             if (!empty($idstosend)) {
index 2c74245..0819aae 100644 (file)
@@ -154,7 +154,7 @@ class renderable extends \table_sql implements \renderable {
         $manage = '';
         // We don't need to check for capability at course level since, user is never shown this page,
         // if he doesn't have the capability.
-        if ($this->hassystemcap || ($rule->courseid !== 0)) {
+        if ($this->hassystemcap || ($rule->courseid != 0)) {
             // There might be site rules which the user can not manage.
             $editurl = new \moodle_url($CFG->wwwroot. '/admin/tool/monitor/edit.php', array('ruleid' => $rule->id,
                     'courseid' => $rule->courseid, 'sesskey' => sesskey()));
@@ -175,7 +175,7 @@ class renderable extends \table_sql implements \renderable {
             $icon = $OUTPUT->action_link($deleteurl, new \pix_icon('t/delete', get_string('deleterule', 'tool_monitor')), $action);
             $manage .= $icon;
         } else {
-            $manage = '-';
+            $manage = get_string('nopermission', 'tool_monitor');
         }
         return $manage;
     }
index 889817d..5eaed9f 100644 (file)
@@ -68,7 +68,7 @@ class renderer extends \plugin_renderer_base {
     }
 
     /**
-     * Html to add a button for adding a new rul.
+     * Html to add a button for adding a new rule.
      *
      * @param int $courseid course id.
      *
@@ -81,4 +81,19 @@ class renderer extends \plugin_renderer_base {
         $addurl = new \moodle_url($CFG->wwwroot. '/admin/tool/monitor/edit.php', array('courseid' => $courseid));
         return \html_writer::link($addurl, $button);
     }
+
+    /**
+     * Html to add a link to go to the subscription page.
+     *
+     * @param moodle_url $manageurl The url of the subscription page.
+     *
+     * @return string html for the link to the subscription page.
+     */
+    public function render_subscriptions_link($manageurl) {
+        echo \html_writer::start_div();
+        $a = \html_writer::link($manageurl, get_string('managesubscriptions', 'tool_monitor'));
+        $link = \html_writer::tag('span', get_string('managesubscriptionslink', 'tool_monitor', $a));
+        echo $link;
+        echo \html_writer::end_div();
+    }
 }
index fe3f8e0..30d6b66 100644 (file)
@@ -71,8 +71,9 @@ class renderer extends \plugin_renderer_base {
      * @return string to display on the mangesubs page.
      */
     protected function render_course_select(rules $renderable) {
-        $select = $renderable->get_user_courses_select();
-        return $this->render($select);;
+        if ($select = $renderable->get_user_courses_select()) {
+            return $this->render($select);
+        }
     }
 
     /**
@@ -91,4 +92,19 @@ class renderer extends \plugin_renderer_base {
 
         return $o;
     }
+
+    /**
+     * Html to add a link to go to the rule manager page.
+     *
+     * @param moodle_url $ruleurl The url of the rule manager page.
+     *
+     * @return string html for the link to the rule manager page.
+     */
+    public function render_rules_link($ruleurl) {
+        echo \html_writer::start_div();
+        $a = \html_writer::link($ruleurl, get_string('managerules', 'tool_monitor'));
+        $link = \html_writer::tag('span', get_string('manageruleslink', 'tool_monitor', $a));
+        echo $link;
+        echo \html_writer::end_div();
+    }
 }
index f891e90..debe732 100644 (file)
@@ -154,14 +154,24 @@ class rules extends \table_sql implements \renderable {
     /**
      * Gets a list of courses where the current user can subscribe to rules as a dropdown.
      *
-     * @return \single_select list of courses.
+     * @return \single_select|bool returns the list of courses, or false if the select box
+     *      should not be displayed.
      */
     public function get_user_courses_select() {
-        $courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname');
+        global $DB;
+
+        // If the number of courses on the site exceed the maximum drop down limit do not display the select box.
+        $numcourses = $DB->count_records('course');
+        if ($numcourses > COURSE_MAX_COURSES_PER_DROPDOWN) {
+            return false;
+        }
+
         $options = array(0 => get_string('site'));
-        $systemcontext = \context_system::instance();
-        foreach ($courses as $course) {
-            $options[$course->id] = format_text($course->fullname, array('context' => $systemcontext));
+        if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname')) {
+            foreach ($courses as $course) {
+                $options[$course->id] = format_string($course->fullname, true,
+                    array('context' => \context_course::instance($course->id)));
+            }
         }
         $url = new \moodle_url('/admin/tool/monitor/index.php');
         $select = new \single_select($url, 'courseid', $options, $this->courseid);
index 864451c..b0c60e0 100644 (file)
@@ -49,6 +49,26 @@ class rule_manager {
         $ruledata->timemodified = $now;
 
         $ruledata->id = $DB->insert_record('tool_monitor_rules', $ruledata);
+
+        // Trigger a rule created event.
+        if ($ruledata->id) {
+            if (!empty($ruledata->courseid)) {
+                $courseid = $ruledata->courseid;
+                $context = \context_course::instance($ruledata->courseid);
+            } else {
+                $courseid = 0;
+                $context = \context_system::instance();
+            }
+
+            $params = array(
+                'objectid' => $ruledata->id,
+                'courseid' => $courseid,
+                'context' => $context
+            );
+            $event = \tool_monitor\event\rule_created::create($params);
+            $event->trigger();
+        }
+
         return new rule($ruledata);
     }
 
@@ -85,14 +105,47 @@ class rule_manager {
      * Delete a rule and associated subscriptions, by rule id.
      *
      * @param int $ruleid id of rule to be deleted.
+     * @param \context|null $coursecontext the context of the course - this is passed when we
+     *      can not get the context via \context_course as the course has been deleted.
      *
      * @return bool
      */
-    public static function delete_rule($ruleid) {
+    public static function delete_rule($ruleid, $coursecontext = null) {
         global $DB;
 
-        subscription_manager::remove_all_subscriptions_for_rule($ruleid);
-        return $DB->delete_records('tool_monitor_rules', array('id' => $ruleid));
+        subscription_manager::remove_all_subscriptions_for_rule($ruleid, $coursecontext);
+
+        // Retrieve the rule from the DB before we delete it, so we have a record when we trigger a rule deleted event.
+        $rule = $DB->get_record('tool_monitor_rules', array('id' => $ruleid));
+
+        $success = $DB->delete_records('tool_monitor_rules', array('id' => $ruleid));
+
+        // If successful trigger a rule deleted event.
+        if ($success) {
+            // It is possible that we are deleting rules associated with a deleted course, so we should be
+            // passing the context as the second parameter.
+            if (!is_null($coursecontext)) {
+                $context = $coursecontext;
+                $courseid = $rule->courseid;
+            } else if (!empty($rule->courseid) && ($context = \context_course::instance($rule->courseid,
+                    IGNORE_MISSING))) {
+                $courseid = $rule->courseid;
+            } else {
+                $courseid = 0;
+                $context = \context_system::instance();
+            }
+
+            $params = array(
+                'objectid' => $rule->id,
+                'courseid' => $courseid,
+                'context' => $context
+            );
+            $event = \tool_monitor\event\rule_deleted::create($params);
+            $event->add_record_snapshot('tool_monitor_rules', $rule);
+            $event->trigger();
+        }
+
+        return $success;
     }
 
     /**
@@ -127,7 +180,34 @@ class rule_manager {
             throw new \coding_exception('Invalid rule ID.');
         }
         $ruledata->timemodified = time();
-        return $DB->update_record('tool_monitor_rules', $ruledata);
+
+        $success = $DB->update_record('tool_monitor_rules', $ruledata);
+
+        // If successful trigger a rule updated event.
+        if ($success) {
+            // If we do not have the course id we need to retrieve it.
+            if (!isset($ruledata->courseid)) {
+                $courseid = $DB->get_field('tool_monitor_rules', 'courseid', array('id' => $ruledata->id), MUST_EXIST);
+            } else {
+                $courseid = $ruledata->courseid;
+            }
+
+            if (!empty($courseid)) {
+                $context = \context_course::instance($courseid);
+            } else {
+                $context = \context_system::instance();
+            }
+
+            $params = array(
+                'objectid' => $ruledata->id,
+                'courseid' => $courseid,
+                'context' => $context
+            );
+            $event = \tool_monitor\event\rule_updated::create($params);
+            $event->trigger();
+        }
+
+        return $success;
     }
 
     /**
index 82796a3..ba2236b 100644 (file)
@@ -59,7 +59,28 @@ class subscription_manager {
         }
 
         $subscription->timecreated = time();
-        return $DB->insert_record('tool_monitor_subscriptions', $subscription);
+        $subscription->id = $DB->insert_record('tool_monitor_subscriptions', $subscription);
+
+        // Trigger a subscription created event.
+        if ($subscription->id) {
+            if (!empty($subscription->courseid)) {
+                $courseid = $subscription->courseid;
+                $context = \context_course::instance($subscription->courseid);
+            } else {
+                $courseid = 0;
+                $context = \context_system::instance();
+            }
+
+            $params = array(
+                'objectid' => $subscription->id,
+                'courseid' => $courseid,
+                'context' => $context
+            );
+            $event = \tool_monitor\event\subscription_created::create($params);
+            $event->trigger();
+        }
+
+        return $subscription->id;
     }
 
     /**
@@ -81,7 +102,33 @@ class subscription_manager {
         if ($checkuser && $subscription->userid != $USER->id) {
             throw new \coding_exception('Invalid subscription supplied');
         }
-        return $DB->delete_records('tool_monitor_subscriptions', array('id' => $subscription->id));
+
+        // Store the subscription before we delete it.
+        $subscription = $DB->get_record('tool_monitor_subscriptions', array('id' => $subscription->id));
+
+        $success = $DB->delete_records('tool_monitor_subscriptions', array('id' => $subscription->id));
+
+        // If successful trigger a subscription_deleted event.
+        if ($success) {
+            if (!empty($subscription->courseid)) {
+                $courseid = $subscription->courseid;
+                $context = \context_course::instance($subscription->courseid);
+            } else {
+                $courseid = 0;
+                $context = \context_system::instance();
+            }
+
+            $params = array(
+                'objectid' => $subscription->id,
+                'courseid' => $courseid,
+                'context' => $context
+            );
+            $event = \tool_monitor\event\subscription_deleted::create($params);
+            $event->add_record_snapshot('tool_monitor_subscriptions', $subscription);
+            $event->trigger();
+        }
+
+        return $success;
     }
 
     /**
@@ -112,12 +159,49 @@ class subscription_manager {
      * Delete all subscribers for a given rule.
      *
      * @param int $ruleid rule id.
+     * @param \context|null $coursecontext the context of the course - this is passed when we
+     *      can not get the context via \context_course as the course has been deleted.
      *
      * @return bool
      */
-    public static function remove_all_subscriptions_for_rule($ruleid) {
+    public static function remove_all_subscriptions_for_rule($ruleid, $coursecontext = null) {
         global $DB;
-        return $DB->delete_records('tool_monitor_subscriptions', array('ruleid' => $ruleid));
+
+        // Store all the subscriptions we have to delete.
+        $subscriptions = $DB->get_recordset('tool_monitor_subscriptions', array('ruleid' => $ruleid));
+
+        // Now delete them.
+        $success = $DB->delete_records('tool_monitor_subscriptions', array('ruleid' => $ruleid));
+
+        // If successful and there were subscriptions that were deleted trigger a subscription deleted event.
+        if ($success && $subscriptions) {
+            foreach ($subscriptions as $subscription) {
+                // It is possible that we are deleting rules associated with a deleted course, so we should be
+                // passing the context as the second parameter.
+                if (!is_null($coursecontext)) {
+                    $context = $coursecontext;
+                    $courseid = $subscription->courseid;
+                } else if (!empty($subscription->courseid) && ($coursecontext =
+                        \context_course::instance($subscription->courseid, IGNORE_MISSING))) {
+                    $courseid = $subscription->courseid;
+                    $context = $coursecontext;
+                } else {
+                    $courseid = 0;
+                    $context = \context_system::instance();
+                }
+
+                $params = array(
+                    'objectid' => $subscription->id,
+                    'courseid' => $courseid,
+                    'context' => $context
+                );
+                $event = \tool_monitor\event\subscription_deleted::create($params);
+                $event->add_record_snapshot('tool_monitor_subscriptions', $subscription);
+                $event->trigger();
+            }
+        }
+
+        return $success;
     }
 
     /**
diff --git a/admin/tool/monitor/classes/task/clean_events.php b/admin/tool/monitor/classes/task/clean_events.php
new file mode 100644 (file)
index 0000000..9efb360
--- /dev/null
@@ -0,0 +1,142 @@
+<?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/>.
+
+/**
+ * Clean the tool_monitor_events table.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\task;
+
+/**
+ * Simple task to clean the tool_monitor_events table.
+ */
+class clean_events extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('taskcleanevents', 'tool_monitor');
+    }
+
+    /**
+     * Performs the cleaning of events.
+     */
+    public function execute() {
+        global $DB;
+
+        // Array to store which events have been triggered in which course.
+        $courses = array();
+
+        // Firstly, let's go through the site wide rules. There may be multiple rules for the site that rely on
+        // the same event being triggered, so we only remove the events when they reach the max timewindow.
+        if ($siterules = $DB->get_recordset('tool_monitor_rules', array('courseid' => 0), 'timewindow DESC')) {
+            // Go through each rule and check if there are any events we can remove.
+            foreach ($siterules as $rule) {
+                // Check if we have already processed this event.
+                if (isset($courses[$rule->courseid][$rule->eventname])) {
+                    continue;
+                }
+                // Store the timewindow for this event.
+                $courses[$rule->courseid][$rule->eventname] = $rule->timewindow;
+                // Delete any events that may exist that have exceeded the timewindow.
+                $DB->delete_records_select('tool_monitor_events', 'eventname = :eventname AND
+                    courseid = :courseid AND timecreated <= :timewindow',
+                    array('eventname' => $rule->eventname, 'courseid' => $rule->courseid,
+                        'timewindow' => time() - $rule->timewindow));
+            }
+            // Free resources.
+            $siterules->close();
+        }
+
+        // Now, get the course rules. The same applies here - there may be multiple rules for the course that rely on
+        // the same event being triggered, so we only remove the events when they reach the max timewindow.
+        if ($rules = $DB->get_recordset_select('tool_monitor_rules', 'courseid != 0', array(), 'timewindow DESC')) {
+            // Go through each rule and check if there are any events we can remove.
+            foreach ($rules as $rule) {
+                // Check if we have already processed this event for this particular course.
+                if (isset($courses[$rule->courseid][$rule->eventname])) {
+                    continue;
+                }
+                // Add the course and event to the list.
+                $courses[$rule->courseid][$rule->eventname] = $rule->timewindow;
+                // If there is a site wide rule listening for this event do not remove it unless the maximum
+                // timewindow between the two has exceeded.
+                $timewindow = $rule->timewindow;
+                if (isset($courses[0][$rule->eventname]) && ($courses[0][$rule->eventname] > $timewindow)) {
+                    $timewindow = $courses[0][$rule->eventname];
+                }
+                // Delete any events that may exist that have exceeded the timewindow.
+                $DB->delete_records_select('tool_monitor_events', 'eventname = :eventname AND
+                    courseid = :courseid AND timecreated <= :timewindow',
+                        array('eventname' => $rule->eventname, 'courseid' => $rule->courseid,
+                            'timewindow' => time() - $timewindow));
+            }
+            // Free resources.
+            $rules->close();
+        }
+
+        if ($siterules || $rules) { // Check that there are rules present.
+            // Get a list of all the events we have been through.
+            $allevents = array();
+            foreach ($courses as $key => $value) {
+                foreach ($courses[$key] as $event => $notused) {
+                    $allevents[] = $event;
+                }
+            }
+            // Remove all the events in the table that are not applicable to any rule. There may be a rule in one course
+            // listening for a certain event, but not in another course, so we can delete the event from the course
+            // where there is no rule. We also have to consider site wide rules. We may have an event that is triggered
+            // in a course we can't remove because there is a site wide rule for this event.
+            if ($events = $DB->get_recordset('tool_monitor_events')) {
+                // Array to store which events we need to remove.
+                $eventstodelete = array();
+                // Store the current time.
+                $now = time();
+                foreach ($events as $event) {
+                    // If the event is not required for a course rule and there is no site wide rule for it, or
+                    // it has extended past or equal to the timewindow for the site rule - it can be deleted.
+                    if (!isset($courses[$event->courseid][$event->eventname]) && (!isset($courses[0][$event->eventname])
+                        || $courses[0][$event->eventname] <= ($now - $event->timecreated))) {
+                        $eventstodelete[] = $event->id;
+                    }
+                }
+                // Free resources.
+                $events->close();
+
+                // Remove the events.
+                if (!empty($eventstodelete)) {
+                    list($eventidsql, $params) = $DB->get_in_or_equal($eventstodelete);
+                    $DB->delete_records_select('tool_monitor_events', "id $eventidsql", $params);
+                }
+            }
+
+            // Remove all the events in the table that are not used by any rule.
+            if (!empty($allevents)) {
+                list($eventnamesql, $params) = $DB->get_in_or_equal($allevents, SQL_PARAMS_QM, 'param', false);
+                $DB->delete_records_select('tool_monitor_events', "eventname $eventnamesql", $params);
+            }
+        } else { // No rules, just remove everything.
+            $DB->delete_records('tool_monitor_events');
+        }
+    }
+}
diff --git a/admin/tool/monitor/db/tasks.php b/admin/tool/monitor/db/tasks.php
new file mode 100644 (file)
index 0000000..e70c344
--- /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/>.
+
+/**
+ * This file defines tasks performed by the tool.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// List of tasks.
+$tasks = array(
+    array(
+        'classname' => 'tool_monitor\task\clean_events',
+        'blocking' => 0,
+        'minute' => '*',
+        'hour' => '*',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    )
+);
index 45c7309..3a02a62 100644 (file)
@@ -107,5 +107,8 @@ if (empty($totalrules)) {
         echo $link;
     }
     echo html_writer::end_div();
+} else if (has_capability('tool/monitor:managerules', $context)) {
+    $manageurl = new moodle_url("/admin/tool/monitor/managerules.php", array('courseid' => $courseid));
+    echo $renderer->render_rules_link($manageurl);
 }
 echo $OUTPUT->footer();
index bfac799..fcef71b 100644 (file)
@@ -39,12 +39,19 @@ $string['description'] = 'Description:';
 $string['duplicaterule'] = 'Duplicate rule';
 $string['editrule'] = 'Edit rule';
 $string['eventnotfound'] = 'Event not found';
+$string['eventrulecreated'] = 'Rule created';
+$string['eventruledeleted'] = 'Rule deleted';
+$string['eventruleupdated'] = 'Rule updated';
+$string['eventsubcreated'] = 'Subscription created';
+$string['eventsubcriteriamet'] = 'Subscription criteria met';
+$string['eventsubdeleted'] = 'Subscription deleted';
 $string['errorincorrectevent'] = 'Please select an event related to the selected plugin';
 $string['freqdesc'] = '{$a->freq} times in {$a->mins} minutes';
 $string['frequency'] = 'Frequency';
 $string['invalidmodule'] = 'Invalid module';
 $string['norules'] = 'There are no rules you can subscribe to.';
 $string['manageruleslink'] = 'You can manage rules from {$a} page.';
+$string['managesubscriptionslink'] = 'You can subscribe to rules from the {$a} page.';
 $string['moduleinstance'] = 'Module instance';
 $string['manage'] = 'Manage';
 $string['managesubscriptions'] = 'Event monitoring';
@@ -61,6 +68,7 @@ $string['messagetemplate_help'] = 'This is the content of the message that will
 $string['minutes'] = 'in minutes:';
 $string['name'] = 'Name of the rule: ';
 $string['name_help'] = "Choose a name for the rule.";
+$string['nopermission'] = "No permission";
 $string['pluginname'] = 'Event monitor';
 $string['processevents'] = 'Process events';
 $string['ruleareyousure'] = 'Are you sure you want to delete rule "{$a}"?';
@@ -84,6 +92,7 @@ $string['subcreatesuccess'] = "Subscription successfully created";
 $string['subdeletesuccess'] = "Subscription successfully removed";
 $string['subhelp'] = 'Subscription details';
 $string['subhelp_help'] = 'This subscription listens for when the event \'{$a->eventname}\' has been triggered in \'{$a->moduleinstance}\' {$a->frequency} time(s) in {$a->minutes} minute(s).';
+$string['taskcleanevents'] = 'Removes any unnecessary event monitor events';
 $string['title'] = '{$a->coursename} : {$a->reportname}';
 $string['monitor:managerules'] = 'Manage event monitor rules';
 $string['monitor:subscribe'] = 'Subscribe to event monitor rules';
index e49cdcc..d06ec5e 100644 (file)
@@ -56,7 +56,7 @@ function tool_monitor_extend_navigation_course($navigation, $course, $context) {
  */
 function tool_monitor_extend_navigation_user_settings($navigation, $user, $usercontext, $course, $coursecontext) {
     global $USER;
-    if (($USER->id == $user->id)) {
+    if (($USER->id == $user->id) && (has_capability('tool/monitor:subscribe', $coursecontext))) {
         $url = new moodle_url('/admin/tool/monitor/index.php', array('courseid' => $course->id));
         $subsnode = navigation_node::create(get_string('managesubscriptions', 'tool_monitor'), $url,
                 navigation_node::TYPE_SETTING, null, null, new pix_icon('i/settings', ''));
index 8259fec..bdafd03 100644 (file)
@@ -90,4 +90,8 @@ if (!empty($action) && $ruleid) {
 $renderable = new \tool_monitor\output\managerules\renderable('toolmonitorrules', $manageurl, $courseid);
 $renderer = $PAGE->get_renderer('tool_monitor', 'managerules');
 echo $renderer->render($renderable);
+if (has_capability('tool/monitor:subscribe', $context)) {
+    $manageurl = new moodle_url("/admin/tool/monitor/index.php", array('courseid' => $courseid));
+    echo $renderer->render_subscriptions_link($manageurl);
+}
 echo $OUTPUT->footer();
index 8d78a0a..4fe3660 100644 (file)
@@ -123,3 +123,16 @@ Feature: tool_monitor_subscriptions
     When  I follow "Messages"
     And   I follow "Do not reply to this email (1)"
     Then  I should see "The course was viewed."
+
+  Scenario: Navigating via quick link to rules
+    Given I log in as "admin"
+    And   I navigate to "Event monitoring" node in "My profile settings"
+    Then  I should see "You can manage rules from Event monitoring rules page."
+    And   I follow "Event monitoring rules"
+    And   I should see "Event monitor"
+    And   I should see "You can subscribe to rules from the Event monitoring page."
+    And   I log out
+    And   I log in as "teacher1"
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring" node in "My profile settings"
+    Then  I should not see "You can manage rules from Event monitoring rules page."
index b8d178a..26d4086 100644 (file)
@@ -46,16 +46,17 @@ class tool_monitor_eventobservers_testcase extends advanced_testcase {
         $this->resetAfterTest(true);
 
         $user = $this->getDataGenerator()->create_user();
-        $course = $this->getDataGenerator()->create_course();
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
         $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
 
         $rule = new stdClass();
         $rule->userid = $user->id;
-        $rule->courseid = $course->id;
+        $rule->courseid = $course1->id;
         $rule->plugin = 'test';
 
         $sub = new stdClass();
-        $sub->courseid = $course->id;
+        $sub->courseid = $course1->id;
         $sub->userid = $user->id;
 
         // Add 10 rules for this course with subscriptions.
@@ -65,9 +66,9 @@ class tool_monitor_eventobservers_testcase extends advanced_testcase {
             $monitorgenerator->create_subscription($sub);
         }
 
-        // Add 10 random rules for random courses.
+        // Add 10 random rules for course 2.
+        $rule->courseid = $course2->id;
         for ($i = 0; $i < 10; $i++) {
-            $rule->courseid = rand(10000000, 50000000);
             $createdrule = $monitorgenerator->create_rule($rule);
             $sub->courseid = $rule->courseid;
             $sub->ruleid = $createdrule->id;
@@ -77,24 +78,24 @@ class tool_monitor_eventobservers_testcase extends advanced_testcase {
         // Verify data before course delete.
         $totalrules = \tool_monitor\rule_manager::get_rules_by_plugin('test');
         $this->assertCount(20, $totalrules);
-        $courserules = \tool_monitor\rule_manager::get_rules_by_courseid($course->id);
+        $courserules = \tool_monitor\rule_manager::get_rules_by_courseid($course1->id);
         $this->assertCount(10, $courserules);
         $totalsubs = $DB->get_records('tool_monitor_subscriptions');
         $this->assertCount(20, $totalsubs);
-        $coursesubs = \tool_monitor\subscription_manager::get_user_subscriptions_for_course($course->id, 0, 0, $user->id);
+        $coursesubs = \tool_monitor\subscription_manager::get_user_subscriptions_for_course($course1->id, 0, 0, $user->id);
         $this->assertCount(10, $coursesubs);
 
         // Let us delete the course now.
-        delete_course($course->id, false);
+        delete_course($course1->id, false);
 
         // Verify data after course delete.
         $totalrules = \tool_monitor\rule_manager::get_rules_by_plugin('test');
         $this->assertCount(10, $totalrules);
-        $courserules = \tool_monitor\rule_manager::get_rules_by_courseid($course->id);
+        $courserules = \tool_monitor\rule_manager::get_rules_by_courseid($course1->id);
         $this->assertCount(0, $courserules); // Making sure all rules are deleted.
         $totalsubs = $DB->get_records('tool_monitor_subscriptions');
         $this->assertCount(10, $totalsubs);
-        $coursesubs = \tool_monitor\subscription_manager::get_user_subscriptions_for_course($course->id, 0, 0, $user->id);
+        $coursesubs = \tool_monitor\subscription_manager::get_user_subscriptions_for_course($course1->id, 0, 0, $user->id);
         $this->assertCount(0, $coursesubs); // Making sure all subscriptions are deleted.
     }
 
@@ -374,16 +375,17 @@ class tool_monitor_eventobservers_testcase extends advanced_testcase {
         $this->resetAfterTest(true);
 
         $user = $this->getDataGenerator()->create_user();
-        $course = $this->getDataGenerator()->create_course();
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
         $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
 
         $rule = new stdClass();
         $rule->userid = $user->id;
-        $rule->courseid = $course->id;
+        $rule->courseid = $course1->id;
         $rule->plugin = 'test';
 
         $sub = new stdClass();
-        $sub->courseid = $course->id;
+        $sub->courseid = $course1->id;
         $sub->userid = $user->id;
 
         // Add 10 rules for this course with subscriptions.
@@ -393,9 +395,9 @@ class tool_monitor_eventobservers_testcase extends advanced_testcase {
             $monitorgenerator->create_subscription($sub);
         }
 
-        // Add 10 random rules for random courses.
+        // Add 10 random rules for course 2.
+        $rule->courseid = $course2->id;
         for ($i = 0; $i < 10; $i++) {
-            $rule->courseid = rand(10000000, 50000000);
             $createdrule = $monitorgenerator->create_rule($rule);
             $sub->courseid = $rule->courseid;
             $sub->ruleid = $createdrule->id;
@@ -428,21 +430,22 @@ class tool_monitor_eventobservers_testcase extends advanced_testcase {
         $this->resetAfterTest(true);
 
         $user = $this->getDataGenerator()->create_user();
-        $course = $this->getDataGenerator()->create_course();
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
         $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
 
         // Now let us create a rule specific to a module instance.
         $cm = new stdClass();
-        $cm->course = $course->id;
+        $cm->course = $course1->id;
         $book = $this->getDataGenerator()->create_module('book', $cm);
 
         $rule = new stdClass();
         $rule->userid = $user->id;
-        $rule->courseid = $course->id;
+        $rule->courseid = $course1->id;
         $rule->plugin = 'test';
 
         $sub = new stdClass();
-        $sub->courseid = $course->id;
+        $sub->courseid = $course1->id;
         $sub->userid = $user->id;
         $sub->cmid = $book->cmid;
 
@@ -453,9 +456,9 @@ class tool_monitor_eventobservers_testcase extends advanced_testcase {
             $monitorgenerator->create_subscription($sub);
         }
 
-        // Add 10 random rules for random courses.
+        // Add 10 random rules for course 2.
+        $rule->courseid = $course2->id;
         for ($i = 0; $i < 10; $i++) {
-            $rule->courseid = rand(10000000, 50000000);
             $createdrule = $monitorgenerator->create_rule($rule);
             $sub->courseid = $rule->courseid;
             $sub->ruleid = $createdrule->id;
diff --git a/admin/tool/monitor/tests/events_test.php b/admin/tool/monitor/tests/events_test.php
new file mode 100644 (file)
index 0000000..9f55c19
--- /dev/null
@@ -0,0 +1,337 @@
+<?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/>.
+
+/**
+ * Events tests.
+ *
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests that the tool_monitor events are valid and triggered correctly.
+ */
+class tool_monitor_events_testcase extends advanced_testcase {
+
+    /**
+     * Tests set up.
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    /**
+     * Test the rule created event.
+     */
+    public function test_rule_created() {
+        // Create the items we need to create a rule.
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_user();
+
+        // Create the variables for the rule we want to create.
+        $ruledata = new stdClass();
+        $ruledata->userid = $user->id;
+        $ruledata->courseid = $course->id;
+        $ruledata->description = 'Rule description';
+        $ruledata->descriptionformat = FORMAT_HTML;
+        $ruledata->template = 'A message template';
+        $ruledata->templateformat = FORMAT_HTML;
+        $ruledata->frequency = 1;
+        $ruledata->timewindow = 60;
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $rule = \tool_monitor\rule_manager::add_rule($ruledata);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event contains the expected values.
+        $this->assertInstanceOf('\tool_monitor\event\rule_created', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals($rule->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+
+        // Now let's add a system rule (courseid = 0).
+        $ruledata->courseid = 0;
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        \tool_monitor\rule_manager::add_rule($ruledata);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event uses the system context.
+        $this->assertInstanceOf('\tool_monitor\event\rule_created', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+    }
+
+    /**
+     * Test the rule updated event.
+     */
+    public function test_rule_updated() {
+        // Create the items we need.
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create the rule we are going to update.
+        $createrule = new stdClass();
+        $createrule->courseid = $course->id;
+        $rule = $monitorgenerator->create_rule($createrule);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $updaterule = new stdClass();
+        $updaterule->id = $rule->id;
+        \tool_monitor\rule_manager::update_rule($updaterule);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event contains the expected values.
+        $this->assertInstanceOf('\tool_monitor\event\rule_updated', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals($rule->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+
+        // Now let's update a system rule (courseid = 0).
+        $createrule->courseid = 0;
+        $rule = $monitorgenerator->create_rule($createrule);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $updaterule = new stdClass();
+        $updaterule->id = $rule->id;
+        \tool_monitor\rule_manager::update_rule($updaterule);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event uses the system context.
+        $this->assertInstanceOf('\tool_monitor\event\rule_updated', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+    }
+
+    /**
+     * Test the rule deleted event.
+     */
+    public function test_rule_deleted() {
+        // Create the items we need.
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create the rule we are going to delete.
+        $createrule = new stdClass();
+        $createrule->courseid = $course->id;
+        $rule = $monitorgenerator->create_rule($createrule);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        \tool_monitor\rule_manager::delete_rule($rule->id);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event contains the expected values.
+        $this->assertInstanceOf('\tool_monitor\event\rule_deleted', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals($rule->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+
+        // Now let's delete a system rule (courseid = 0).
+        $createrule = new stdClass();
+        $createrule->courseid = 0;
+        $rule = $monitorgenerator->create_rule($createrule);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        \tool_monitor\rule_manager::delete_rule($rule->id);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event uses the system context.
+        $this->assertInstanceOf('\tool_monitor\event\rule_deleted', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+    }
+
+    /**
+     * Test the subscription created event.
+     */
+    public function test_subscription_created() {
+        // Create the items we need to test this.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        // Create a rule to subscribe to.
+        $rule = $monitorgenerator->create_rule();
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $subscriptionid = \tool_monitor\subscription_manager::create_subscription($rule->id, $course->id, 0, $user->id);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event contains the expected values.
+        $this->assertInstanceOf('\tool_monitor\event\subscription_created', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals($subscriptionid, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+
+        // Create a system subscription - trigger and capture the event.
+        $sink = $this->redirectEvents();
+        \tool_monitor\subscription_manager::create_subscription($rule->id, 0, 0, $user->id);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event uses the system context.
+        $this->assertInstanceOf('\tool_monitor\event\subscription_created', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+    }
+
+    /**
+     * Test the subscription deleted event.
+     */
+    public function test_subscription_deleted() {
+        // Create the items we need to test this.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        // Create a rule to subscribe to.
+        $rule = $monitorgenerator->create_rule();
+
+        $sub = new stdClass();
+        $sub->courseid = $course->id;
+        $sub->userid = $user->id;
+        $sub->ruleid = $rule->id;
+
+        // Create the subscription we are going to delete.
+        $subscription = $monitorgenerator->create_subscription($sub);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        \tool_monitor\subscription_manager::delete_subscription($subscription->id, false);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event contains the expected values.
+        $this->assertInstanceOf('\tool_monitor\event\subscription_deleted', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+
+        // Now let's delete a system subscription.
+        $sub = new stdClass();
+        $sub->courseid = 0;
+        $sub->userid = $user->id;
+        $sub->ruleid = $rule->id;
+
+        // Create the subscription we are going to delete.
+        $subscription = $monitorgenerator->create_subscription($sub);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        \tool_monitor\subscription_manager::delete_subscription($subscription->id, false);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event uses the system context.
+        $this->assertInstanceOf('\tool_monitor\event\subscription_deleted', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+
+        // Now, create a bunch of subscriptions for the rule we created.
+        $sub->courseid = $course->id;
+        for ($i = 1; $i <= 10; $i++) {
+            $sub->userid = $i;
+            $subscription = $monitorgenerator->create_subscription($sub);
+            if ($i == 1) {
+                $subscription1 = $subscription;
+            }
+        }
+
+        // Trigger and capture the events.
+        $sink = $this->redirectEvents();
+        \tool_monitor\subscription_manager::remove_all_subscriptions_for_rule($rule->id);
+        $events = $sink->get_events();
+
+        // Check that there were 10 events in total.
+        $this->assertCount(10, $events);
+
+        // Get the first event and ensure it is valid (we can assume the rest are the same).
+        $event = reset($events);
+        $this->assertInstanceOf('\tool_monitor\event\subscription_deleted', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals($subscription1->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * Test the subscription criteria met event.
+     */
+    public function test_subscription_criteria_met() {
+        // Create the items we need to test this.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $book = $this->getDataGenerator()->create_module('book', array('course' => $course->id));
+        $bookgenerator = $this->getDataGenerator()->get_plugin_generator('mod_book');
+        $chapter = $bookgenerator->create_chapter(array('bookid' => $book->id));
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        // Create a rule we want to subscribe to.
+        $rule = new stdClass();
+        $rule->userid = $user->id;
+        $rule->courseid = $course->id;
+        $rule->plugin = 'mod_book';
+        $rule->eventname = '\mod_book\event\chapter_viewed';
+        $rule->frequency = 1;
+        $rule->timewindow = 60;
+        $rule = $monitorgenerator->create_rule($rule);
+
+        // Create the subscription.
+        $sub = new stdClass();
+        $sub->courseid = $course->id;
+        $sub->userid = $user->id;
+        $sub->ruleid = $rule->id;
+        $monitorgenerator->create_subscription($sub);
+
+        // Now create the \mod_book\event\chapter_viewed event we are listening for.
+        $context = context_module::instance($book->cmid);
+        $event = \mod_book\event\chapter_viewed::create_from_chapter($book, $context, $chapter);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        \tool_monitor\eventobservers::process_event($event);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Confirm that the event contains the expected values.
+        $this->assertInstanceOf('\tool_monitor\event\subscription_criteria_met', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEventContextNotUsed($event);
+    }
+}
index f9e05dc..1296791 100644 (file)
@@ -108,11 +108,14 @@ class tool_monitor_rule_manager_testcase extends advanced_testcase {
 
         $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
 
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+
         $record = new stdClass();
-        $record->courseid = 3;
+        $record->courseid = $course1->id;
 
         $record2 = new stdClass();
-        $record2->courseid = 4;
+        $record2->courseid = $course2->id;
 
         $ruleids = array();
         for ($i = 0; $i < 10; $i++) {
@@ -122,8 +125,8 @@ class tool_monitor_rule_manager_testcase extends advanced_testcase {
             $ruleids[] = $rule->id;
             $rule = $monitorgenerator->create_rule($record2); // Create rules in a different course.
         }
-        $ruledata = \tool_monitor\rule_manager::get_rules_by_courseid(3);
-        $this->assertEquals($ruleids, array_keys($ruledata));
+        $ruledata = \tool_monitor\rule_manager::get_rules_by_courseid($course1->id);
+        $this->assertEmpty(array_merge(array_diff(array_keys($ruledata), $ruleids), array_diff($ruleids, array_keys($ruledata))));
         $this->assertCount(20, $ruledata);
     }
 
@@ -150,7 +153,7 @@ class tool_monitor_rule_manager_testcase extends advanced_testcase {
         }
 
         $ruledata = \tool_monitor\rule_manager::get_rules_by_plugin('core');
-        $this->assertEquals($ruleids, array_keys($ruledata));
+        $this->assertEmpty(array_merge(array_diff(array_keys($ruledata), $ruleids), array_diff($ruleids, array_keys($ruledata))));
         $this->assertCount(10, $ruledata);
     }
 
diff --git a/admin/tool/monitor/tests/task_clean_events_test.php b/admin/tool/monitor/tests/task_clean_events_test.php
new file mode 100644 (file)
index 0000000..6737c1b
--- /dev/null
@@ -0,0 +1,219 @@
+<?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/>.
+
+/**
+ * Unit tests for the tool_monitor clean events task.
+ *
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Class used to test the tool_monitor clean events task.
+ */
+class tool_monitor_task_clean_events_testcase extends advanced_testcase {
+
+    /**
+     * Test set up.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Tests the cleaning up of events.
+     */
+    public function test_clean_events() {
+        global $DB;
+
+        // Create the necessary items for testing.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $bookgenerator = $this->getDataGenerator()->get_plugin_generator('mod_book');
+        $book = $this->getDataGenerator()->create_module('book', array('course' => $course->id));
+        $bookcontext = context_module::instance($book->cmid);
+        $bookchapter = $bookgenerator->create_chapter(array('bookid' => $book->id));
+        $course2 = $this->getDataGenerator()->create_course();
+        $book2 = $this->getDataGenerator()->create_module('book', array('course' => $course2->id));
+        $book2context = context_module::instance($book2->cmid);
+        $book2chapter = $bookgenerator->create_chapter(array('bookid' => $book2->id));
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        // Let's set some data for the rules we need before we can generate them.
+        $rule = new stdClass();
+        $rule->userid = $user->id;
+        $rule->courseid = $course->id;
+        $rule->plugin = 'mod_book';
+        $rule->eventname = '\mod_book\event\course_module_viewed';
+        $rule->timewindow = 500;
+
+        // Let's add a few rules we want to monitor.
+        $rule1 = $monitorgenerator->create_rule($rule);
+
+        $rule->eventname = '\mod_book\event\course_module_instance_list_viewed';
+        $rule2 = $monitorgenerator->create_rule($rule);
+
+        // Add the same rules for the same course, but this time with a lower timewindow (used to test that we do not
+        // remove an event for a course if there is still a rule where the maximum timewindow has not been reached).
+        $rule->eventname = '\mod_book\event\course_module_viewed';
+        $rule->timewindow = 200;
+        $rule3 = $monitorgenerator->create_rule($rule);
+
+        $rule->eventname = '\mod_book\event\course_module_instance_list_viewed';
+        $rule4 = $monitorgenerator->create_rule($rule);
+
+        // Add another rule in a different course.
+        $rule->courseid = $course2->id;
+        $rule->eventname = '\mod_book\event\chapter_viewed';
+        $rule->timewindow = 200;
+        $rule5 = $monitorgenerator->create_rule($rule);
+
+        // Add a site wide rule.
+        $rule->courseid = 0;
+        $rule->eventname = '\mod_book\event\chapter_viewed';
+        $rule->timewindow = 500;
+        $rule6 = $monitorgenerator->create_rule($rule);
+
+        // Now let's populate the tool_monitor table with the events associated with those rules.
+        \mod_book\event\course_module_viewed::create_from_book($book, $bookcontext)->trigger();
+        \mod_book\event\course_module_instance_list_viewed::create_from_course($course)->trigger();
+        \mod_book\event\chapter_viewed::create_from_chapter($book, $bookcontext, $bookchapter)->trigger();
+
+        // Let's trigger the viewed events again, but in another course. The rules created for these events are
+        // associated with another course, so these events should get deleted when we trigger the cleanup task.
+        \mod_book\event\course_module_viewed::create_from_book($book2, $book2context)->trigger();
+        \mod_book\event\course_module_instance_list_viewed::create_from_course($course2)->trigger();
+        // Trigger a chapter_viewed event in this course - this should not get deleted as the rule is site wide.
+        \mod_book\event\chapter_viewed::create_from_chapter($book2, $book2context, $book2chapter)->trigger();
+
+        // Trigger a bunch of other events.
+        $eventparams = array(
+            'context' => context_course::instance($course->id)
+        );
+        for ($i = 0; $i < 5; $i++) {
+            \mod_quiz\event\course_module_instance_list_viewed::create($eventparams)->trigger();
+            \mod_scorm\event\course_module_instance_list_viewed::create($eventparams)->trigger();
+        }
+
+        // Check that the events exist - there will be additional events for creating courses, modules and rules.
+        $this->assertEquals(26, $DB->count_records('tool_monitor_events'));
+
+        // Run the task and check that all the quiz, scorm and rule events are removed as well as the course_module_*
+        // viewed events in the second course.
+        $task = new \tool_monitor\task\clean_events();
+        $task->execute();
+
+        $events = $DB->get_records('tool_monitor_events');
+        $this->assertEquals(4, count($events));
+        $event1 = array_shift($events);
+        $event2 = array_shift($events);
+        $event3 = array_shift($events);
+        $event4 = array_shift($events);
+        $this->assertEquals('\mod_book\event\course_module_viewed', $event1->eventname);
+        $this->assertEquals($course->id, $event1->courseid);
+        $this->assertEquals('\mod_book\event\course_module_instance_list_viewed', $event2->eventname);
+        $this->assertEquals($course->id, $event2->courseid);
+        $this->assertEquals('\mod_book\event\chapter_viewed', $event3->eventname);
+        $this->assertEquals($course->id, $event3->courseid);
+        $this->assertEquals('\mod_book\event\chapter_viewed', $event4->eventname);
+        $this->assertEquals($course2->id, $event4->courseid);
+
+        // Update the timewindow for two of the rules.
+        $updaterule = new stdClass();
+        $updaterule->id = $rule1->id;
+        $updaterule->timewindow = 0;
+        \tool_monitor\rule_manager::update_rule($updaterule);
+        $updaterule->id = $rule2->id;
+        \tool_monitor\rule_manager::update_rule($updaterule);
+
+        // Run the task and check that the events remain as we still have not reached the maximum timewindow.
+        $task = new \tool_monitor\task\clean_events();
+        $task->execute();
+
+        $this->assertEquals(4, $DB->count_records('tool_monitor_events'));
+
+        // Now, remove the rules associated with course_module_* events so they get deleted.
+        \tool_monitor\rule_manager::delete_rule($rule1->id);
+        \tool_monitor\rule_manager::delete_rule($rule2->id);
+        \tool_monitor\rule_manager::delete_rule($rule3->id);
+        \tool_monitor\rule_manager::delete_rule($rule4->id);
+
+        // Run the task and check all the course_module_* events are gone.
+        $task = new \tool_monitor\task\clean_events();
+        $task->execute();
+
+        // We now should only have the chapter_viewed events.
+        $events = $DB->get_records('tool_monitor_events');
+        $this->assertEquals(2, count($events));
+        $event1 = array_shift($events);
+        $event2 = array_shift($events);
+        $this->assertEquals('\mod_book\event\chapter_viewed', $event1->eventname);
+        $this->assertEquals($course->id, $event1->courseid);
+        $this->assertEquals('\mod_book\event\chapter_viewed', $event2->eventname);
+        $this->assertEquals($course2->id, $event2->courseid);
+
+        // Set the timewindow of the rule for the event chapter_viewed in the second course to 0.
+        $updaterule->id = $rule5->id;
+        \tool_monitor\rule_manager::update_rule($updaterule);
+
+        // Run the task.
+        $task = new \tool_monitor\task\clean_events();
+        $task->execute();
+
+        // Check that nothing was deleted as we still have a site wide rule for the chapter_viewed event.
+        $this->assertEquals(2, $DB->count_records('tool_monitor_events'));
+
+        // Set the timewindow back to 500.
+        $updaterule->id = $rule5->id;
+        $updaterule->timewindow = 500;
+        \tool_monitor\rule_manager::update_rule($updaterule);
+
+        // Set the site rule timewindow to 0.
+        $updaterule->id = $rule6->id;
+        $updaterule->timewindow = 0;
+        \tool_monitor\rule_manager::update_rule($updaterule);
+
+        // Run the task.
+        $task = new \tool_monitor\task\clean_events();
+        $task->execute();
+
+        // We now should only have one chapter_viewed event for the second course.
+        $events = $DB->get_records('tool_monitor_events');
+        $this->assertEquals(1, count($events));
+        $event1 = array_shift($events);
+        $this->assertEquals('\mod_book\event\chapter_viewed', $event1->eventname);
+        $this->assertEquals($course2->id, $event1->courseid);
+
+        // Remove the site rule.
+        \tool_monitor\rule_manager::delete_rule($rule6->id);
+
+        // Remove the last remaining rule.
+        \tool_monitor\rule_manager::delete_rule($rule5->id);
+
+        // Run the task.
+        $task = new \tool_monitor\task\clean_events();
+        $task->execute();
+
+        // There should be no events left.
+        $this->assertEquals(0, $DB->count_records('tool_monitor_events'));
+    }
+}
index dba3696..b8150b0 100644 (file)
@@ -26,6 +26,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2014061900;       // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2014061901;       // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2014061900;       // Requires this Moodle version.
 $plugin->component = 'tool_monitor'; // Full name of the plugin (used for diagnostics).
index fd33545..a173e72 100644 (file)
@@ -35,6 +35,13 @@ global $CFG;
  */
 class tool_uploadcourse_course_testcase extends advanced_testcase {
 
+    /**
+     * Tidy up open files that may be left open.
+     */
+    protected function tearDown() {
+        gc_collect_cycles();
+    }
+
     public function test_proceed_without_prepare() {
         $this->resetAfterTest(true);
         $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
index 1dd20dc..2d2ec04 100644 (file)
@@ -36,6 +36,13 @@ require_once($CFG->libdir . '/csvlib.class.php');
  */
 class tool_uploadcourse_processor_testcase extends advanced_testcase {
 
+    /**
+     * Tidy up open files that may be left open.
+     */
+    protected function tearDown() {
+        gc_collect_cycles();
+    }
+
     public function test_basic() {
         global $DB;
         $this->resetAfterTest(true);
index 64f34f3..3931ea9 100644 (file)
@@ -52,14 +52,9 @@ class auth_db_testcase extends advanced_testcase {
             set_config('host', $CFG->dbhost.':'.$CFG->dboptions['dbport'], 'auth/db');
         }
 
-        switch (get_class($DB)) {
-            case 'mssql_native_moodle_database':
-                set_config('type', 'mssql_n', 'auth/db');
-                set_config('sybasequoting', '1', 'auth/db');
-                break;
+        switch ($DB->get_dbfamily()) {
 
-            case 'mariadb_native_moodle_database':
-            case 'mysqli_native_moodle_database':
+            case 'mysql':
                 set_config('type', 'mysqli', 'auth/db');
                 set_config('setupsql', "SET NAMES 'UTF-8'", 'auth/db');
                 set_config('sybasequoting', '0', 'auth/db');
@@ -72,12 +67,12 @@ class auth_db_testcase extends advanced_testcase {
                 }
                 break;
 
-            case 'oci_native_moodle_database':
+            case 'oracle':
                 set_config('type', 'oci8po', 'auth/db');
                 set_config('sybasequoting', '1', 'auth/db');
                 break;
 
-            case 'pgsql_native_moodle_database':
+            case 'postgres':
                 set_config('type', 'postgres7', 'auth/db');
                 $setupsql = "SET NAMES 'UTF-8'";
                 if (!empty($CFG->dboptions['dbschema'])) {
@@ -98,13 +93,17 @@ class auth_db_testcase extends advanced_testcase {
                 }
                 break;
 
-            case 'sqlsrv_native_moodle_database':
-                set_config('type', 'mssqlnative', 'auth/db');
+            case 'mssql':
+                if (get_class($DB) == 'mssql_native_moodle_database') {
+                    set_config('type', 'mssql_n', 'auth/db');
+                } else {
+                    set_config('type', 'mssqlnative', 'auth/db');
+                }
                 set_config('sybasequoting', '1', 'auth/db');
                 break;
 
             default:
-                throw new exception('Unknown database driver '.get_class($DB));
+                throw new exception('Unknown database family ' . $DB->get_dbfamily());
         }
 
         $table = new xmldb_table('auth_db_users');
index df9e085..e2250e2 100644 (file)
@@ -342,7 +342,8 @@ class availability_profile_condition_testcase extends advanced_testcase {
         // The list of fields should include the text field added in setUp(),
         // but should not include the textarea field added just now.
         $fields = condition::get_custom_profile_fields();
-        $this->assertEquals(array('frogtype'), array_keys($fields));
+        $this->assertArrayHasKey('frogtype', $fields);
+        $this->assertArrayNotHasKey('longtext', $fields);
     }
 
     /**
index 4dd37be..651b8d1 100644 (file)
@@ -958,7 +958,7 @@ class backup_gradebook_structure_step extends backup_structure_step {
         $grade_category   = new backup_nested_element('grade_category', array('id'), array(
                 //'courseid',
                 'parent', 'depth', 'path', 'fullname', 'aggregation', 'keephigh',
-                'droplow', 'aggregateonlygraded', 'aggregateoutcomes', 'aggregatesubcats',
+                'droplow', 'aggregateonlygraded', 'aggregateoutcomes',
                 'timecreated', 'timemodified', 'hidden'));
 
         $letters = new backup_nested_element('grade_letters');
index 5492497..3035c7e 100644 (file)
@@ -282,6 +282,11 @@ class restore_gradebook_structure_step extends restore_structure_step {
             }
         }
 
+        // Add a warning about a removed setting.
+        if (!empty($data->aggregatesubcats)) {
+            set_config('show_aggregatesubcats_upgrade_' . $data->courseid, 1);
+        }
+
         //need to insert a course category
         if (empty($newitemid)) {
             $newitemid = $DB->insert_record('grade_categories', $data);
index bb937aa..960c9ec 100644 (file)
@@ -38,6 +38,13 @@ require_once($CFG->libdir . '/completionlib.php');
  */
 class core_backup_moodle2_testcase extends advanced_testcase {
 
+    /**
+     * Tidy up open files that may be left open.
+     */
+    protected function tearDown() {
+        gc_collect_cycles();
+    }
+
     /**
      * Tests the availability field on modules and sections is correctly
      * backed up and restored.
index 4405da4..e4d51ec 100644 (file)
@@ -556,6 +556,9 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
         }
 
         $store = new cachestore_mongodb('Test mongodb', $configuration);
+        if (!$store->is_ready()) {
+            return false;
+        }
         $store->initialise($definition);
 
         return $store;
@@ -581,6 +584,9 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
         $configuration['usesafe'] = 1;
 
         $store = new cachestore_mongodb('Test mongodb', $configuration);
+        if (!$store->is_ready()) {
+            return false;
+        }
         $store->initialise($definition);
 
         return $store;
index 676b26d..ba0fc08 100644 (file)
@@ -2928,7 +2928,7 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
             $timezone;
     $eventrecord->timestart = strtotime($event->properties['DTSTART'][0]->value . ' ' . $tz);
     if (empty($event->properties['DTEND'])) {
-        $eventrecord->timeduration = 3600; // one hour if no end time specified
+        $eventrecord->timeduration = 0; // no duration if no end time specified
     } else {
         $endtz = isset($event->properties['DTEND'][0]->parameters['TZID']) ? $event->properties['DTEND'][0]->parameters['TZID'] :
                 $timezone;
index 1f5b051..33014d6 100644 (file)
@@ -291,6 +291,14 @@ abstract class moodleform_mod extends moodleform {
             }
         }
 
+        // Ratings: Don't let them select an aggregate type without selecting a scale.
+        // If the user has selected to use ratings but has not chosen a scale or set max points then the form is
+        // invalid. If ratings have been selected then the user must select either a scale or max points.
+        // This matches (horrible) logic in data_preprocessing.
+        if (isset($data['assessed']) && $data['assessed'] > 0 && empty($data['scale'])) {
+            $errors['assessed'] = get_string('scaleselectionrequired', 'rating');
+        }
+
         // Completion: Don't let them choose automatic completion without turning
         // on some conditions. Ignore this check when completion settings are
         // locked, as the options are then disabled.
index 62ac25a..77ef9e0 100644 (file)
@@ -33,6 +33,13 @@ require_once($CFG->dirroot . '/tag/lib.php');
 
 class core_course_courselib_testcase extends advanced_testcase {
 
+    /**
+     * Tidy up open files that may be left open.
+     */
+    protected function tearDown() {
+        gc_collect_cycles();
+    }
+
     /**
      * Set forum specific test values for calling create_module().
      *
index 13826b8..756c626 100644 (file)
@@ -47,6 +47,13 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         require_once($CFG->dirroot . '/course/externallib.php');
     }
 
+    /**
+     * Tidy up open files that may be left open.
+     */
+    protected function tearDown() {
+        gc_collect_cycles();
+    }
+
     /**
      * Test create_categories
      */
index 1f6d0d4..6acd0f8 100644 (file)
@@ -94,6 +94,9 @@
 
     require_once($CFG->dirroot.'/calendar/lib.php');    /// This is after login because it needs $USER
 
+    // Must set layout before gettting section info. See MDL-47555.
+    $PAGE->set_pagelayout('course');
+
     if ($section and $section > 0) {
 
         // Get section details and check it exists.
     // Fix course format if it is no longer installed
     $course->format = course_get_format($course)->get_format();
 
-    $PAGE->set_pagelayout('course');
     $PAGE->set_pagetype('course-view-' . $course->format);
     $PAGE->set_other_editing_capability('moodle/course:update');
     $PAGE->set_other_editing_capability('moodle/course:manageactivities');
index 39f8078..7829833 100644 (file)
@@ -54,14 +54,9 @@ class enrol_database_testcase extends advanced_testcase {
             set_config('dbhost', $CFG->dbhost.':'.$CFG->dboptions['dbport'], 'enrol_database');
         }
 
-        switch (get_class($DB)) {
-            case 'mssql_native_moodle_database':
-                set_config('dbtype', 'mssql_n', 'enrol_database');
-                set_config('dbsybasequoting', '1', 'enrol_database');
-                break;
+        switch ($DB->get_dbfamily()) {
 
-            case 'mariadb_native_moodle_database':
-            case 'mysqli_native_moodle_database':
+            case 'mysql':
                 set_config('dbtype', 'mysqli', 'enrol_database');
                 set_config('dbsetupsql', "SET NAMES 'UTF-8'", 'enrol_database');
                 set_config('dbsybasequoting', '0', 'enrol_database');
@@ -74,12 +69,12 @@ class enrol_database_testcase extends advanced_testcase {
                 }
                 break;
 
-            case 'oci_native_moodle_database':
+            case 'oracle':
                 set_config('dbtype', 'oci8po', 'enrol_database');
                 set_config('dbsybasequoting', '1', 'enrol_database');
                 break;
 
-            case 'pgsql_native_moodle_database':
+            case 'postgres':
                 set_config('dbtype', 'postgres7', 'enrol_database');
                 $setupsql = "SET NAMES 'UTF-8'";
                 if (!empty($CFG->dboptions['dbschema'])) {
@@ -100,8 +95,12 @@ class enrol_database_testcase extends advanced_testcase {
                 }
                 break;
 
-            case 'sqlsrv_native_moodle_database':
-                set_config('dbtype', 'mssqlnative', 'enrol_database');
+            case 'mssql':
+                if (get_class($DB) == 'mssql_native_moodle_database') {
+                    set_config('dbtype', 'mssql_n', 'enrol_database');
+                } else {
+                    set_config('dbtype', 'mssqlnative', 'enrol_database');
+                }
                 set_config('dbsybasequoting', '1', 'enrol_database');
                 break;
 
index baa4087..ae103fb 100644 (file)
@@ -46,38 +46,34 @@ class edit_letter_form extends moodleform {
         $gradeletter       = get_string('gradeletter', 'grades');
         $gradeboundary     = get_string('gradeboundary', 'grades');
 
-        $percentages = array(-1 => get_string('unused', 'grades'));
-        for ($i=100; $i > -1; $i--) {
-            $percentages[$i] = "$i %";
-        }
-
-        for($i=1; $i<$num+1; $i++) {
+        for ($i=1; $i<$num+1; $i++) {
             $gradelettername = 'gradeletter'.$i;
             $gradeboundaryname = 'gradeboundary'.$i;
 
-            $mform->addElement('text', $gradelettername, $gradeletter." $i");
-            if ($i == 1) {
-                $mform->addHelpButton($gradelettername, 'gradeletter', 'grades');
-            }
+            $entry = array();
+            $entry[] = $mform->createElement('text', $gradelettername, $gradeletter . " $i");
             $mform->setType($gradelettername, PARAM_TEXT);
 
             if (!$admin) {
                 $mform->disabledIf($gradelettername, 'override', 'notchecked');
-                $mform->disabledIf($gradelettername, $gradeboundaryname, 'eq', -1);
             }
 
-            $mform->addElement('select', $gradeboundaryname, $gradeboundary." $i", $percentages);
-            if ($i == 1) {
-                $mform->addHelpButton($gradeboundaryname, 'gradeboundary', 'grades');
-            }
-            $mform->setDefault($gradeboundaryname, -1);
-            $mform->setType($gradeboundaryname, PARAM_INT);
+            $entry[] = $mform->createElement('static', '', '', '&ge;');
+            $entry[] = $mform->createElement('text', $gradeboundaryname, $gradeboundary." $i");
+            $entry[] = $mform->createElement('static', '', '', '%');
+            $mform->addGroup($entry, 'gradeentry'.$i, $gradeletter." $i", array(' '), false);
+
+            $mform->setType($gradeboundaryname, PARAM_FLOAT);
 
             if (!$admin) {
                 $mform->disabledIf($gradeboundaryname, 'override', 'notchecked');
             }
         }
 
+        if ($num > 0) {
+            $mform->addHelpButton('gradeentry1', 'gradeletter', 'grades');
+        }
+
         // hidden params
         $mform->addElement('hidden', 'id');
         $mform->setType('id', PARAM_INT);
index d0dc35a..1974367 100644 (file)
@@ -146,7 +146,8 @@ if (!$edit) {
                 if ($letter == '') {
                     continue;
                 }
-                $letters[$data->$gradeboundaryname] = $letter;
+                // The keys need to be strings so floats are not truncated.
+                $letters[strval($data->$gradeboundaryname)] = $letter;
             }
         }
         krsort($letters, SORT_NUMERIC);
index 25ca265..5d680b8 100644 (file)
@@ -70,13 +70,6 @@ class edit_category_form extends moodleform {
             }
         }
 
-        $mform->addElement('advcheckbox', 'aggregatesubcats', get_string('aggregatesubcats', 'grades'));
-        $mform->addHelpButton('aggregatesubcats', 'aggregatesubcats', 'grades');
-
-        if ((int)$CFG->grade_aggregatesubcats_flag & 2) {
-            $mform->setAdvanced('aggregatesubcats');
-        }
-
         $mform->addElement('text', 'keephigh', get_string('keephigh', 'grades'), 'size="3"');
         $mform->setType('keephigh', PARAM_INT);
         $mform->addHelpButton('keephigh', 'keephigh', 'grades');
@@ -360,9 +353,6 @@ class edit_category_form extends moodleform {
                 if ($mform->elementExists('aggregateoutcomes')) {
                     $mform->removeElement('aggregateoutcomes');
                 }
-                if ($mform->elementExists('aggregatesubcats')) {
-                    $mform->removeElement('aggregatesubcats');
-                }
             }
 
             // If it is a course category, remove the "required" rule from the "fullname" element
index 4a86cb3..da33c55 100644 (file)
@@ -109,7 +109,7 @@ class grade_edit_tree {
      * @return string HTML
      */
     public function build_html_tree($element, $totals, $parents, $level, &$row_count) {
-        global $CFG, $COURSE, $USER, $OUTPUT;
+        global $CFG, $COURSE, $PAGE, $OUTPUT;
 
         $object = $element['object'];
         $eid    = $element['eid'];
@@ -126,30 +126,41 @@ class grade_edit_tree {
             $rowclasses[] = $parent_eid;
         }
 
-        $actions = '';
         $moveaction = '';
+        $actionsmenu = new action_menu();
+        $actionsmenu->initialise_js($PAGE);
+        $actionsmenu->set_menu_trigger(get_string('edit'));
+        $actionsmenu->set_owner_selector('grade-item-' . $eid);
+        $actionsmenu->set_alignment(action_menu::TL, action_menu::BL);
 
-        if (!$is_category_item) {
-            $actions .= $this->gtree->get_edit_icon($element, $this->gpr);
+        if (!$is_category_item && ($icon = $this->gtree->get_edit_icon($element, $this->gpr, true))) {
+            $actionsmenu->add($icon);
         }
 
-        if ($this->show_calculations) {
-            $actions .= $this->gtree->get_calculation_icon($element, $this->gpr);
+        if ($this->show_calculations && ($icon = $this->gtree->get_calculation_icon($element, $this->gpr, true))) {
+            $actionsmenu->add($icon);
         }
 
         if ($element['type'] == 'item' or ($element['type'] == 'category' and $element['depth'] > 1)) {
             if ($this->element_deletable($element)) {
                 $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'delete', 'eid' => $eid, 'sesskey' => sesskey()));
-                $actions .= $OUTPUT->action_icon($aurl, new pix_icon('t/delete', get_string('delete')));
+                $icon = new action_menu_link_secondary($aurl, new pix_icon('t/delete', get_string('delete')), get_string('delete'));
+                $actionsmenu->add($icon);
             }
 
             $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'moveselect', 'eid' => $eid, 'sesskey' => sesskey()));
             $moveaction .= $OUTPUT->action_icon($aurl, new pix_icon('t/move', get_string('move')));
         }
 
-        $actions .= $this->gtree->get_hiding_icon($element, $this->gpr);
+        if ($icon = $this->gtree->get_hiding_icon($element, $this->gpr, true)) {
+            $actionsmenu->add($icon);
+        }
+
+        if ($icon = $this->gtree->get_reset_icon($element, $this->gpr, true)) {
+            $actionsmenu->add($icon);
+        }
 
-        $actions .= $this->gtree->get_reset_icon($element, $this->gpr);
+        $actions = $OUTPUT->render($actionsmenu);
 
         $returnrows = array();
         $root = false;
@@ -279,6 +290,7 @@ class grade_edit_tree {
             }
 
             $row = new html_table_row();
+            $row->id = 'grade-item-' . $eid;
             $row->attributes['class'] = $courseclass . ' category ' . $dimmed;
             foreach ($rowclasses as $class) {
                 $row->attributes['class'] .= ' ' . $class;
@@ -327,6 +339,7 @@ class grade_edit_tree {
 
             $dimmed = ($item->is_hidden()) ? "dimmed_text" : "";
             $gradeitemrow = new html_table_row();
+            $gradeitemrow->id = 'grade-item-' . $eid;
             $gradeitemrow->attributes['class'] = $categoryitemclass . ' item ' . $dimmed;
             foreach ($rowclasses as $class) {
                 $gradeitemrow->attributes['class'] .= ' ' . $class;
@@ -698,9 +711,11 @@ class grade_edit_tree_column_weight extends grade_edit_tree_column {
         }
         $itemcell = parent::get_item_cell($item, $params);
         $itemcell->text = '&nbsp;';
+        $object = $params['element']['object'];
 
-        if (!in_array($params['element']['object']->itemtype, array('courseitem', 'categoryitem', 'category'))
-                && !in_array($params['element']['object']->gradetype, array(GRADE_TYPE_NONE, GRADE_TYPE_TEXT))) {
+        if (!in_array($object->itemtype, array('courseitem', 'categoryitem', 'category'))
+                && !in_array($object->gradetype, array(GRADE_TYPE_NONE, GRADE_TYPE_TEXT))
+                && (!$object->is_outcome_item() || $object->load_parent_category()->aggregateoutcomes)) {
             $itemcell->text = grade_edit_tree::get_weight_input($item);
         }
 
index aeece1f..285acbe 100644 (file)
@@ -202,7 +202,7 @@ class edit_outcomeitem_form extends moodleform {
 
                 $parent_category->apply_forced_settings();
 
-                if (!$parent_category->is_aggregationcoef_used()) {
+                if (!$parent_category->is_aggregationcoef_used() || !$parent_category->aggregateoutcomes) {
                     if ($mform->elementExists('aggregationcoef')) {
                         $mform->removeElement('aggregationcoef');
                     }
@@ -228,8 +228,11 @@ class edit_outcomeitem_form extends moodleform {
                         $mform->addHelpButton('aggregationcoef', $aggcoef, 'grades');
                     }
                 }
-                // Remove fields used by natural weighting if the parent category is not using natural weighting.
-                if ($parent_category->aggregation != GRADE_AGGREGATE_SUM) {
+
+                // Remove the natural weighting fields for other aggregations,
+                // or when the category does not aggregate outcomes.
+                if ($parent_category->aggregation != GRADE_AGGREGATE_SUM ||
+                        !$parent_category->aggregateoutcomes) {
                     if ($mform->elementExists('weightoverride')) {
                         $mform->removeElement('weightoverride');
                     }
index d33be86..40aa1c5 100644 (file)
@@ -467,27 +467,69 @@ function hide_natural_aggregation_upgrade_notice($courseid) {
     unset_config('show_sumofgrades_upgrade_' . $courseid);
 }
 
+/**
+ * Hide warning about changed grades during upgrade to 2.8.
+ *
+ * @param int $courseid The current course id.
+ */
+function hide_aggregatesubcats_upgrade_notice($courseid) {
+    unset_config('show_aggregatesubcats_upgrade_' . $courseid);
+}
+
 /**
  * Print warning about changed grades during upgrade to 2.8.
  *
  * @param int $courseid The current course id.
  * @param context $context The course context.
+ * @param string $thispage The relative path for the current page. E.g. /grade/report/user/index.php
  * @param boolean $return return as string
  *
  * @return nothing or string if $return true
  */
-function print_natural_aggregation_upgrade_notice($courseid, $context, $return=false) {
+function print_natural_aggregation_upgrade_notice($courseid, $context, $thispage, $return=false) {
     global $OUTPUT;
     $html = '';
-    $show = get_config('core', 'show_sumofgrades_upgrade_' . $courseid);
 
-    if ($show) {
+    $hidesubcatswarning = optional_param('seenaggregatesubcatsupgradedgrades', false, PARAM_BOOL) && confirm_sesskey();
+    $showsubcatswarning = get_config('core', 'show_aggregatesubcats_upgrade_' . $courseid);
+    $hidenaturalwarning = optional_param('seensumofgradesupgradedgrades', false, PARAM_BOOL) && confirm_sesskey();
+    $shownaturalwarning = get_config('core', 'show_sumofgrades_upgrade_' . $courseid);
+
+    // Do not do anything if they are not a teacher.
+    if ($hidesubcatswarning || $showsubcatswarning || $hidenaturalwarning || $shownaturalwarning) {
+        if (!has_capability('moodle/grade:manage', $context)) {
+            return '';
+        }
+    }
+
+    // Hide the warning if the user told it to go away.
+    if ($hidenaturalwarning) {
+        hide_natural_aggregation_upgrade_notice($courseid);
+    }
+    // Hide the warning if the user told it to go away.
+    if ($hidesubcatswarning) {
+        hide_aggregatesubcats_upgrade_notice($courseid);
+    }
+
+    if (!$hidenaturalwarning && $shownaturalwarning) {
         $message = get_string('sumofgradesupgradedgrades', 'grades');
-        $hidemessage = get_string('sumofgradesupgradedgradeshidemessage', 'grades');
+        $hidemessage = get_string('upgradedgradeshidemessage', 'grades');
         $urlparams = array( 'id' => $courseid,
                             'seensumofgradesupgradedgrades' => true,
                             'sesskey' => sesskey());
-        $goawayurl = new moodle_url('/grade/report/grader/index.php', $urlparams);
+        $goawayurl = new moodle_url($thispage, $urlparams);
+        $goawaybutton = $OUTPUT->single_button($goawayurl, $hidemessage, 'get');
+        $html .= $OUTPUT->notification($message, 'notifysuccess');
+        $html .= $goawaybutton;
+    }
+
+    if (!$hidesubcatswarning && $showsubcatswarning) {
+        $message = get_string('aggregatesubcatsupgradedgrades', 'grades');
+        $hidemessage = get_string('upgradedgradeshidemessage', 'grades');
+        $urlparams = array( 'id' => $courseid,
+                            'seenaggregatesubcatsupgradedgrades' => true,
+                            'sesskey' => sesskey());
+        $goawayurl = new moodle_url($thispage, $urlparams);
         $goawaybutton = $OUTPUT->single_button($goawayurl, $hidemessage, 'get');
         $html .= $OUTPUT->notification($message, 'notifysuccess');
         $html .= $goawaybutton;
@@ -853,6 +895,11 @@ function print_grade_page_head($courseid, $active_type, $active_plugin=null,
         }
     }
 
+    $returnval .= print_natural_aggregation_upgrade_notice($courseid,
+                                                           context_course::instance($courseid),
+                                                           $PAGE->url,
+                                                           $return);
+
     if ($return) {
         return $returnval;
     }
@@ -1517,16 +1564,17 @@ class grade_structure {
      *
      * @param array  $element An array representing an element in the grade_tree
      * @param object $gpr A grade_plugin_return object
-     * @return string
+     * @param bool $returnactionmenulink return the instance of action_menu_link instead of string
+     * @return string|action_menu_link
      */
-    public function get_reset_icon($element, $gpr) {
+    public function get_reset_icon($element, $gpr, $returnactionmenulink = false) {
         global $CFG, $OUTPUT;
 
         // Limit to category items set to use the natural weights aggregation method, and users
         // with the capability to manage grades.
         if ($element['type'] != 'category' || $element['object']->aggregation != GRADE_AGGREGATE_SUM ||
                 !has_capability('moodle/grade:manage', $this->context)) {
-            return '';
+            return $returnactionmenulink ? null : '';
         }
 
         $str = get_string('resetweights', 'grades', $this->get_params_for_iconstr($element));
@@ -1537,7 +1585,12 @@ class grade_structure {
             'sesskey' => sesskey(),
         ));
 
-        return $OUTPUT->action_icon($gpr->add_url_params($url), new pix_icon('t/reset', $str));
+        if ($returnactionmenulink) {
+            return new action_menu_link_secondary($gpr->add_url_params($url), new pix_icon('t/reset', $str),
+                get_string('resetweightsshort', 'grades'));
+        } else {
+            return $OUTPUT->action_icon($gpr->add_url_params($url), new pix_icon('t/reset', $str));
+        }
     }
 
     /**
@@ -1545,17 +1598,17 @@ class grade_structure {
      *
      * @param array  $element An array representing an element in the grade_tree
      * @param object $gpr A grade_plugin_return object
-     *
-     * @return string
+     * @param bool $returnactionmenulink return the instance of action_menu_link instead of string
+     * @return string|action_menu_link
      */
-    public function get_edit_icon($element, $gpr) {
+    public function get_edit_icon($element, $gpr, $returnactionmenulink = false) {
         global $CFG, $OUTPUT;
 
         if (!has_capability('moodle/grade:manage', $this->context)) {
             if ($element['type'] == 'grade' and has_capability('moodle/grade:edit', $this->context)) {
                 // oki - let them override grade
             } else {
-                return '';
+                return $returnactionmenulink ? null : '';
             }
         }
 
@@ -1609,10 +1662,16 @@ class grade_structure {
         }
 
         if ($url) {
-            return $OUTPUT->action_icon($gpr->add_url_params($url), new pix_icon('t/edit', $stredit));
+            if ($returnactionmenulink) {
+                return new action_menu_link_secondary($gpr->add_url_params($url),
+                    new pix_icon('t/edit', $stredit),
+                    get_string('editsettings'));
+            } else {
+                return $OUTPUT->action_icon($gpr->add_url_params($url), new pix_icon('t/edit', $stredit));
+            }
 
         } else {
-            return '';
+            return $returnactionmenulink ? null : '';
         }
     }
 
@@ -1621,19 +1680,19 @@ class grade_structure {
      *
      * @param array  $element An array representing an element in the grade_tree
      * @param object $gpr A grade_plugin_return object
-     *
-     * @return string
+     * @param bool $returnactionmenulink return the instance of action_menu_link instead of string
+     * @return string|action_menu_link
      */
-    public function get_hiding_icon($element, $gpr) {
+    public function get_hiding_icon($element, $gpr, $returnactionmenulink = false) {
         global $CFG, $OUTPUT;
 
         if (!$element['object']->can_control_visibility()) {
-            return '';
+            return $returnactionmenulink ? null : '';
         }
 
         if (!has_capability('moodle/grade:manage', $this->context) and
             !has_capability('moodle/grade:hide', $this->context)) {
-            return '';
+            return $returnactionmenulink ? null : '';
         }
 
         $strparams = $this->get_params_for_iconstr($element);
@@ -1656,11 +1715,19 @@ class grade_structure {
 
             $url->param('action', 'show');
 
-            $hideicon = $OUTPUT->action_icon($url, new pix_icon('t/'.$type, $tooltip, 'moodle', array('alt'=>$strshow, 'class'=>'smallicon')));
+            if ($returnactionmenulink) {
+                $hideicon = new action_menu_link_secondary($url, new pix_icon('t/'.$type, $tooltip), get_string('show'));
+            } else {
+                $hideicon = $OUTPUT->action_icon($url, new pix_icon('t/'.$type, $tooltip, 'moodle', array('alt'=>$strshow, 'class'=>'smallicon')));
+            }
 
         } else {
             $url->param('action', 'hide');
-            $hideicon = $OUTPUT->action_icon($url, new pix_icon('t/hide', $strhide));
+            if ($returnactionmenulink) {
+                $hideicon = new action_menu_link_secondary($url, new pix_icon('t/hide', $strhide), get_string('hide'));
+            } else {
+                $hideicon = $OUTPUT->action_icon($url, new pix_icon('t/hide', $strhide));
+            }
         }
 
         return $hideicon;
@@ -1728,13 +1795,13 @@ class grade_structure {
      *
      * @param array  $element An array representing an element in the grade_tree
      * @param object $gpr A grade_plugin_return object
-     *
-     * @return string
+     * @param bool $returnactionmenulink return the instance of action_menu_link instead of string
+     * @return string|action_menu_link
      */
-    public function get_calculation_icon($element, $gpr) {
+    public function get_calculation_icon($element, $gpr, $returnactionmenulink = false) {
         global $CFG, $OUTPUT;
         if (!has_capability('moodle/grade:manage', $this->context)) {
-            return '';
+            return $returnactionmenulink ? null : '';
         }
 
         $type   = $element['type'];
@@ -1757,11 +1824,17 @@ class grade_structure {
 
                 $url = new moodle_url('/grade/edit/tree/calculation.php', array('courseid' => $this->courseid, 'id' => $object->id));
                 $url = $gpr->add_url_params($url);
-                return $OUTPUT->action_icon($url, new pix_icon($icon, $streditcalculation));
+                if ($returnactionmenulink) {
+                    return new action_menu_link_secondary($url,
+                        new pix_icon($icon, $streditcalculation),
+                        get_string('editcalculation', 'grades'));
+                } else {
+                    return $OUTPUT->action_icon($url, new pix_icon($icon, $streditcalculation));
+                }
             }
         }
 
-        return '';
+        return $returnactionmenulink ? null : '';
     }
 }
 
index b45dc24..316a98f 100644 (file)
@@ -89,6 +89,7 @@ function grade_get_course_grades($courseid, $userid_or_ids=null) {
             $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
             $grade->usermodified   = $grade_grades[$userid]->usermodified;
             $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
+            $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
 
             // create text representation of grade
             if ($grade_item->needsupdate) {
index 2e7022b..822a758 100644 (file)
@@ -128,13 +128,6 @@ $reportname = get_string('pluginname', 'gradereport_grader');
 // Print header
 print_grade_page_head($COURSE->id, 'report', 'grader', $reportname, false, $buttons);
 
-// Hide the following warning if the user told it to go away.
-if (optional_param('seensumofgradesupgradedgrades', false, PARAM_BOOL) && confirm_sesskey()) {
-    hide_natural_aggregation_upgrade_notice($courseid);
-}
-// This shows a notice about the upgrade to Natural aggregation.
-print_natural_aggregation_upgrade_notice($COURSE->id, $context);
-
 //Initialise the grader report object that produces the table
 //the class grade_report_grader_ajax was removed as part of MDL-21562
 $report = new grade_report_grader($courseid, $gpr, $context, $page, $sortitemid);
index 374c742..dca2b93 100644 (file)
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js differ
index 56aa1a3..d324e6f 100644 (file)
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js differ
index fe61924..212ab0b 100644 (file)
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js differ
index 21000d8..faf7b3b 100644 (file)
@@ -203,6 +203,15 @@ FloatingHeaders.prototype = {
      */
     firstUserCellWidth: 0,
 
+    /**
+     * The width of the dock if it is visible.
+     *
+     * @property dockWidth
+     * @type Number
+     * @protected
+     */
+    dockWidth: 0,
+
     /**
      * The position of the top of the final user cell.
      * This is used when processing the scroll event as an optimisation. It must be updated when
@@ -303,6 +312,13 @@ FloatingHeaders.prototype = {
             this.footerRowPosition = this.tableFooterRow.getY();
         }
 
+        // Add the width of the dock if it is visible.
+        this.dockWidth = 0;
+        var dock = Y.one('.has_dock #dock');
+        if (dock) {
+            this.dockWidth = dock.get(OFFSETWIDTH);
+        }
+
         var userCellList = Y.all(SELECTORS.USERCELL);
 
         // The left of the user cells matches the left of the headerRow.
@@ -436,7 +452,9 @@ FloatingHeaders.prototype = {
             // Listen for window scrolls, resizes, and rotation events.
             Y.one(Y.config.win).on('scroll', this._handleScrollEvent, this),
             Y.one(Y.config.win).on('resize', this._handleResizeEvent, this),
-            Y.one(Y.config.win).on('orientationchange', this._handleResizeEvent, this)
+            Y.one(Y.config.win).on('orientationchange', this._handleResizeEvent, this),
+            Y.Global.on('dock:shown', this._handleResizeEvent, this),
+            Y.Global.on('dock:hidden', this._handleResizeEvent, this)
         );
     },
 
@@ -451,7 +469,7 @@ FloatingHeaders.prototype = {
         var userColumn = Y.all(SELECTORS.USERCELL),
 
         // Create a floating table.
-            floatingUserColumn = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater"></div>'),
+            floatingUserColumn = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater sideonly"></div>'),
 
         // Get the XY for the floating element.
             coordinates = this._getRelativeXY(this.firstUserCell);
@@ -498,7 +516,7 @@ FloatingHeaders.prototype = {
         this.headerCell = Y.one(SELECTORS.STUDENTHEADER);
 
         // Create the floating row and cell.
-        var floatingUserHeaderRow = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater heading"></div>'),
+        var floatingUserHeaderRow = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater sideonly heading"></div>'),
             floatingUserHeaderCell = Y.Node.create('<div></div>'),
             nodepos = this._getRelativeXY(this.headerCell)[0],
             coordinates = this._getRelativeXY(this.headerRow),
@@ -678,7 +696,7 @@ FloatingHeaders.prototype = {
         }
 
         // Create the floating row and cell.
-        var floatingRow = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater"></div>'),
+        var floatingRow = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater sideonly"></div>'),
             floatingCell = Y.Node.create('<div></div>'),
             coordinates = this._getRelativeXY(origin),
             width = this.firstUserCell.getComputedStyle(WIDTH),
@@ -761,14 +779,14 @@ FloatingHeaders.prototype = {
 
         // User column position.
         if (right_to_left()) {
-            floatingUserTriggerPoint = Y.config.win.innerWidth + Y.config.win.pageXOffset;
+            floatingUserTriggerPoint = Y.config.win.innerWidth + Y.config.win.pageXOffset - this.dockWidth;
             floatingUserRelativePoint = floatingUserTriggerPoint - this.firstUserCellWidth;
             userFloats = floatingUserTriggerPoint < (this.firstUserCellLeft + this.firstUserCellWidth);
             leftTitleFloats = (floatingUserTriggerPoint - this.firstNonUserCellWidth) <
                               (this.firstNonUserCellLeft + this.firstUserCellWidth);
         } else {
-            floatingUserTriggerPoint = Y.config.win.pageXOffset;
-            floatingUserRelativePoint = floatingUserTriggerPoint;
+            floatingUserRelativePoint = Y.config.win.pageXOffset;
+            floatingUserTriggerPoint = floatingUserRelativePoint + this.dockWidth;
             userFloats = floatingUserTriggerPoint > this.firstUserCellLeft;
             leftTitleFloats = floatingUserTriggerPoint > (this.firstNonUserCellLeft - this.firstUserCellWidth);
         }
index 3409eac..04c308f 100644 (file)
@@ -63,6 +63,12 @@ class tablelog extends \table_sql implements \renderable {
      */
     protected $cms;
 
+    /**
+     * @var int The default number of decimal points to use in this course
+     * when a grade item does not itself define the number of decimal points.
+     */
+    protected $defaultdecimalpoints;
+
     /**
      * Sets up the table_log parameters.
      *
@@ -83,6 +89,7 @@ class tablelog extends \table_sql implements \renderable {
      */
     public function __construct($uniqueid, \context_course $context, $url, $filters = array(), $download = '', $page = 0,
                                 $perpage = 100) {
+        global $CFG;
         parent::__construct($uniqueid);
 
         $this->set_attribute('class', 'gradereport_history generaltable generalbox');
@@ -96,6 +103,7 @@ class tablelog extends \table_sql implements \renderable {
         $this->gradeitems = \grade_item::fetch_all(array('courseid' => $this->courseid));
         $this->cms = get_fast_modinfo($this->courseid);
         $this->useridfield = 'userid';
+        $this->defaultdecimalpoints = grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
 
         // Define columns in the table.
         $this->define_table_columns();
@@ -167,6 +175,40 @@ class tablelog extends \table_sql implements \renderable {
         $this->define_headers(array_values($cols));
     }
 
+    /**
+     * Method to display the final grade.
+     *
+     * @param \stdClass $history an entry of history record.
+     *
+     * @return string HTML to display
+     */
+    public function col_finalgrade(\stdClass $history) {
+        if (!empty($this->gradeitems[$history->itemid])) {
+            $decimalpoints = $this->gradeitems[$history->itemid]->get_decimals();
+        } else {
+            $decimalpoints = $this->defaultdecimalpoints;
+        }
+
+        return format_float($history->finalgrade, $decimalpoints);
+    }
+
+    /**
+     * Method to display the previous grade.
+     *
+     * @param \stdClass $history an entry of history record.
+     *
+     * @return string HTML to display
+     */
+    public function col_prevgrade(\stdClass $history) {
+        if (!empty($this->gradeitems[$history->itemid])) {
+            $decimalpoints = $this->gradeitems[$history->itemid]->get_decimals();
+        } else {
+            $decimalpoints = $this->defaultdecimalpoints;
+        }
+
+        return format_float($history->prevgrade, $decimalpoints);
+    }
+
     /**
      * Method to display column timemodifed.
      *
index 3813c77..eb1ebf9 100644 (file)
@@ -48,7 +48,7 @@ if (!isset($USER->grade_last_report)) {
 }
 $USER->grade_last_report[$course->id] = 'history';
 
-$select = "itemtype != 'course' AND itemname != '' AND courseid = :courseid";
+$select = "itemtype <> 'course' AND courseid = :courseid AND " . $DB->sql_isnotempty('grade_items', 'itemname', true, true);
 $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;
 
index a305c9a..a53eb2a 100644 (file)
Binary files a/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-debug.js and b/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-debug.js differ
index 6efa157..8d69f0d 100644 (file)
Binary files a/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-min.js and b/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector-min.js differ
index 3209946..390997d 100644 (file)
Binary files a/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector.js and b/grade/report/history/yui/build/moodle-gradereport_history-userselector/moodle-gradereport_history-userselector.js differ
index 38f662e..0622cb9 100644 (file)
@@ -641,6 +641,10 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
         if (this._userTabFocus) {
             this._userTabFocus.setAttribute('tabindex', '-1');
         }
+        if (!user) {
+            // We were not passed a user, there is apparently none in the dialogue. Nothing to do here \\\o/.
+            return;
+        }
 
         this._userTabFocus = user.one(SELECTORS.USERSELECT);
         this._userTabFocus.setAttribute('tabindex', '0');
index 21f04b9..b0aa35b 100644 (file)
@@ -490,8 +490,8 @@ abstract class grade_report {
                 array_key_exists($course_item->id, $hiding_affected['alteredgrademax']) ||
                 array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus']) ||
                 array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
-            if (!$this->showtotalsifcontainhidden[$courseid]) {
-                //hide the grade
+            if (!$this->showtotalsifcontainhidden[$courseid] && array_key_exists($course_item->id, $hiding_affected['altered'])) {
+                // Hide the grade, but only when it has changed.
                 $finalgrade = null;
             } else {
                 //use reprocessed marks that exclude hidden items
@@ -510,6 +510,13 @@ abstract class grade_report {
                 if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
                     $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id];
                 }
+
+                if (!$this->showtotalsifcontainhidden[$courseid]) {
+                    // If the course total is hidden we must hide the weight otherwise
+                    // it can be used to compute the course total.
+                    $aggregationstatus = 'unknown';
+                    $aggregationweight = null;
+                }
             }
         } else if (!empty($hiding_affected['unknown'][$course_item->id])) {
             //not sure whether or not this item depends on a hidden item
index 0cb0321..ad20cdd 100644 (file)
@@ -108,9 +108,7 @@ class grade extends tablelike implements selectable_items, filterable_items {
     public function original_definition() {
         $def = array('finalgrade', 'feedback');
 
-        if ($this->requiresextra) {
-            $def[] = 'override';
-        }
+        $def[] = 'override';
 
         $def[] = 'exclude';
 
index cdd56d9..c588d6e 100644 (file)
@@ -33,6 +33,7 @@ use gradereport_singleview\local\ui\range;
 use gradereport_singleview\local\ui\bulk_insert;
 use grade_item;
 use grade_grade;
+use stdClass;
 
 defined('MOODLE_INTERNAL') || die;
 
@@ -122,8 +123,6 @@ class user extends tablelike implements selectable_items {
 
         $this->requirespaging = count($this->items) > $this->perpage;
 
-        unset($seq);
-
         $this->setup_structure();
 
         $this->definition = array(
@@ -179,16 +178,26 @@ class user extends tablelike implements selectable_items {
         if (isset($item->cmid)) {
             $realmodid = $item->cmid;
         }
-        $url = new moodle_url('/mod/' . $item->itemmodule . '/view.php', array('id' => $realmodid));
+
         $iconstring = get_string('filtergrades', 'gradereport_singleview', $item->get_name());
+
+        // Create a fake gradetreeitem so we can call get_element_header().
+        // The type logic below is from grade_category->_get_children_recursion().
+        $gradetreeitem = array();
+        if (in_array($item->itemtype, array('course', 'category'))) {
+            $gradetreeitem['type'] = $item->itemtype.'item';
+        } else {
+            $gradetreeitem['type'] = 'item';
+        }
+        $gradetreeitem['object'] = $item;
+        $gradetreeitem['userid'] = $this->item->id;
+
+        $itemlabel = $this->structure->get_element_header($gradetreeitem, true, false);
         $grade->label = $item->get_name();
 
         $line = array(
             $OUTPUT->action_icon($this->format_link('grade', $item->id), new pix_icon('t/editstring', $iconstring)),
-            $this->format_icon($item) . $lockicon,
-            html_writer::link($url, $item->get_name()),
-            $this->category($item),
-            (new range($item))
+            $this->format_icon($item) . $lockicon, $itemlabel, $this->category($item), (new range($item))
         );
         return $this->format_definition($line, $grade);
     }
index a4d3545..2b5bd32 100644 (file)
@@ -33,6 +33,7 @@ $string['exclude'] = 'Exclude';
 $string['excludeall'] = 'Exclude all grades';
 $string['excludefor'] = 'Exclude for {$a}';
 $string['excludenone'] = 'Exclude no grades';
+$string['eventgradereportviewed'] = 'Grade single view report viewed.';
 $string['feedbackfor'] = 'Feedback for {$a}';
 $string['filtergrades'] = 'Show grades for {$a}.';
 $string['gradefor'] = 'Grade for {$a}';
index f8f8090..8683961 100644 (file)
@@ -15,6 +15,9 @@ Feature: We can use Single view
       | student2 | Student | 2 | student1@asd.com | s2 | holly |
       | student3 | Student | 3 | student1@asd.com | s3 | anna |
       | student4 | Student | 4 | student1@asd.com | s4 | zac |
+    And the following "grade items" exist:
+      | itemname | course  |
+      | new grade item 1 | C1 |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
@@ -36,7 +39,7 @@ Feature: We can use Single view
     And I follow "Course 1"
     And I follow "Grades"
 
- @javascript
 @javascript
   Scenario: I can update grades, add feedback and exclude grades.
     Given I click on "Single view" "option"
     And I click on "Student 4" "option"
@@ -71,10 +74,8 @@ Feature: We can use Single view
     And I follow "Single view for Student 1"
     Then I should see "Student 1"
 
-  @javascript
   Scenario: Navigation works in the Single view.
-    Given I click on "Single view" "option"
-    Then I click on "Student 1" "option"
+    Given I follow "Single view for Student 1"
     Then I should see "Student 1"
     And I follow "Student 2"
     Then I should see "Student 2"
@@ -86,3 +87,10 @@ Feature: We can use Single view
     Then I should see "Test assignment three"
     And I follow "Test assignment four"
     Then I should see "Test assignment four"
+
+  Scenario: Activities are clickable only when
+    it has a valid activity page.
+    Given I follow "Single view for Student 1"
+    And "new grade item 1" "link" should not exist in the "//tbody//tr[position()=1]//td[position()=2]" "xpath_element"
+    Then "Category total" "link" should not exist in the "//tbody//tr[position()=2]//td[position()=2]" "xpath_element"
+    And "Course total" "link" should not exist in the "//tbody//tr[position()=last()]//td[position()=2]" "xpath_element"
index 39d7988..e02c99f 100644 (file)
@@ -236,16 +236,6 @@ class grade_report_user extends grade_report {
         // Grab the grade_tree for this course
         $this->gtree = new grade_tree($this->courseid, false, $this->switch, null, !$CFG->enableoutcomes);
 
-        // Determine the number of rows and indentation
-        $this->maxdepth = 1;
-        $this->inject_rowspans($this->gtree->top_element);
-        $this->maxdepth++; // Need to account for the lead column that spans all children
-        for ($i = 1; $i <= $this->maxdepth; $i++) {
-            $this->evenodd[$i] = 0;
-        }
-
-        $this->tabledata = array();
-
         // Get the user (for full name).
         $this->user = $DB->get_record('user', array('id' => $userid));
 
@@ -259,6 +249,16 @@ class grade_report_user extends grade_report {
             $this->canviewhidden = has_capability('moodle/grade:viewhidden', $coursecontext);
         }
 
+        // Determine the number of rows and indentation.
+        $this->maxdepth = 1;
+        $this->inject_rowspans($this->gtree->top_element);
+        $this->maxdepth++; // Need to account for the lead column that spans all children.
+        for ($i = 1; $i <= $this->maxdepth; $i++) {
+            $this->evenodd[$i] = 0;
+        }
+
+        $this->tabledata = array();
+
         // base url for sorting by first/last name
         $this->baseurl = $CFG->wwwroot.'/grade/report?id='.$courseid.'&amp;userid='.$userid;
         $this->pbarurl = $this->baseurl;
@@ -287,7 +287,15 @@ class grade_report_user extends grade_report {
         $count = 1;
 
         foreach ($element['children'] as $key=>$child) {
-            $count += $this->inject_rowspans($element['children'][$key]);
+            // If category is hidden then do not include it in the rowspan.
+            if ($child['type'] == 'category' && $child['object']->is_hidden() && !$this->canviewhidden
+                    && ($this->showhiddenitems == GRADE_REPORT_USER_HIDE_HIDDEN
+                    || ($this->showhiddenitems == GRADE_REPORT_USER_HIDE_UNTIL && !$child['object']->is_hiddenuntil()))) {
+                // Just calculate the rowspans for children of this category, don't add them to the count.
+                $this->inject_rowspans($element['children'][$key]);
+            } else {
+                $count += $this->inject_rowspans($element['children'][$key]);
+            }
         }
 
         $element['rowspan'] = $count;
@@ -438,6 +446,32 @@ class grade_report_user extends grade_report {
                 }
             }
 
+            // Actual Grade - We need to calculate this whether the row is hidden or not.
+            $gradeval = $grade_grade->finalgrade;
+            $hint = $grade_grade->get_aggregation_hint();
+            if (!$this->canviewhidden) {
+                /// Virtual Grade (may be calculated excluding hidden items etc).
+                $adjustedgrade = $this->blank_hidden_total_and_adjust_bounds($this->courseid,
+                                                                             $grade_grade->grade_item,
+                                                                             $gradeval);
+
+                $gradeval = $adjustedgrade['grade'];
+
+                // We temporarily adjust the view of this grade item - because the min and
+                // max are affected by the hidden values in the aggregation.
+                $grade_grade->grade_item->grademax = $adjustedgrade['grademax'];
+                $grade_grade->grade_item->grademin = $adjustedgrade['grademin'];
+                $hint['status'] = $adjustedgrade['aggregationstatus'];
+                $hint['weight'] = $adjustedgrade['aggregationweight'];
+            } else {
+                // The max and min for an aggregation may be different to the grade_item.
+                if (!is_null($gradeval)) {
+                    $grade_grade->grade_item->grademax = $grade_grade->rawgrademax;
+                    $grade_grade->grade_item->grademin = $grade_grade->rawgrademin;
+                }
+            }
+
+
             if (!$hide) {
                 /// Excluded Item
                 /**
@@ -465,31 +499,6 @@ class grade_report_user extends grade_report {
                 $data['itemname']['celltype'] = 'th';
                 $data['itemname']['id'] = $header_row;
 
-                /// Actual Grade
-                $gradeval = $grade_grade->finalgrade;
-                $hint = $grade_grade->get_aggregation_hint();
-                if (!$this->canviewhidden) {
-                    /// Virtual Grade (may be calculated excluding hidden items etc).
-                    $adjustedgrade = $this->blank_hidden_total_and_adjust_bounds($this->courseid,
-                                                                                 $grade_grade->grade_item,
-                                                                                 $gradeval);
-
-                    $gradeval = $adjustedgrade['grade'];
-
-                    // We temporarily adjust the view of this grade item - because the min and
-                    // max are affected by the hidden values in the aggregation.
-                    $grade_grade->grade_item->grademax = $adjustedgrade['grademax'];
-                    $grade_grade->grade_item->grademin = $adjustedgrade['grademin'];
-                    $hint['status'] = $adjustedgrade['aggregationstatus'];
-                    $hint['weight'] = $adjustedgrade['aggregationweight'];
-                } else {
-                    // The max and min for an aggregation may be different to the grade_item.
-                    if (!is_null($gradeval)) {
-                        $grade_grade->grade_item->grademax = $grade_grade->rawgrademax;
-                        $grade_grade->grade_item->grademin = $grade_grade->rawgrademin;
-                    }
-                }
-
                 if ($this->showfeedback) {
                     // Copy $class before appending itemcenter as feedback should not be centered
                     $classfeedback = $class;
@@ -641,17 +650,20 @@ class grade_report_user extends grade_report {
                     $data['contributiontocoursetotal']['content'] = '-';
                     $data['contributiontocoursetotal']['headers'] = "$header_cat $header_row contributiontocoursetotal";
 
-                    $hint['grademax'] = $grade_grade->grade_item->grademax;
-                    $hint['grademin'] = $grade_grade->grade_item->grademin;
-                    $hint['grade'] = $gradeval;
-                    $parent = $grade_object->load_parent_category();
-                    if ($grade_object->is_category_item()) {
-                        $parent = $parent->load_parent_category();
-                    }
-                    $hint['parent'] = $parent->load_grade_item()->id;
-                    $this->aggregationhints[$grade_grade->itemid] = $hint;
                 }
             }
+            // We collect the aggregation hints whether they are hidden or not.
+            if ($this->showcontributiontocoursetotal) {
+                $hint['grademax'] = $grade_grade->grade_item->grademax;
+                $hint['grademin'] = $grade_grade->grade_item->grademin;
+                $hint['grade'] = $gradeval;
+                $parent = $grade_object->load_parent_category();
+                if ($grade_object->is_category_item()) {
+                    $parent = $parent->load_parent_category();
+                }
+                $hint['parent'] = $parent->load_grade_item()->id;
+                $this->aggregationhints[$grade_grade->itemid] = $hint;
+            }
         }
 
         /// Category
@@ -727,9 +739,14 @@ class grade_report_user extends grade_report {
 
                 // Multiply the normalised value by the weight
                 // of all the categories higher in the tree.
+                $parent = null;
                 do {
                     if (!is_null($this->aggregationhints[$itemid]['weight'])) {
                         $gradeval *= $this->aggregationhints[$itemid]['weight'];
+                    } else if (empty($parent)) {
+                        // If we are in the first loop, and the weight is null, then we cannot calculate the contribution.
+                        $gradeval = null;
+                        break;
                     }
 
                     // The second part of this if is to prevent infinite loops
@@ -743,8 +760,12 @@ class grade_report_user extends grade_report {
                         $parent = false;
                     }
                 } while ($parent);
+
                 // Finally multiply by the course grademax.
-                $gradeval *= $this->aggregationhints[$itemid]['grademax'];
+                if (!is_null($gradeval)) {
+                    // Convert to percent.
+                    $gradeval *= 100;
+                }
 
                 // Now we need to loop through the "built" table data and update the
                 // contributions column for the current row.
@@ -752,8 +773,12 @@ class grade_report_user extends grade_report {
                 foreach ($this->tabledata as $key => $row) {
                     if (isset($row['itemname']) && ($row['itemname']['id'] == $header_row)) {
                         // Found it - update the column.
-                        $decimals = $grade_object->get_decimals();
-                        $this->tabledata[$key]['contributiontocoursetotal']['content'] = format_float($gradeval, $decimals, true);
+                        $content = '-';
+                        if (!is_null($gradeval)) {
+                            $decimals = $grade_object->get_decimals();
+                            $content = format_float($gradeval, $decimals, true) . ' %';
+                        }
+                        $this->tabledata[$key]['contributiontocoursetotal']['content'] = $content;
                         break;
                     }
                 }
index 6272c83..46a7c3f 100644 (file)
@@ -59,14 +59,49 @@ class behat_grade extends behat_base {
      * @return Given[]
      */
     public function i_set_the_following_settings_for_grade_item($gradeitem, TableNode $data) {
+
+        $steps = array();
+        $gradeitem = $this->getSession()->getSelectorsHandler()->xpathLiteral($gradeitem);
+
+        if ($this->running_javascript()) {
+            $xpath = "//tr[contains(.,$gradeitem)]//*[contains(@class,'moodle-actionmenu')]//a[contains(@class,'toggle-display')]";
+            if ($this->getSession()->getPage()->findAll('xpath', $xpath)) {
+                $steps[] = new Given('I click on "' . $this->escape($xpath) . '" "xpath_element"');
+            }
+        }
+
         $savechanges = get_string('savechanges', 'grades');
         $edit = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('edit') . '  ');
-        $gradeitem = $this->getSession()->getSelectorsHandler()->xpathLiteral($gradeitem);
         $linkxpath = "//a[./img[starts-with(@title,$edit) and contains(@title,$gradeitem)]]";
-        return array(
-            new Given('I click on "' . $this->escape($linkxpath) . '" "xpath_element"'),
-            new Given('I set the following fields to these values:', $data),
-            new Given('I press "' . $this->escape($savechanges) . '"'),
-        );
+        $steps[] = new Given('I click on "' . $this->escape($linkxpath) . '" "xpath_element"');
+        $steps[] = new Given('I set the following fields to these values:', $data);
+        $steps[] = new Given('I press "' . $this->escape($savechanges) . '"');
+        return $steps;
+    }
+
+    /**
+     * Resets the weights for the grade category
+     *
+     * Teacher must be on the grade setup page.
+     *
+     * @Given /^I reset weights for grade category "(?P<grade_item_string>(?:[^"]|\\")*)"$/
+     * @param $gradeitem
+     * @return array
+     */
+    public function i_reset_weights_for_grade_category($gradeitem) {
+
+        $steps = array();
+
+        if ($this->running_javascript()) {
+            $gradeitemliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($gradeitem);
+            $xpath = "//tr[contains(.,$gradeitemliteral)]//*[contains(@class,'moodle-actionmenu')]//a[contains(@class,'toggle-display')]";
+            if ($this->getSession()->getPage()->findAll('xpath', $xpath)) {
+                $steps[] = new Given('I click on "' . $this->escape($xpath) . '" "xpath_element"');
+            }
+        }
+
+        $linktext = get_string('resetweights', 'grades', (object)array('itemname' => $gradeitem));
+        $steps[] = new Given('I click on "' . $this->escape($linktext) . '" "link"');
+        return $steps;
     }
 }
index 40e7063..e545f61 100644 (file)
@@ -311,8 +311,6 @@ Feature: We can use calculated grade totals
     And I set the following settings for grade item "Course 1":
       | Aggregation                     | Natural |
       | Include outcomes in aggregation | 0       |
-    And I set the following settings for grade item "Test outcome item one":
-     | Extra credit     | 0   |
     And I log out
     And I log in as "student1"
     And I follow "Course 1"
@@ -394,16 +392,16 @@ Feature: We can use calculated grade totals
     And I set the field "Select all or one user" to "Student 1"
     And the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Range | Contribution to course total |
-      | Test assignment five | 28.57 % | 10.00 (50.00 %) | 0–20 | 6.43 |
-      | Test assignment six | 50.00 % | 5.00 (50.00 %) | 0–10 | 11.25 |
-      | Test assignment seven | 21.43 % | - | 0–15 | 0.00 |
-      | Test assignment eight | 66.67 % | 10.00 (50.00 %) | 0–20 | 10.00 |
-      | Test assignment nine | 33.33 % | 5.00 (50.00 %) | 0–10 | 5.00 |
-      | Test assignment ten | -( Empty ) | - | 0–15 | 0.00 |
-      | Test assignment one | 48.00 % | 60.00 (20.00 %) | 0–300 | 60.00 |
-      | Test assignment two | 16.00 % | 20.00 (20.00 %) | 0–100 | 20.00 |
-      | Test assignment three | 24.00 %( Extra credit ) | 40.00 (26.67 %) | 0–150 | 40.00 |
-      | Test assignment four | 24.00 % | - | 0–150 | 0.00 |
+      | Test assignment five | 28.57 % | 10.00 (50.00 %) | 0–20 | 1.03 % |
+      | Test assignment six | 50.00 % | 5.00 (50.00 %) | 0–10 | 1.80 % |
+      | Test assignment seven | 21.43 % | - | 0–15 | 0.00 |
+      | Test assignment eight | 66.67 % | 10.00 (50.00 %) | 0–20 | 1.60 % |
+      | Test assignment nine | 33.33 % | 5.00 (50.00 %) | 0–10 | 0.80 % |
+      | Test assignment ten | 0.00 %( Empty ) | - | 0–15 | 0.00 % |
+      | Test assignment one | 48.00 % | 60.00 (20.00 %) | 0–300 | 9.60 % |
+      | Test assignment two | 16.00 % | 20.00 (20.00 %) | 0–100 | 3.20 % |
+      | Test assignment three | 24.00 %( Extra credit ) | 40.00 (26.67 %) | 0–150 | 6.40 % |
+      | Test assignment four | 24.00 % | - | 0–150 | 0.00 |
     And I log out
     And I log in as "student1"
     And I follow "Course 1"
@@ -413,13 +411,13 @@ Feature: We can use calculated grade totals
     And I set the field "Grade report" to "User report"
     And the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Range | Contribution to course total |
-      | Test assignment six | 70.00 % | 5.00 (50.00 %) | 0–10 | 8.75 |
-      | Test assignment seven | 30.00 % | - | 0–15 | 0.00 |
-      | Test assignment nine | 100.00 % | 5.00 (50.00 %) | 0–10 | 5.00 |
-      | Test assignment ten | -( Empty ) | - | 0–15 | 0.00 |
-      | Test assignment one | 61.86 % | 60.00 (20.00 %) | 0–300 | 60.0 |
-      | Test assignment three | 30.93 %( Extra credit ) | 40.00 (26.67 %) | 0–150 | 40.0 |
-      | Test assignment four | 30.93 % | - | 0–150 | 0.00 |
+      | Test assignment six | 70.00 % | 5.00 (50.00 %) | 0–10 | 1.80 % |
+      | Test assignment seven | 30.00 % | - | 0–15 | 0.00 |
+      | Test assignment nine | 100.00 % | 5.00 (50.00 %) | 0–10 | 1.03 % |
+      | Test assignment ten | -( Empty ) | - | 0–15 | - |
+      | Test assignment one | 61.86 % | 60.00 (20.00 %) | 0–300 | 12.37 % |
+      | Test assignment three | 30.93 %( Extra credit ) | 40.00 (26.67 %) | 0–150 | 8.25 % |
+      | Test assignment four | 30.93 % | - | 0–150 | 0.00 |
 
   @javascript
   Scenario: Natural aggregation with drop lowest
@@ -504,15 +502,12 @@ Feature: We can use calculated grade totals
   @javascript
   Scenario: Natural aggregation from the setup screen
     And I set the field "Grade report" to "Categories and items"
-    And I follow "Edit   Course 1"
-    And I set the field "Aggregation" to "Natural"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 1"
-    And I set the field "Aggregation" to "Natural"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 2"
-    And I set the field "Aggregation" to "Natural"
-    And I press "Save changes"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation          | Natural |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Natural |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Natural |
 
     And I set the field "Override weight of Test assignment one" to "1"
     And the field "Weight of Test assignment one" matches value "37.975"
@@ -553,7 +548,7 @@ Feature: We can use calculated grade totals
     And I set the field "Override weight of Sub category 1" to "1"
     And the field "Weight of Test assignment one" matches value "37.975"
     And the field "Weight of Sub category 1" matches value "5.696"
-    And I click on "Reset weights of Sub category 2" "link"
+    And I reset weights for grade category "Sub category 2"
     And the field "Weight of Test assignment ten" matches value "33.333"
 
   @javascript
@@ -587,15 +582,15 @@ Feature: We can use calculated grade totals
     And I set the field "Select all or one user" to "Student 1"
     And the following should exist in the "user-grade" table:
       | Grade item            | Calculated weight | Grade           | Contribution to course total |
-      | Test assignment five  | 57.14 %           | 10.00 (50.00 %) | 10.00                        |
-      | Test assignment six   | 0.00 %            | 5.00 (50.00 %)  | 0.00                         |
-      | Test assignment seven | 42.86 %           | -               | 0.00                         |
-      | Test assignment eight | 0.00 %            | 10.00 (50.00 %) | 0.00                         |
-      | Test assignment nine  | 100.00 %          | 5.00 (50.00 %)  | 5.00                         |
-      | Test assignment ten   | 0.00 %            | -               | 0.00                         |
-      | Test assignment one   | 0.00 %            | 60.00 (20.00 %) | 0.00                         |
-      | Test assignment two   | 22.47 %           | 20.00 (20.00 %) | 20.00                        |
-      | Test assignment three | 33.71 %           | 40.00 (26.67 %) | 40.00                        |
+      | Test assignment five  | 57.14 %           | 10.00 (50.00 %) | 2.25 %                        |
+      | Test assignment six   | 0.00 %            | 5.00 (50.00 %)  | 0.00 %                        |
+      | Test assignment seven | 42.86 %           | -               | 0.00 %                        |
+      | Test assignment eight | 0.00 %            | 10.00 (50.00 %) | 0.00 %                        |
+      | Test assignment nine  | 100.00 %          | 5.00 (50.00 %)  | 1.12 %                         |
+      | Test assignment ten   | 0.00 %            | -               | 0.00                         |
+      | Test assignment one   | 0.00 %            | 60.00 (20.00 %) | 0.00                         |
+      | Test assignment two   | 22.47 %           | 20.00 (20.00 %) | 4.49 %                        |
+      | Test assignment three | 33.71 %           | 40.00 (26.67 %) | 8.99 %                        |
       | Test assignment four  | 33.71 %           | -               | 0.00                         |
     And I log out
     And I log in as "student1"
@@ -606,10 +601,10 @@ Feature: We can use calculated grade totals
     And I set the field "Grade report" to "User report"
     And the following should exist in the "user-grade" table:
       | Grade item            | Calculated weight | Grade           | Contribution to course total |
-      | Test assignment six   | 0.00 %            | 5.00 (50.00 %)  | 0.00                         |
-      | Test assignment seven | 100.00 %          | -               | 0.00                         |
-      | Test assignment nine  | 100.00 %          | 5.00 (50.00 %)  | 5.00                         |
-      | Test assignment ten   | 0.00              | -               | 0.00                         |
-      | Test assignment one   | 0.00 %            | 60.00 (20.00 %) | 0.00                         |
-      | Test assignment three | 46.15 %           | 40.00 (26.67 %) | 40.00                        |
-      | Test assignment four  | 46.15 %           | -               | 0.00                         |
+      | Test assignment six   | 0.00 %            | 5.00 (50.00 %)  | 0.00                         |
+      | Test assignment seven | 100.00 %          | -               | 0.00                         |
+      | Test assignment nine  | 100.00 %          | 5.00 (50.00 %)  | 1.54 %                         |
+      | Test assignment ten   | 0.00              | -               | 0.00                         |
+      | Test assignment one   | 0.00 %            | 60.00 (20.00 %) | 0.00                         |
+      | Test assignment three | 46.15 %           | 40.00 (26.67 %) | 12.31 %                        |
+      | Test assignment four  | 46.15 %           | -               | 0.00 %                        |
index fc61a26..13d1177 100644 (file)
@@ -65,12 +65,12 @@ Feature: We can understand the gradebook user report
     # Check the values in the weights column.
     Then the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Contribution to course total |
-      | Test assignment one | 25.00 % | 60.00 | 15.00 |
-      | Test assignment two | 25.00 % | 20.00 | 5.00 |
-      | Test assignment three | 25.00 % | 40.00 | 10.00 |
-      | Test assignment four | 33.33 % | 10.00 | 0.83 |
-      | Test assignment five | 33.33 % | 70.00 | 5.83 |
-      | Test assignment six | 33.33 % | 30.00 | 2.50 |
+      | Test assignment one | 25.00 % | 60.00 | 15.00 |
+      | Test assignment two | 25.00 % | 20.00 | 5.00 |
+      | Test assignment three | 25.00 % | 40.00 | 10.00 |
+      | Test assignment four | 33.33 % | 10.00 | 0.83 |
+      | Test assignment five | 33.33 % | 70.00 | 5.83 |
+      | Test assignment six | 33.33 % | 30.00 | 2.50 |
 
   @javascript
   Scenario: Weighted mean of grades aggregation
@@ -90,12 +90,12 @@ Feature: We can understand the gradebook user report
     # Check the values in the weights column.
     Then the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Contribution to course total |
-      | Test assignment one | 40.00 % | 60.00 | 24.00 |
-      | Test assignment two | 20.00 % | 20.00 | 4.00 |
-      | Test assignment three | 20.00 % | 40.00 | 8.00 |
-      | Test assignment four | 33.33 % | 10.00 | 0.67 |
-      | Test assignment five | 33.33 % | 70.00 | 4.67 |
-      | Test assignment six | 33.33 % | 30.00 | 2.00 |
+      | Test assignment one | 40.00 % | 60.00 | 24.00 |
+      | Test assignment two | 20.00 % | 20.00 | 4.00 |
+      | Test assignment three | 20.00 % | 40.00 | 8.00 |
+      | Test assignment four | 33.33 % | 10.00 | 0.67 |
+      | Test assignment five | 33.33 % | 70.00 | 4.67 |
+      | Test assignment six | 33.33 % | 30.00 | 2.00 |
 
   @javascript
   Scenario: Simple weighted mean of grades aggregation
@@ -111,12 +111,12 @@ Feature: We can understand the gradebook user report
     # Check the values in the weights column.
     Then the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Contribution to course total |
-      | Test assignment one | 33.33 % | 60.00 | 20.00 |
-      | Test assignment two | 33.33 % | 20.00 | 6.67 |
-      | Test assignment three | 33.33 %( Extra credit ) | 40.00 | 13.33 |
-      | Test assignment four | 33.33 % | 10.00 | 1.11 |
-      | Test assignment five | 33.33 % | 70.00 | 7.78 |
-      | Test assignment six | 33.33 % | 30.00 | 3.33 |
+      | Test assignment one | 33.33 % | 60.00 | 20.00 |
+      | Test assignment two | 33.33 % | 20.00 | 6.67 |
+      | Test assignment three | 33.33 %( Extra credit ) | 40.00 | 13.33 |
+      | Test assignment four | 33.33 % | 10.00 | 1.11 |
+      | Test assignment five | 33.33 % | 70.00 | 7.78 |
+      | Test assignment six | 33.33 % | 30.00 | 3.33 |
 
   @javascript
   Scenario: Mean of grades (with extra credits) aggregation
@@ -130,12 +130,12 @@ Feature: We can understand the gradebook user report
     # Check the values in the weights column.
     Then the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Contribution to course total |
-      | Test assignment one | 33.33 % | 60.00 | 20.00 |
-      | Test assignment two | 33.33 % | 20.00 | 6.67 |
-      | Test assignment three | 33.33 %( Extra credit ) | 40.00 | 13.33 |
-      | Test assignment four | 33.33 % | 10.00 | 1.11 |
-      | Test assignment five | 33.33 % | 70.00 | 7.78 |
-      | Test assignment six | 33.33 % | 30.00 | 3.33 |
+      | Test assignment one | 33.33 % | 60.00 | 20.00 |
+      | Test assignment two | 33.33 % | 20.00 | 6.67 |
+      | Test assignment three | 33.33 %( Extra credit ) | 40.00 | 13.33 |
+      | Test assignment four | 33.33 % | 10.00 | 1.11 |
+      | Test assignment five | 33.33 % | 70.00 | 7.78 |
+      | Test assignment six | 33.33 % | 30.00 | 3.33 |
 
   @javascript
   Scenario: Median of grades aggregation
@@ -147,12 +147,12 @@ Feature: We can understand the gradebook user report
     # Check the values in the weights column.
     Then the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Contribution to course total |
-      | Test assignment one | 0.00 % | 60.00 | 0.00 |
-      | Test assignment two | 0.00 % | 20.00 | 0.00 |
-      | Test assignment three | 50.00 % | 40.00 | 20.00 |
-      | Test assignment four | 33.33 % | 10.00 | 1.67 |
-      | Test assignment five | 33.33 % | 70.00 | 11.67 |
-      | Test assignment six | 33.33 % | 30.00 | 5.00 |
+      | Test assignment one | 0.00 % | 60.00 | 0.00 |
+      | Test assignment two | 0.00 % | 20.00 | 0.00 |
+      | Test assignment three | 50.00 % | 40.00 | 20.00 |
+      | Test assignment four | 33.33 % | 10.00 | 1.67 |
+      | Test assignment five | 33.33 % | 70.00 | 11.67 |
+      | Test assignment six | 33.33 % | 30.00 | 5.00 |
 
   @javascript
   Scenario: Lowest grade aggregation
@@ -164,12 +164,12 @@ Feature: We can understand the gradebook user report
     # Check the values in the weights column.
     Then the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Contribution to course total |
-      | Test assignment one | 0.00 % | 60.00 | 0.00 |
-      | Test assignment two | 100.00 % | 20.00 | 20.00 |
-      | Test assignment three | 0.00 % | 40.00 | 0.00 |
-      | Test assignment four | 33.33 % | 10.00 | 0.00 |
-      | Test assignment five | 33.33 % | 70.00 | 0.00 |
-      | Test assignment six | 33.33 % | 30.00 | 0.00 |
+      | Test assignment one | 0.00 % | 60.00 | 0.00 |
+      | Test assignment two | 100.00 % | 20.00 | 20.00 |
+      | Test assignment three | 0.00 % | 40.00 | 0.00 |
+      | Test assignment four | 33.33 % | 10.00 | 0.00 |
+      | Test assignment five | 33.33 % | 70.00 | 0.00 |
+      | Test assignment six | 33.33 % | 30.00 | 0.00 |
 
   @javascript
   Scenario: Highest grade aggregation
@@ -181,12 +181,12 @@ Feature: We can understand the gradebook user report
     # Check the values in the weights column.
     Then the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Contribution to course total |
-      | Test assignment one | 100.00 % | 60.00 | 60.00 |
-      | Test assignment two | 0.00 % | 20.00 | 0.00 |
-      | Test assignment three | 0.00 % | 40.00 | 0.00 |
-      | Test assignment four | 33.33 % | 10.00 | 0.00 |
-      | Test assignment five | 33.33 % | 70.00 | 0.00 |
-      | Test assignment six | 33.33 % | 30.00 | 0.00 |
+      | Test assignment one | 100.00 % | 60.00 | 60.00 |
+      | Test assignment two | 0.00 % | 20.00 | 0.00 |
+      | Test assignment three | 0.00 % | 40.00 | 0.00 |
+      | Test assignment four | 33.33 % | 10.00 | 0.00 |
+      | Test assignment five | 33.33 % | 70.00 | 0.00 |
+      | Test assignment six | 33.33 % | 30.00 | 0.00 |
 
   @javascript
   Scenario: Mode of grades aggregation
@@ -198,12 +198,12 @@ Feature: We can understand the gradebook user report
     # Check the values in the weights column.
     Then the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Contribution to course total |
-      | Test assignment one | 100.00 % | 60.00 | 60.00 |
-      | Test assignment two | 0.00 % | 20.00 | 0.00 |
-      | Test assignment three | 0.00 % | 40.00 | 0.00 |
-      | Test assignment four | 33.33 % | 10.00 | 0.00 |
-      | Test assignment five | 33.33 % | 70.00 | 0.00 |
-      | Test assignment six | 33.33 % | 30.00 | 0.00 |
+      | Test assignment one | 100.00 % | 60.00 | 60.00 |
+      | Test assignment two | 0.00 % | 20.00 | 0.00 |
+      | Test assignment three | 0.00 % | 40.00 | 0.00 |
+      | Test assignment four | 33.33 % | 10.00 | 0.00 |
+      | Test assignment five | 33.33 % | 70.00 | 0.00 |
+      | Test assignment six | 33.33 % | 30.00 | 0.00 |
 
   @javascript
   Scenario: View user report with mixed aggregation methods
@@ -219,12 +219,12 @@ Feature: We can understand the gradebook user report
     # Check the values in the weights column.
     Then the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Contribution to course total |
-      | Test assignment one | 33.33 % | 60.00 | 60.00 |
-      | Test assignment two | 33.33 % | 20.00 | 20.00 |
-      | Test assignment three | 33.33 %( Extra credit ) | 40.00 | 40.00 |
-      | Test assignment four | 33.33 % | 10.00 | 3.33 |
-      | Test assignment five | 33.33 % | 70.00 | 23.33 |
-      | Test assignment six | 33.33 % | 30.00 | 10.00 |
+      | Test assignment one | 33.33 % | 60.00 | 20.00 % |
+      | Test assignment two | 33.33 % | 20.00 | 6.67 % |
+      | Test assignment three | 33.33 %( Extra credit ) | 40.00 | 13.33 % |
+      | Test assignment four | 33.33 % | 10.00 | 1.11 % |
+      | Test assignment five | 33.33 % | 70.00 | 7.78 % |
+      | Test assignment six | 33.33 % | 30.00 | 3.33 % |
       | Category totalWeighted mean of grades. | 33.33 % | 36.67 | - |
       | Course total | - | 156.67 | - |
 
@@ -238,11 +238,11 @@ Feature: We can understand the gradebook user report
     # Check the values in the weights column.
     Then the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Contribution to course total |
-      | Test assignment one | 20.00 % | 60.00 | 60.00 |
-      | Test assignment two | 20.00 % | 20.00 | 20.00 |
-      | Test assignment three | 20.00 %( Extra credit ) | 40.00 | 40.00 |
-      | Test assignment four | 33.33 % | 10.00 | 10.00 |
-      | Test assignment five | 33.33 % | 70.00 | 70.00 |
-      | Test assignment six | 33.33 % | 30.00 | 30.00 |
+      | Test assignment one | 20.00 % | 60.00 | 12.00 % |
+      | Test assignment two | 20.00 % | 20.00 | 4.00 % |
+      | Test assignment three | 20.00 %( Extra credit ) | 40.00 | 8.00 % |
+      | Test assignment four | 33.33 % | 10.00 | 2.00 % |
+      | Test assignment five | 33.33 % | 70.00 | 14.00 % |
+      | Test assignment six | 33.33 % | 30.00 | 6.00 % |
       | Category total | 60.00 % | 110.00 | - |
       | Course total | - | 230.00 | - |
diff --git a/grade/tests/behat/grade_contribution_with_extra_credit.feature b/grade/tests/behat/grade_contribution_with_extra_credit.feature
new file mode 100644 (file)
index 0000000..ed10866
--- /dev/null
@@ -0,0 +1,81 @@
+@core @core_grades @javascript
+Feature: Extra credit contributions are normalised when going out of bounds
+  In order to use extra credit
+  As a teacher
+  I need to add some extra credit items.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@asd.com | t1 |
+      | student1 | Student | 1 | student1@asd.com | s1 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | grade_aggregations_visible | Simple weighted mean of grades,Mean of grades (with extra credits),Natural |
+    And I am on homepage
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 1 |
+      | Maximum grade | 150 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 2 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 3 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 4 |
+    And I press "Save changes"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
+    And I set the field "Show weighting" to "Show"
+    And I set the field "Show contribution to course total" to "Show"
+    And I press "Save changes"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Manual item 1"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Manual item 2"
+    And I give the grade "70.00" to the user "Student 1" for the grade item "Manual item 3"
+    And I give the grade "90.00" to the user "Student 1" for the grade item "Manual item 4"
+    And I press "Save changes"
+
+  Scenario Outline: The contribution of extra credit items is normalised
+    Given I set the field "Grade report" to "Categories and items"
+    When I set the following settings for grade item "Course 1":
+      | Aggregation | <aggregation> |
+    And I set the following settings for grade item "Manual item 2":
+      | Extra credit | 1 |
+    And I set the following settings for grade item "Manual item 3":
+      | Extra credit | 1 |
+    And I set the following settings for grade item "Manual item 4":
+      | Extra credit | 1 |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item    | Calculated weight | Grade  | Contribution to course total |
+      | Manual item 1 | <m1w>             | 80.00  | <m1c>                        |
+      | Manual item 2 | <m2w>             | 10.00  | <m2c>                        |
+      | Manual item 3 | <m3w>             | 70.00  | <m3c>                        |
+      | Manual item 4 | 0.00 %            | 90.00  | 0.00 %                       |
+
+    Examples:
+      | aggregation                         | m1w      | m1c   | m2w      | m2c   | m3w     | m3c   |
+      | Natural                             | 100.00 % | 53.33 % | 66.67 %  | 6.67 % | 57.14 % | 40.00 % |
+      | Simple weighted mean of grades      | 100.00 % | 53.33 % | 66.67 %  | 6.67 % | 57.14 % | 40.00 % |
+      | Mean of grades (with extra credits) | 100.00 % | 53.33 % | 100.00 % | 10.00 % | 52.38 % | 36.67 % |
index 05d8052..3a1623f 100644 (file)
@@ -105,18 +105,18 @@ Feature: We can use a minimum grade different than zero
     And I set the field "Select all or one user" to "Student 1"
     Then the following should exist in the "user-grade" table:
       | Grade item    | Calculated weight | Grade  | Contribution to course total |
-      | Manual item 1 | 18.18 %           | -25.00 | -25.00                       |
-      | Manual item 2 | 18.18 %           | 50.00  | 50.00                        |
-      | Manual item 3 | 33.33 %           | -80.00 | -80.00                       |
-      | Manual item 4 | 66.67 %           | -10.00 | -10.00                       |
-      | Manual item 5 | 50.00 %           | 50.00  | 50.00                        |
-      | Manual item 6 | 50.00 %           | 75.00  | 75.00                        |
+      | Manual item 1 | 18.18 %           | -25.00 | -4.55 %                      |
+      | Manual item 2 | 18.18 %           | 50.00  | 9.09 %                       |
+      | Manual item 3 | 33.33 %           | -80.00 | -14.55 %                     |
+      | Manual item 4 | 66.67 %           | -10.00 | -1.82 %                      |
+      | Manual item 5 | 50.00 %           | 50.00  | 9.09 %                       |
+      | Manual item 6 | 50.00 %           | 75.00  | 13.64 %                      |
     And I set the field "Select all or one user" to "Student 2"
     And the following should exist in the "user-grade" table:
       | Grade item    | Calculated weight | Grade  | Contribution to course total |
-      | Manual item 1 | 18.18 %           | 0.00   | 0.00                         |
-      | Manual item 2 | 18.18 %           | 50.00  | 50.00                        |
-      | Manual item 3 | 33.33 %           | -10.00 | -10.00                       |
-      | Manual item 4 | 66.67 %           | 50.00  | 50.00                        |
-      | Manual item 5 | 50.00 %           | 50.00  | 50.00                        |
-      | Manual item 6 | 50.00 %           | 50.00  | 50.00                        |
\ No newline at end of file
+      | Manual item 1 | 18.18 %           | 0.00   | 0.00 %                       |
+      | Manual item 2 | 18.18 %           | 50.00  | 9.09 %                       |
+      | Manual item 3 | 33.33 %           | -10.00 | -1.82 %                      |
+      | Manual item 4 | 66.67 %           | 50.00  | 9.09 %                       |
+      | Manual item 5 | 50.00 %           | 50.00  | 9.09 %                       |
+      | Manual item 6 | 50.00 %           | 50.00  | 9.09 %                       |
index a2d50cb..860a7b0 100644 (file)
@@ -99,9 +99,8 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
   @javascript
   Scenario: Grade items weights are normalised when all grade item weights are overridden (sum exactly 100). Extra credit is set to zero.
 
-    When I follow "Edit  assign Test assignment seven"
-    And I set the field "Extra credit" to "1"
-    And I press "Save changes"
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 1 |
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
     And the field "Weight of Test assignment seven" matches value "50.0"
@@ -115,7 +114,7 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     And the field "Weight of Test assignment five" matches value "60.000"
     And the field "Weight of Test assignment six" matches value "40.000"
     And the field "Weight of Test assignment seven" matches value "0.0"
-    And I follow "Reset weights of Sub category 1"
+    And I reset weights for grade category "Sub category 1"
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
     And the field "Weight of Test assignment seven" matches value "50.0"
@@ -123,9 +122,8 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
   @javascript
   Scenario: Grade items weights are normalised when all grade item weights are overridden (sum over 100). Extra credit is set to zero.
 
-    When I follow "Edit  assign Test assignment seven"
-    And I set the field "Extra credit" to "1"
-    And I press "Save changes"
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 1 |
     And I set the field "Override weight of Test assignment five" to "1"
     And I set the field "Override weight of Test assignment six" to "1"
     And I set the field "Weight of Test assignment five" to "60"
@@ -136,7 +134,7 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     And the field "Weight of Test assignment five" matches value "54.545"
     And the field "Weight of Test assignment six" matches value "45.455"
     And the field "Weight of Test assignment seven" matches value "0.0"
-    And I follow "Reset weights of Sub category 1"
+    And I reset weights for grade category "Sub category 1"
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
     And the field "Weight of Test assignment seven" matches value "50.0"
@@ -144,9 +142,8 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
   @javascript
   Scenario: Grade items weights are normalised when all grade item weights are overridden (sum under 100). Extra credit is set to zero.
 
-    When I follow "Edit  assign Test assignment seven"
-    And I set the field "Extra credit" to "1"
-    And I press "Save changes"
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 1 |
     And I set the field "Override weight of Test assignment five" to "1"
     And I set the field "Override weight of Test assignment six" to "1"
     And I set the field "Weight of Test assignment five" to "40"
@@ -157,7 +154,7 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     And the field "Weight of Test assignment five" matches value "57.143"
     And the field "Weight of Test assignment six" matches value "42.857"
     And the field "Weight of Test assignment seven" matches value "0.0"
-    And I follow "Reset weights of Sub category 1"
+    And I reset weights for grade category "Sub category 1"
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
     And the field "Weight of Test assignment seven" matches value "50.0"
@@ -165,9 +162,8 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
   @javascript
   Scenario: Grade items weights are normalised when not all grade item weights are overridden. Extra credit is set respectful to non-overridden items.
 
-    When I follow "Edit  assign Test assignment seven"
-    And I set the field "Extra credit" to "1"
-    And I press "Save changes"
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 1 |
     And I set the field "Override weight of Test assignment five" to "1"
     And I set the field "Weight of Test assignment five" to "40"
     And I press "Save changes"
@@ -176,7 +172,7 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     And the field "Weight of Test assignment five" matches value "40.00"
     And the field "Weight of Test assignment six" matches value "60.000"
     And the field "Weight of Test assignment seven" matches value "90.0"
-    And I follow "Reset weights of Sub category 1"
+    And I reset weights for grade category "Sub category 1"
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
     And the field "Weight of Test assignment seven" matches value "50.0"
@@ -185,9 +181,8 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
   Scenario: The extra credit grade item weight is overridden to a figure over one hundred and then
   the grade item is set to normal.
 
-    When I follow "Edit  assign Test assignment seven"
-    And I set the field "Extra credit" to "1"
-    And I press "Save changes"
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 1 |
     And I set the field "Override weight of Test assignment seven" to "1"
     And I set the field "Weight of Test assignment seven" to "105"
     And I press "Save changes"
@@ -195,9 +190,8 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
     And the field "Weight of Test assignment seven" matches value "105.0"
-    And I follow "Edit  assign Test assignment seven"
-    And I set the field "Extra credit" to "0"
-    And I press "Save changes"
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 0 |
     And I should see "Your weights have been adjusted to total 100."
 
     And the field "Weight of Test assignment five" matches value "0.0"
@@ -208,14 +202,13 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
   Scenario: The extra credit grade item weight is overridden to a figure over one hundred and then
   the grade category is reset.
 
-    When I follow "Edit  assign Test assignment seven"
-    And I set the field "Extra credit" to "1"
-    And I press "Save changes"
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 1 |
     And I set the field "Override weight of Test assignment seven" to "1"
     And I set the field "Weight of Test assignment seven" to "105"
     And I press "Save changes"
 
-    And I follow "Reset weights of Sub category 1"
+    And I reset weights for grade category "Sub category 1"
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
     And the field "Weight of Test assignment seven" matches value "50.0"
@@ -238,15 +231,14 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
   @javascript
   Scenario: With one grade item set as extra credit, when I reset the weights for a category they return to the natural weights.
 
-  When I follow "Edit  assign Test assignment five"
-  And I set the field "Extra credit" to "1"
-  And I press "Save changes"
+  When I set the following settings for grade item "Test assignment five":
+    | Extra credit | 1 |
   And I set the field "Override weight of Test assignment six" to "1"
   And I set the field "Override weight of Test assignment seven" to "1"
   And I set the field "Weight of Test assignment six" to "55"
   And I set the field "Weight of Test assignment seven" to "40"
   And I press "Save changes"
-  And I follow "Reset weights of Sub category 1"
+  And I reset weights for grade category "Sub category 1"
   Then the field "Weight of Test assignment five" matches value "80.0"
   And the field "Weight of Test assignment six" matches value "40.0"
   And the field "Weight of Test assignment seven" matches value "60.0"
@@ -263,5 +255,14 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
   And I set the field "Override weight of Test assignment six" to "1"
   And I set the field "Weight of Test assignment six" to "-25"
   And I press "Save changes"
-  Then the field "Weight of Test assignment six" matches value "0.0"
+  And the field "Weight of Test assignment six" matches value "0.0"
   And the field "Weight of Test assignment seven" matches value "100.0"
+  And I follow "Reset weights of Sub category 1"
+  And I set the field "Override weight of Test assignment five" to "1"
+  And I set the field "Override weight of Test assignment six" to "1"
+  And I set the field "Weight of Test assignment five" to "-10"
+  And I set the field "Weight of Test assignment six" to "120"
+  And I press "Save changes"
+  And the field "Weight of Test assignment five" matches value "0.0"
+  And the field "Weight of Test assignment six" matches value "100.0"
+  And the field "Weight of Test assignment seven" matches value "0.0"
index c884ba8..c04f0f8 100644 (file)
@@ -89,7 +89,7 @@ Feature: View gradebook when scales are used
     And I click on "Select all or one user" "select"
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range | Percentage | Contribution to course total |
-      | Test assignment one | C     | F–A   | 50.00 %    | 3.00                         |
+      | Test assignment one | C     | F–A   | 50.00 %    | 60.00 %                      |
       | Category total      | 3.00  | 0–5   | 60.00 %    | -                            |
       | Course total        | 3.00  | 0–5   | 60.00 %    | -                            |
     And I set the field "jump" to "Categories and items"
@@ -104,7 +104,7 @@ Feature: View gradebook when scales are used
     And I follow "Grades"
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range | Percentage | Contribution to course total |
-      | Test assignment one | B     | F–A   | 75.00 %    | 4.00                         |
+      | Test assignment one | B     | F–A   | 75.00 %    | 80.00 %                      |
       | Category total      | 4.00  | 0–5   | 80.00 %    | -                            |
       | Course total        | 4.00  | 0–5   | 80.00 %    | -                            |
 
@@ -158,11 +158,11 @@ Feature: View gradebook when scales are used
 
     Examples:
       | aggregation                         | coursetotal1 | coursetotal2 | coursetotal3 | coursetotal4 | coursetotal5 |overallavg | courseperc2 | courseperc3 | contrib2 | contrib3 |
-      | Mean of grades                      | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00    | 50.00    |
-      | Weighted mean of grades             | -            | -            | -            | -            | -            | -         | -           | -           | 0.00     | 0.00     |
-      | Simple weighted mean of grades      | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00    | 50.00    |
-      | Mean of grades (with extra credits) | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00    | 50.00    |
-      | Median of grades                    | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00    | 50.00    |
-      | Lowest grade                        | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00    | 50.00    |
-      | Highest grade                       | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00    | 50.00    |
-      | Mode of grades                      | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00    | 50.00    |
+      | Mean of grades                      | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
+      | Weighted mean of grades             | -            | -            | -            | -            | -            | -         | -           | -           | 0.00 %   | 0.00 %   |
+      | Simple weighted mean of grades      | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
+      | Mean of grades (with extra credits) | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
+      | Median of grades                    | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
+      | Lowest grade                        | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
+      | Highest grade                       | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
+      | Mode of grades                      | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
index 6692c49..5a83d48 100644 (file)
@@ -70,13 +70,13 @@ Feature: View gradebook when single item scales are used
     And I set the field "Select all or one user" to "Student 1"
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range     | Contribution to course total |
-      | Test assignment one | Ace!  | Ace!–Ace! | 1.00                         |
+      | Test assignment one | Ace!  | Ace!–Ace! | 100.00 %                     |
       | Category total      | 1.00  | 0–1       | -                            |
       | Course total        | 1.00  | 0–1       | -                            |
     And I set the field "Select all or one user" to "Student 2"
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range     | Contribution to course total |
-      | Test assignment one | -     | Ace!–Ace! | 0.00                         |
+      | Test assignment one | -     | Ace!–Ace! | -                            |
       | Category total      | -     | 0–1       | -                            |
       | Course total        | -     | 0–1       | -                            |
     And I set the field "jump" to "Categories and items"
@@ -124,11 +124,11 @@ Feature: View gradebook when single item scales are used
 
     Examples:
       | aggregation                         | contrib1 | cattotal1 | coursetotal1 | catavg | overallavg |
-      | Mean of grades                      | 100.00   | 100.00    | 100.00       | 100.00 | 100.00     |
-      | Weighted mean of grades             | 0.00     | 100.00    | -            | 100.00 | -          |
-      | Simple weighted mean of grades      | 0.00     | -         | -            | -      | -          |
-      | Mean of grades (with extra credits) | 100.00   | 100.00    | 100.00       | 100.00 | 100.00     |
-      | Median of grades                    | 100.00   | 100.00    | 100.00       | 100.00 | 100.00     |
-      | Lowest grade                        | 100.00   | 100.00    | 100.00       | 100.00 | 100.00     |
-      | Highest grade                       | 100.00   | 100.00    | 100.00       | 100.00 | 100.00     |
-      | Mode of grades                      | 100.00   | 100.00    | 100.00       | 100.00 | 100.00     |
+      | Mean of grades                      | 100.00 % | 100.00    | 100.00       | 100.00 | 100.00     |
+      | Weighted mean of grades             | 0.00 %   | 100.00    | -            | 100.00 | -          |
+      | Simple weighted mean of grades      | 0.00 %   | -         | -            | -      | -          |
+      | Mean of grades (with extra credits) | 100.00 % | 100.00    | 100.00       | 100.00 | 100.00     |
+      | Median of grades                    | 100.00 % | 100.00    | 100.00       | 100.00 | 100.00     |
+      | Lowest grade                        | 100.00 % | 100.00    | 100.00       | 100.00 | 100.00     |
+      | Highest grade                       | 100.00 % | 100.00    | 100.00       | 100.00 | 100.00     |
+      | Mode of grades                      | 100.00 % | 100.00    | 100.00       | 100.00 | 100.00     |
index cf3d498..8282265 100644 (file)
@@ -80,6 +80,7 @@ class core_grade_report_graderlib_testcase extends advanced_testcase {
         // Grade above max. Should be pulled down to max.
         $toobig = 200.00;
         $data->grade[$student->id][$forum1->id] = $toobig;
+        $data->timepageload = time();
         $warnings = $report->process_data($data);
         $this->assertEquals(count($warnings), 1);
 
@@ -89,6 +90,7 @@ class core_grade_report_graderlib_testcase extends advanced_testcase {
         // Grade below min. Should be pulled up to min.
         $toosmall = -10.00;
         $data->grade[$student->id][$forum1->id] = $toosmall;
+        $data->timepageload = time();
         $warnings = $report->process_data($data);
         $this->assertEquals(count($warnings), 1);
 
@@ -99,6 +101,7 @@ class core_grade_report_graderlib_testcase extends advanced_testcase {
         $CFG->unlimitedgrades = 1;
 
         $data->grade[$student->id][$forum1->id] = $toobig;
+        $data->timepageload = time();
         $warnings = $report->process_data($data);
         $this->assertEquals(count($warnings), 0);
 
index ac326e7..0218022 100644 (file)
@@ -136,43 +136,43 @@ echo $OUTPUT->header();
 ?>
 <div id="addmembersform">
     <h3 class="main"><?php print_string('addgroupstogroupings', 'group'); echo ": $groupingname"; ?></h3>
-
     <form id="assignform" method="post" action="">
     <div>
     <input type="hidden" name="sesskey" value="<?php p(sesskey()); ?>" />
-
-    <table summary="" cellpadding="5" cellspacing="0">
+    <table summary="" class="generaltable generalbox groupmanagementtable boxaligncenter">
     <tr>
-      <td valign="top">
+      <td id="existingcell">
           <label for="removeselect"><?php print_string('existingmembers', 'group', $currentmemberscount); ?></label>
-          <br />
+          <div class="userselector" id="removeselect_wrapper">
           <select name="removeselect[]" size="20" id="removeselect" multiple="multiple"
                   onfocus="document.getElementById('assignform').add.disabled=true;
                            document.getElementById('assignform').remove.disabled=false;
                            document.getElementById('assignform').addselect.selectedIndex=-1;">
           <?php echo $currentmembersoptions ?>
-          </select></td>
-      <td valign="top">
-
+          </select></div></td>
+      <td id="buttonscell">
         <p class="arrow_button">
-            <input name="add" id="add" type="submit" value="<?php echo '&nbsp;'.$OUTPUT->larrow().' &nbsp; &nbsp; '.get_string('add'); ?>" title="<?php print_string('add'); ?>" />
-            <br />
-            <input name="remove" id="remove" type="submit" value="<?php echo '&nbsp; '.$OUTPUT->rarrow().' &nbsp; &nbsp; '.get_string('remove'); ?>" title="<?php print_string('remove'); ?>" />
+            <input name="add" id="add" type="submit"
+                   value="<?php echo $OUTPUT->larrow().'&nbsp;'.get_string('add'); ?>"
+                   title="<?php print_string('add'); ?>" /><br>
+            <input name="remove" id="remove" type="submit"
+                   value="<?php echo get_string('remove').'&nbsp;'.$OUTPUT->rarrow(); ?>"
+                   title="<?php print_string('remove'); ?>" />
         </p>
       </td>
-      <td valign="top">
+      <td id="potentialcell">
           <label for="addselect"><?php print_string('potentialmembers', 'group', $potentialmemberscount); ?></label>
-          <br />
+          <div class="userselector" id="addselect_wrapper">
           <select name="addselect[]" size="20" id="addselect" multiple="multiple"
                   onfocus="document.getElementById('assignform').add.disabled=false;
                            document.getElementById('assignform').remove.disabled=true;
                            document.getElementById('assignform').removeselect.selectedIndex=-1;">
          <?php echo $potentialmembersoptions ?>
          </select>
-         <br />
+          </div>
        </td>
     </tr>
-    <tr><td>
+    <tr><td colspan="3" id="backcell">
         <input type="submit" name="cancel" value="<?php print_string('backtogroupings', 'group'); ?>" />
     </td></tr>
     </table>
index 11940b7..7a33b71 100644 (file)
@@ -46,7 +46,8 @@ $string['dmlexceptiononinstall'] = '<p>Det oppstod en databasefeil [{$a->errorco
 $string['downloadedfilecheckfailed'] = 'Sjekk av nedlastet fil mislykkes.';
 $string['invalidmd5'] = 'Ugyldig md5, prøv igjen';
 $string['missingrequiredfield'] = 'Noen påkrevde felt mangler';
-$string['remotedownloaderror'] = 'Mislykkes i å laste ned komponenten til din server, vennligst sjekk proxy-innstillingene. PHP cURL tillegget er sterkt anbefalt. <br /><br />Du må laste ned <a href="{$a->url}">{$a->url}</a> filen manuelt, kopiere den til "{$a->dest}" på serveren din og pakke den ut der.';
+$string['remotedownloaderror'] = '<p>Mislykkes i å laste ned komponenten til din server, vennligst sjekk proxy-innstillingene. PHP cURL tillegget er sterkt anbefalt. </p>
+<p>Du må laste ned <a href="{$a->url}">{$a->url}</a> filen manuelt, kopiere den til "{$a->dest}" på serveren din og pakke den ut der.</p>';
 $string['wrongdestpath'] = 'Gal målmappe';
 $string['wrongsourcebase'] = 'Feil kilde URL base';
 $string['wrongzipfilename'] = 'Galt ZIP-filnavn.';
index 8500c78..1ba17f3 100644 (file)
@@ -35,7 +35,7 @@ $string['availablelangs'] = 'Tilgjengelige språkpakker';
 $string['chooselanguagehead'] = 'Velg et språk';
 $string['chooselanguagesub'] = 'Velg språk (bare for INSTALLASJONEN). Du vil kunne velge språk for nettsted og bruker på et skjermbilde senere.';
 $string['clialreadyconfigured'] = 'Filen config.php finnes allerede. Vennligst bruk admin/cli/install_database.php hvis du vil installere denne portalen.';
-$string['clialreadyinstalled'] = 'Filen config.php eksisterer allerede. Vennligst bruk admin/cli/upgrade.php hvis du vil oppgradere denne portalen.';
+$string['clialreadyinstalled'] = 'Filen config.php eksisterer allerede. Vennligst bruk admin/cli/install_database.php hvis du vil oppgradere Moodle på denne portalen.';
 $string['cliinstallheader'] = 'Moodle {$a} kommandolinje installasjonsprogram';
 $string['databasehost'] = 'Databasevert';
 $string['databasename'] = 'Databasenavn';
@@ -67,7 +67,7 @@ $string['pathsrodataroot'] = 'Dataroot katalog er ikke skrivbar.';
 $string['pathsroparentdataroot'] = 'Overordnet katalog ({$a->parent}) er ikke skrivbar. Datakatalogen ({$a->dataroot}) kan ikke opprettes av installasjonsprogrammet.';
 $string['pathssubadmindir'] = 'Noen ganske få webhoteller bruker  /admin som en egen url for å få tilgang til et kontrollpanel. Dessverre kommer det i konflikt med standard lokalisering av Moodle sine admin-sider. Du kan fikse dette ved å endre navn på admin-mappen og deretter oppgi dette navnet her. F.eks. <em>moodleadmin</em>.  Dette vil fikse adminlenkene i Moodle.';
 $string['pathssubdataroot'] = 'Du trenger et sted hvor Moodle kan lagre opplastede filer. Denne mappen må være med lese og skriverettigheter for webserver-brukeren (veldig ofte \'nobody\' eller \'apache\'), men denne mappen må IKKE være direkte tilgjengelig via web. Installasjonsprogrammet vil forsøke å opprette den om den ikke finnes fra før.';
-$string['pathssubdirroot'] = 'Full mappesti til moodleinstallasjonen.';
+$string['pathssubdirroot'] = '<p>Full mappesti til moodleinstallasjonen.</p>';
 $string['pathssubwwwroot'] = 'Full webadresse til der hvor Moodle skal vises. Det er ikke mulig å bruke Moodle med mer enn en adresse. Dersom portalen din har flere webadresser må du bruke videresending for til den webadressen du oppgir her. Dersom portalen din er tilgjengelig både fra intranett og internett, skal du oppgi den offentlige internettadressen her og sette opp DNS slik at intranettbrukerne også benytter denne offisielle adressen.
 Dersom adressen ikke er korrekt, vennligst endre URL i nettleseren slik at at installasjonen restartes med andre verdier.';
 $string['pathsunsecuredataroot'] = 'Dataroot plassering er ikke sikker';
index 3b76ec9..69146fe 100644 (file)
@@ -44,7 +44,8 @@ $string['dmlexceptiononinstall'] = '<p>Произошла ошибка базы
 $string['downloadedfilecheckfailed'] = 'Ошибка проверки загруженного файла';
 $string['invalidmd5'] = 'Некорректная md5';
 $string['missingrequiredfield'] = 'Отсутствуют некоторые обязательные поля';
-$string['remotedownloaderror'] = 'Не удалось загрузить компонент на сервер, проверьте настройки прокси-сервера, настоятельно рекомендуется установка расширения cURL языка PHP.<br /> <br />Вам следует вручную загрузить файл по ссылке <a href="{$a->url}">{$a->url}</a>, скопировать его в папку «{$a->dest}» на своем сервере и там его распаковать.';
+$string['remotedownloaderror'] = '<p>Не удалось загрузить компонент на сервер. Проверьте настройки прокси-сервера; настоятельно рекомендуется установка расширения  PHP cURL.</p>
+<p>Вам следует вручную загрузить файл по ссылке <a href="{$a->url}">{$a->url}</a>, скопировать его в папку «{$a->dest}» на своем сервере и там его распаковать.</p>';
 $string['wrongdestpath'] = 'Ошибочный путь назначения';
 $string['wrongsourcebase'] = 'Неправильный адрес источника';
 $string['wrongzipfilename'] = 'Неверное имя ZIP-файла';
index b290601..1187a49 100644 (file)
@@ -49,9 +49,6 @@ This setting determines whether empty grades are not included in the aggregation
 $string['aggregateoutcomes'] = 'Include outcomes in aggregation';
 $string['aggregateoutcomes_help'] = 'If enabled, outcomes are included in the aggregation. This may result in an unexpected category total.';
 $string['aggregatesonly'] = 'Aggregates only';
-$string['aggregatesubcats'] = 'Aggregate including subcategories';
-$string['aggregatesubcatsshort'] = 'Include subcategories';
-$string['aggregatesubcats_help'] = 'This setting determines whether grades in subcategories are included in the aggregation.';
 $string['aggregatesum'] = 'Natural';
 $string['aggregateweightedmean'] = 'Weighted mean of grades';
 $string['aggregateweightedmean2'] = 'Simple weighted mean of grades';
@@ -193,7 +190,6 @@ $string['errorsavegrade'] = 'Could not save grade, sorry.';
 $string['errorsettinggrade'] = 'Error saving "{$a->itemname}" grade for userid {$a->userid}';
 $string['errorupdatinggradecategoryaggregateonlygraded'] = 'Error updating the "Aggregate only non-empty grades" setting of grade category ID {$a->id}';
 $string['errorupdatinggradecategoryaggregateoutcomes'] = 'Error updating the "Include outcomes in aggregation" setting of grade category ID {$a->id}';
-$string['errorupdatinggradecategoryaggregatesubcats'] = 'Error updating the "Aggregate including subcategories" setting of grade category ID {$a->id}';
 $string['errorupdatinggradecategoryaggregation'] = 'Error updating the aggregation type of grade category ID {$a->id}';
 $string['errorupdatinggradeitemaggregationcoef'] = 'Error updating the aggregation coefficient (weight or extra credit) of grade item ID {$a->id}';
 $string['eventgradedeleted'] = 'Grade deleted';
@@ -580,6 +576,7 @@ $string['reportdefault'] = 'Report default ({$a})';
 $string['reportplugins'] = 'Report plugins';
 $string['reportsettings'] = 'Report settings';
 $string['reprintheaders'] = 'Reprint headers';
+$string['resetweightsshort'] = 'Reset weights';
 $string['resetweights'] = 'Reset weights of {$a->itemname}';
 $string['respectingcurrentdata'] = 'leaving current configuration unmodified';
 $string['rowpreviewnum'] = 'Preview rows';
@@ -680,7 +677,8 @@ $string['subcategory'] = 'Normal category';
 $string['submissions'] = 'Submissions';
 $string['submittedon'] = 'Submitted: {$a}';
 $string['sumofgradesupgradedgrades'] = 'Note: The aggregation method "Sum of grades" has been changed to "Natural" as part of a site upgrade. Since "Sum of grades" was previously used in this course, it is recommended that you review this change in the gradebook.';
-$string['sumofgradesupgradedgradeshidemessage'] = 'OK';
+$string['aggregatesubcatsupgradedgrades'] = 'Note: The aggregation setting "Aggregate including subcategories" has been removed as part of a site upgrade. Since "Aggregate including subcategories" was previously used in this course, it is recommended that you review this change in the gradebook.';
+$string['upgradedgradeshidemessage'] = 'OK';
 $string['switchtofullview'] = 'Switch to full view';
 $string['switchtosimpleview'] = 'Switch to simple view';
 $string['tabs'] = 'Tabs';
index 79cc8d2..1282b05 100644 (file)
@@ -55,3 +55,4 @@ $string['ratingtime'] = 'Restrict ratings to items with dates in this range:';
 $string['ratings'] = 'Ratings';
 $string['rolewarning'] = 'Roles with permission to rate';
 $string['rolewarning_help'] = 'To submit ratings users require the moodle/rating:rate capability and any module specific capabilities. Users assigned the following roles should be able to rate items. The list of roles may be amended via the permissions link in the administration block.';
+$string['scaleselectionrequired'] = 'When selecting a ratings aggregate type you must also select to use either a scale or set a maximum points.';
\ No newline at end of file
index 66ad92b..be24d9e 100644 (file)
@@ -2441,13 +2441,22 @@ class admin_setting_configfile extends admin_setting_configtext {
         '<div class="form-file defaultsnext"><input type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" />'.$executable.'</div>',
         $this->description, true, '', $default, $query);
     }
+
     /**
-     * checks if execpatch has been disabled in config.php
+     * Checks if execpatch has been disabled in config.php
      */
     public function write_setting($data) {
         global $CFG;
         if (!empty($CFG->preventexecpath)) {
-            return '';
+            if ($this->get_setting() === null) {
+                // Use default during installation.
+                $data = $this->get_defaultsetting();
+                if ($data === null) {
+                    $data = '';
+                }
+            } else {
+                return '';
+            }
         }
         return parent::write_setting($data);
     }
index fc0b3be..0a810c2 100644 (file)
@@ -106,7 +106,7 @@ class calendar_event_updated extends base {
         if (empty($this->other['name'])) {
             throw new \coding_exception('The \'name\' value must be set in other.');
         }
-        if (empty($this->other['timestart'])) {
+        if (!isset($this->other['timestart'])) {
             throw new \coding_exception('The \'timestart\' value must be set in other.');
         }
     }
index 5781acc..11ba4d3 100644 (file)
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die;
 
 require_once("$CFG->libdir/externallib.php");
 require_once("$CFG->libdir/gradelib.php");
+require_once("$CFG->dirroot/grade/querylib.php");
 
 /**
  * core grades functions
@@ -69,6 +70,11 @@ class core_grades_external extends external_api {
         $params = self::validate_parameters(self::get_grades_parameters(),
             array('courseid' => $courseid, 'component' => $component, 'activityid' => $activityid, 'userids' => $userids));
 
+        $gradesarray = array(
+            'items'     => array(),
+            'outcomes'  => array()
+        );
+
         $coursecontext = context_course::instance($params['courseid']);
 
         try {
@@ -106,155 +112,164 @@ class core_grades_external extends external_api {
 
         $itemtype = null;
         $itemmodule = null;
+        $iteminstance = null;
+
         if (!empty($params['component'])) {
             list($itemtype, $itemmodule) = normalize_component($params['component']);
         }
 
         $cm = null;
-        if (!empty($itemmodule) && !empty($activityid)) {
-            if (! $cm = get_coursemodule_from_id($itemmodule, $activityid)) {
+        if (!empty($itemmodule) && !empty($params['activityid'])) {
+            if (!$cm = get_coursemodule_from_id($itemmodule, $params['activityid'])) {
                 throw new moodle_exception('invalidcoursemodule');
             }
+            $iteminstance = $cm->instance;
         }
 
-        $cminstanceid = null;
-        if (!empty($cm)) {
-            $cminstanceid = $cm->instance;
-        }
-        $grades = grade_get_grades($params['courseid'], $itemtype, $itemmodule, $cminstanceid, $params['userids']);
+        // Load all the module info.
+        $modinfo = get_fast_modinfo($params['courseid']);
+        $activityinstances = $modinfo->get_instances();
 
-        $acitivityinstances = null;
-        if (empty($cm)) {
-            // If we're dealing with multiple activites load all the module info.
-            $modinfo = get_fast_modinfo($params['courseid']);
-            $acitivityinstances = $modinfo->get_instances();
+        $gradeparams = array('courseid' => $params['courseid']);
+        if (!empty($itemtype)) {
+            $gradeparams['itemtype'] = $itemtype;
+        }
+        if (!empty($itemmodule)) {
+            $gradeparams['itemmodule'] = $itemmodule;
+        }
+        if (!empty($iteminstance)) {
+            $gradeparams['iteminstance'] = $iteminstance;
         }
 
-        foreach ($grades->items as $gradeitem) {
-            if (!empty($cm)) {
-                // If they only requested one activity we will already have the cm.
-                $modulecm = $cm;
-            } else if (!empty($gradeitem->itemmodule)) {
-                $modulecm = $acitivityinstances[$gradeitem->itemmodule][$gradeitem->iteminstance];
-            } else {
-                // Course grade item.
-                continue;
-            }
+        if ($activitygrades = grade_item::fetch_all($gradeparams)) {
+            $canviewhidden = has_capability('moodle/grade:viewhidden', context_course::instance($params['courseid']));
 
-            // Make student feedback ready for output.
-            foreach ($gradeitem->grades as $studentgrade) {
-                if (!empty($studentgrade->feedback)) {
-                    list($studentgrade->feedback, $categoryinfo->feedbackformat) =
-                        external_format_text($studentgrade->feedback, $studentgrade->feedbackformat,
-                        $modulecm->id, $params['component'], 'feedback', null);
-                }
-            }
-        }
+            foreach ($activitygrades as $activitygrade) {
 
-        // Convert from objects to arrays so all web service clients are supported.
-        // While we're doing that we also remove grades the current user can't see due to hiding.
-        $gradesarray = array();
-        $canviewhidden = has_capability('moodle/grade:viewhidden', context_course::instance($params['courseid']));
-
-        $gradesarray['items'] = array();
-        foreach ($grades->items as $gradeitem) {
-            // Switch the stdClass instance for a grade item instance so we can call is_hidden() and use the ID.
-            $gradeiteminstance = self::get_grade_item(
-                $course->id, $gradeitem->itemtype, $gradeitem->itemmodule, $gradeitem->iteminstance, 0);
-            if (!$canviewhidden && $gradeiteminstance->is_hidden()) {
-                continue;
-            }
+                if ($activitygrade->itemtype != 'course' and $activitygrade->itemtype != 'mod') {
+                    // This function currently only supports course and mod grade items. Manual and category not supported.
+                    continue;
+                }
 
-            // Format mixed bool/integer parameters.
-            $gradeitem->hidden = (empty($gradeitem->hidden)) ? 0 : $gradeitem->hidden;
-            $gradeitem->locked = (empty($gradeitem->locked)) ? 0 : $gradeitem->locked;
-
-            $gradeitemarray = (array)$gradeitem;
-            $gradeitemarray['grades'] = array();
-
-            if (!empty($gradeitem->grades)) {
-                foreach ($gradeitem->grades as $studentid => $studentgrade) {
-                    if (!$canviewhidden) {
-                        // Need to load the grade_grade object to check visibility.
-                        $gradegradeinstance = grade_grade::fetch(
-                            array(
-                                'userid' => $studentid,
-                                'itemid' => $gradeiteminstance->id
-                            )
-                        );
-                        // The grade grade may be legitimately missing if the student has no grade.
-                        if (!empty($gradegradeinstance) && $gradegradeinstance->is_hidden()) {
-                            continue;
-                        }
-                    }
+                if ($activitygrade->itemtype == 'course') {
+                    $item = grade_get_course_grades($course->id, $params['userids']);
+                    $item->itemnumber = 0;
 
-                    // Format mixed bool/integer parameters.
-                    $studentgrade->hidden = (empty($studentgrade->hidden)) ? 0 : $studentgrade->hidden;
-                    $studentgrade->locked = (empty($studentgrade->locked)) ? 0 : $studentgrade->locked;
-                    $studentgrade->overridden = (empty($studentgrade->overridden)) ? 0 : $studentgrade->overridden;
+                    $grades = new stdClass;
+                    $grades->items = array($item);
+                    $grades->outcomes = array();
 
-                    $gradeitemarray['grades'][$studentid] = (array)$studentgrade;
-                    // Add the student ID as some WS clients can't access the array key.
-                    $gradeitemarray['grades'][$studentid]['userid'] = $studentid;
+                } else {
+                    $cm = $activityinstances[$activitygrade->itemmodule][$activitygrade->iteminstance];
+                    $instance = $cm->instance;
+                    $grades = grade_get_grades($params['courseid'], $activitygrade->itemtype,
+                                                $activitygrade->itemmodule, $instance, $params['userids']);
                 }
-            }
 
-            // If they requested grades for multiple activities load the cm object now.
-            $modulecm = $cm;
-            if (empty($modulecm) && !empty($gradeiteminstance->itemmodule)) {
-                $modulecm = $acitivityinstances[$gradeiteminstance->itemmodule][$gradeiteminstance->iteminstance];
-            }
-            if ($gradeiteminstance->itemtype == 'course') {
-                $gradesarray['items']['course'] = $gradeitemarray;
-                $gradesarray['items']['course']['activityid'] = 'course';
-            } else {
-                $gradesarray['items'][$modulecm->id] = $gradeitemarray;
-                // Add the activity ID as some WS clients can't access the array key.
-                $gradesarray['items'][$modulecm->id]['activityid'] = $modulecm->id;
-            }
-        }
+                // Convert from objects to arrays so all web service clients are supported.
+                // While we're doing that we also remove grades the current user can't see due to hiding.
+                foreach ($grades->items as $gradeitem) {
+                    // Switch the stdClass instance for a grade item instance so we can call is_hidden() and use the ID.
+                    $gradeiteminstance = self::get_grade_item(
+                        $course->id, $activitygrade->itemtype, $activitygrade->itemmodule, $activitygrade->iteminstance, 0);
+                    if (!$canviewhidden && $gradeiteminstance->is_hidden()) {
+                        continue;
+                    }
 
-        $gradesarray['outcomes'] = array();
-        foreach ($grades->outcomes as $outcome) {
-            $modulecm = $cm;
-            if (empty($modulecm)) {
-                $modulecm = $acitivityinstances[$outcome->itemmodule][$outcome->iteminstance];
-            }
+                    // Format mixed bool/integer parameters.
+                    $gradeitem->hidden = (empty($gradeitem->hidden)) ? 0 : $gradeitem->hidden;
+                    $gradeitem->locked = (empty($gradeitem->locked)) ? 0 : $gradeitem->locked;
+
+                    $gradeitemarray = (array)$gradeitem;
+                    $gradeitemarray['grades'] = array();
 
-            // Format mixed bool/integer parameters.
-            $outcome->hidden = (empty($outcome->hidden)) ? 0 : $outcome->hidden;
-            $outcome->locked = (empty($outcome->locked)) ? 0 : $outcome->locked;
-
-            $gradesarray['outcomes'][$modulecm->id] = (array)$outcome;
-            $gradesarray['outcomes'][$modulecm->id]['activityid'] = $modulecm->id;
-
-            $gradesarray['outcomes'][$modulecm->id]['grades'] = array();
-            if (!empty($outcome->grades)) {
-                foreach ($outcome->grades as $studentid => $studentgrade) {
-                    if (!$canviewhidden) {
-                        // Need to load the grade_grade object to check visibility.
-                        $gradeiteminstance = self::get_grade_item(
-                            $course->id, $outcome->itemtype, $outcome->itemmodule, $outcome->iteminstance, $outcome->itemnumber);
-                        $gradegradeinstance = grade_grade::fetch(
-                            array(
-                                'userid' => $studentid,
-                                'itemid' => $gradeiteminstance->id
-                            )
-                        );
-                        // The grade grade may be legitimately missing if the student has no grade.
-                        if (!empty($gradegradeinstance ) && $gradegradeinstance->is_hidden()) {
-                            continue;
+                    if (!empty($gradeitem->grades)) {
+                        foreach ($gradeitem->grades as $studentid => $studentgrade) {
+                            if (!$canviewhidden) {
+                                // Need to load the grade_grade object to check visibility.
+                                $gradegradeinstance = grade_grade::fetch(
+                                    array(
+                                        'userid' => $studentid,
+                                        'itemid' => $gradeiteminstance->id
+                                    )
+                                );
+                                // The grade grade may be legitimately missing if the student has no grade.
+                                if (!empty($gradegradeinstance) && $gradegradeinstance->is_hidden()) {
+                                    continue;
+                                }
+                            }
+
+                            // Format mixed bool/integer parameters.
+                            $studentgrade->hidden = (empty($studentgrade->hidden)) ? 0 : $studentgrade->hidden;
+                            $studentgrade->locked = (empty($studentgrade->locked)) ? 0 : $studentgrade->locked;
+                            $studentgrade->overridden = (empty($studentgrade->overridden)) ? 0 : $studentgrade->overridden;
+
+                            if ($gradeiteminstance->itemtype != 'course' and !empty($studentgrade->feedback)) {
+                                list($studentgrade->feedback, $studentgrade->feedbackformat) =
+                                    external_format_text($studentgrade->feedback, $studentgrade->feedbackformat,
+                                    $cm->id, $params['component'], 'feedback', null);
+                            }
+
+                            $gradeitemarray['grades'][$studentid] = (array)$studentgrade;
+                            // Add the student ID as some WS clients can't access the array key.
+                            $gradeitemarray['grades'][$studentid]['userid'] = $studentid;
                         }
                     }
 
-                    // Format mixed bool/integer parameters.
-                    $studentgrade->hidden = (empty($studentgrade->hidden)) ? 0 : $studentgrade->hidden;
-                    $studentgrade->locked = (empty($studentgrade->locked)) ? 0 : $studentgrade->locked;
-
-                    $gradesarray['outcomes'][$modulecm->id]['grades'][$studentid] = (array)$studentgrade;
+                    if ($gradeiteminstance->itemtype == 'course') {
+                        $gradesarray['items']['course'] = $gradeitemarray;
+                        $gradesarray['items']['course']['activityid'] = 'course';
+                    } else {
+                        $gradesarray['items'][$cm->id] = $gradeitemarray;
+                        // Add the activity ID as some WS clients can't access the array key.
+                        $gradesarray['items'][$cm->id]['activityid'] = $cm->id;
+                    }
+                }
 
-                    // Add the student ID into the grade structure as some WS clients can't access the key.
-                    $gradesarray['outcomes'][$modulecm->id]['grades'][$studentid]['userid'] = $studentid;
+                foreach ($grades->outcomes as $outcome) {
+                    // Format mixed bool/integer parameters.
+                    $outcome->hidden = (empty($outcome->hidden)) ? 0 : $outcome->hidden;
+                    $outcome->locked = (empty($outcome->locked)) ? 0 : $outcome->locked;
+
+                    $gradesarray['outcomes'][$cm->id] = (array)$outcome;
+                    $gradesarray['outcomes'][$cm->id]['activityid'] = $cm->id;
+
+                    $gradesarray['outcomes'][$cm->id]['grades'] = array();
+                    if (!empty($outcome->grades)) {
+                        foreach ($outcome->grades as $studentid => $studentgrade) {
+                            if (!$canviewhidden) {
+                                // Need to load the grade_grade object to check visibility.
+                                $gradeiteminstance = self::get_grade_item($course->id, $activitygrade->itemtype,
+                                                                           $activitygrade->itemmodule, $activitygrade->iteminstance,
+                                                                           $activitygrade->itemnumber);
+                                $gradegradeinstance = grade_grade::fetch(
+                                    array(
+                                        'userid' => $studentid,
+                                        'itemid' => $gradeiteminstance->id
+                                    )
+                                );
+                                // The grade grade may be legitimately missing if the student has no grade.
+                                if (!empty($gradegradeinstance ) && $gradegradeinstance->is_hidden()) {
+                                    continue;
+                                }
+                            }
+
+                            // Format mixed bool/integer parameters.
+                            $studentgrade->hidden = (empty($studentgrade->hidden)) ? 0 : $studentgrade->hidden;
+                            $studentgrade->locked = (empty($studentgrade->locked)) ? 0 : $studentgrade->locked;
+
+                            if (!empty($studentgrade->feedback)) {
+                                list($studentgrade->feedback, $studentgrade->feedbackformat) =
+                                    external_format_text($studentgrade->feedback, $studentgrade->feedbackformat,
+                                    $cm->id, $params['component'], 'feedback', null);
+                            }
+
+                            $gradesarray['outcomes'][$cm->id]['grades'][$studentid] = (array)$studentgrade;
+
+                            // Add the student ID into the grade structure as some WS clients can't access the key.
+                            $gradesarray['outcomes'][$cm->id]['grades'][$studentid]['userid'] = $studentid;
+                        }
+                    }
                 }
             }
         }
index d112172..17313e0 100644 (file)
@@ -93,12 +93,18 @@ class manager {
         $tasks = self::load_default_scheduled_tasks_for_component($componentname);
 
         $tasklocks = array();
-        foreach ($tasks as $task) {
+        foreach ($tasks as $taskid => $task) {
             $classname = get_class($task);
             if (strpos($classname, '\\') !== 0) {
                 $classname = '\\' . $classname;
             }
 
+            // If there is an existing task with a custom schedule, do not override it.
+            $currenttask = self::get_scheduled_task($classname);
+            if ($currenttask && $currenttask->is_customised()) {
+                $tasks[$taskid] = $currenttask;
+            }
+
             if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
                 // Could not get all the locks required - release all locks and fail.
                 foreach ($tasklocks as $tasklock) {
@@ -330,7 +336,7 @@ class manager {
 
         $tasks = array();
         // We are just reading - so no locks required.
-        $records = $DB->get_records('task_scheduled', array('componentname' => $componentname), 'classname', '*', IGNORE_MISSING);
+        $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
         foreach ($records as $record) {
             $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).
index 4305be4..6a6c9e4 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20141007" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20141017" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="droplow" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Drop the X lowest items"/>
         <FIELD NAME="aggregateonlygraded" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="aggregate only graded activities"/>
         <FIELD NAME="aggregateoutcomes" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Aggregate outcomes"/>
-        <FIELD NAME="aggregatesubcats" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="ignore subcategories in aggregation"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="hidden" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="droplow" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Drop the X lowest items"/>
         <FIELD NAME="aggregateonlygraded" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="aggregate only graded items"/>
         <FIELD NAME="aggregateoutcomes" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Aggregate outcomes"/>
-        <FIELD NAME="aggregatesubcats" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="ignore subcategories in aggregation"/>
+        <FIELD NAME="aggregatesubcats" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="This setting was removed from grade_categories. It is kept here only to preserve history."/>
         <FIELD NAME="hidden" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
index e01a493..f71f55f 100644 (file)
@@ -968,7 +968,13 @@ $services = array(
             'mod_forum_get_forum_discussions_paginated',
             'mod_forum_get_forum_discussion_posts',
             'core_files_get_files',
-            'core_message_get_messages'),
+            'core_message_get_messages',
+            'core_message_create_contacts',
+            'core_message_delete_contacts',
+            'core_message_block_contacts',
+            'core_message_unblock_contacts',
+            'core_message_get_contacts',
+            'core_message_search_contacts'),
         'enabled' => 0,
         'restrictedusers' => 0,
         'shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE,
index ec35ffa..62bcd6e 100644 (file)
@@ -4018,5 +4018,34 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2014101001.00);
     }
 
+    if ($oldversion < 2014102000.00) {
+
+        // Define field aggregatesubcats to be dropped from grade_categories.
+        $table = new xmldb_table('grade_categories');
+        $field = new xmldb_field('aggregatesubcats');
+
+        // Conditionally launch drop field aggregatesubcats.
+        if ($dbman->field_exists($table, $field)) {
+
+            $sql = 'SELECT DISTINCT courseid
+                      FROM {grade_categories}
+                     WHERE aggregatesubcats = ?';
+            $courses = $DB->get_records_sql($sql, array(1));
+
+            foreach ($courses as $course) {
+                set_config('show_aggregatesubcats_upgrade_' . $course->courseid, 1);
+                // Set each of the grade items to needing an update so that when the user visits the grade reports the
+                // figures will be updated.
+                $DB->set_field('grade_items', 'needsupdate', 1, array('courseid' => $course->courseid));
+            }
+
+
+            $dbman->drop_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2014102000.00);
+    }
+
     return true;
 }
index 9378dfe..3090b0d 100644 (file)
@@ -18,6 +18,7 @@ Feature: Atto accessibility checker
     And I press "Save image"
     And I press "Accessibility checker"
     And I should see "Congratulations, no accessibility problems found!"
+    And I click on ".moodle-dialogue-focused .closebutton" "css_element"
     And I select the text in the "Description" Atto editor
     And I click on "Image" "button"
     And I set the field "Describe this image" to ""
index 9b9e076..536633c 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 3a7076c..b8a6663 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 0b12292..2b6b2e9 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 44195eb..d129134 100644 (file)
@@ -89,7 +89,7 @@ EditorAutosave.prototype = {
      * @property lastText
      * @type string
      */
-    lastText: null,
+    lastText: "",
 
     /**
      * Autosave instance.
@@ -144,10 +144,23 @@ EditorAutosave.prototype = {
                 success: function(id,o) {
                     if (typeof o.responseText !== "undefined" && o.responseText !== "") {
                         response_json = JSON.parse(o.responseText);
+
+                        // Revert untouched editor contents to an empty string.
+                        // Check for FF and Chrome.
+                        if (response_json.result === '<p></p>' || response_json.result === '<p><br></p>' ||
+                            response_json.result === '<br>') {
+                            response_json.result = '';
+                        }
+
+                        // Check for IE 9 and 10.
+                        if (response_json.result === '<p>&nbsp;</p>' || response_json.result === '<p><br>&nbsp;</p>') {
+                            response_json.result = '';
+                        }
+
                         if (response_json.error || typeof response_json.result === 'undefined') {
                             Y.log('Error occurred recovering draft text: ' + response_json.error, 'debug', LOGNAME_AUTOSAVE);
                             this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'), NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
-                        } else if (response_json !== this.textarea.get('value')) {
+                        } else if (response_json.result !== this.textarea.get('value')) {
                             Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
                             this.recoverText(response_json.result);
                         }
@@ -259,7 +272,7 @@ EditorAutosave.prototype = {
                     error: ajaxErrorFunction,
                     failure: ajaxErrorFunction,
                     success: function(code, response) {
-                        if (response.response !== "") {
+                        if (response.responseText !== "") {
                             Y.soon(Y.bind(ajaxErrorFunction, this, [code, response]));
                         } else {
                             // All working.
index cc79446..7fcd08d 100644 (file)
@@ -2764,21 +2764,17 @@ function file_modify_html_header($text) {
     global $CFG;
 
     $stylesheetshtml = '';
-/*    foreach ($CFG->stylesheets as $stylesheet) {
+/*
+    foreach ($CFG->stylesheets as $stylesheet) {
         //TODO: MDL-21120
         $stylesheetshtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
-    }*/
-
-    $ufo = '';
-    if (filter_is_enabled('mediaplugin')) {
-        // this script is needed by most media filter plugins.
-        $attributes = array('type'=>'text/javascript', 'src'=>$CFG->httpswwwroot . '/lib/ufo.js');
-        $ufo = html_writer::tag('script', '', $attributes) . "\n";
     }
+*/
+    // TODO The code below is actually a waste of CPU. When MDL-29738 will be implemented it should be re-evaluated too.
 
     preg_match('/\<head\>|\<HEAD\>/', $text, $matches);
     if ($matches) {
-        $replacement = '<head>'.$ufo.$stylesheetshtml;
+        $replacement = '<head>'.$stylesheetshtml;
         $text = preg_replace('/\<head\>|\<HEAD\>/', $replacement, $text, 1);
         return $text;
     }
@@ -2787,7 +2783,7 @@ function file_modify_html_header($text) {
     preg_match('/\<html\>|\<HTML\>/', $text, $matches);
     if ($matches) {
         // replace <html> tag with <html><head>includes</head>
-        $replacement = '<html>'."\n".'<head>'.$ufo.$stylesheetshtml.'</head>';
+        $replacement = '<html>'."\n".'<head>'.$stylesheetshtml.'</head>';
         $text = preg_replace('/\<html\>|\<HTML\>/', $replacement, $text, 1);
         return $text;
     }
@@ -2795,13 +2791,13 @@ function file_modify_html_header($text) {
     // if not, look for <body> tag, and stick <head> before body
     preg_match('/\<body\>|\<BODY\>/', $text, $matches);
     if ($matches) {
-        $replacement = '<head>'.$ufo.$stylesheetshtml.'</head>'."\n".'<body>';
+        $replacement = '<head>'.$stylesheetshtml.'</head>'."\n".'<body>';
         $text = preg_replace('