Merge branch 'MDL-47637-master' of git://github.com/FMCorz/moodle
authorDan Poltawski <dan@moodle.com>
Thu, 23 Oct 2014 15:16:07 +0000 (16:16 +0100)
committerDan Poltawski <dan@moodle.com>
Thu, 23 Oct 2014 15:16:07 +0000 (16:16 +0100)
65 files changed:
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/rule_manager.php
admin/tool/monitor/classes/subscription_manager.php
admin/tool/monitor/index.php
admin/tool/monitor/lang/en/tool_monitor.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
admin/tool/uploadcourse/tests/course_test.php
admin/tool/uploadcourse/tests/processor_test.php
backup/moodle2/tests/moodle2_test.php
course/tests/courselib_test.php
course/tests/externallib_test.php
grade/edit/letter/edit_form.php
grade/edit/letter/index.php
grade/lib.php
grade/report/lib.php
grade/report/user/lib.php
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_calculated_weights.feature
grade/tests/behat/grade_contribution_with_extra_credit.feature
grade/tests/behat/grade_mingrade.feature
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_single_item_scales.feature
grade/tests/report_graderlib_test.php
install/lang/no/error.php
install/lang/no/install.php
install/lang/ru/error.php
lib/classes/task/manager.php
lib/db/install.xml [changed mode: 0755->0644]
lib/grade/grade_category.php
lib/grade/grade_grade.php
lib/tests/event_user_graded_test.php
lib/tests/questionlib_test.php
lib/tests/scheduled_task_test.php
mod/assign/feedback/editpdf/tests/editpdf_test.php
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/tabs.php
mod/lesson/tests/behat/teacher_grade_essays.feature [new file with mode: 0644]
mod/lesson/version.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]
report/participation/index.php
theme/bootstrapbase/layout/popup.php
user/index.php
version.php

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..edf8b49 100644 (file)
@@ -91,4 +91,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 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;
     }
 
     /**
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 0d39eeb..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}"?';
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..72d04e8 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,7 +125,7 @@ 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);
+        $ruledata = \tool_monitor\rule_manager::get_rules_by_courseid($course1->id);
         $this->assertEquals($ruleids, array_keys($ruledata));
         $this->assertCount(20, $ruledata);
     }
index 94c0c4d..6737c1b 100644 (file)
@@ -114,13 +114,11 @@ class tool_monitor_task_clean_events_testcase extends advanced_testcase {
             \mod_scorm\event\course_module_instance_list_viewed::create($eventparams)->trigger();
         }
 
-        // Check that there are a bunch of events now. There will be additional events for creating courses and modules.
-        $this->assertEquals(20, $DB->count_records('tool_monitor_events'));
+        // 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 mod_quiz/mod_scorm events are removed as well as the course_module_*
-        // viewed events in the second course. The chapter_viewed event in the second course should remain though as
-        // there is a rule associated with that event in that course. The chapter_viewed event in the first course
-        // should also remain as there is a site wide rule.
+        // 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();
 
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 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 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 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 1b8c824..aa33568 100644 (file)
@@ -897,7 +897,7 @@ function print_grade_page_head($courseid, $active_type, $active_plugin=null,
 
     $returnval .= print_natural_aggregation_upgrade_notice($courseid,
                                                            context_course::instance($courseid),
-                                                           '/grade/report/' . $active_plugin . '/index.php',
+                                                           $PAGE->url,
                                                            $return);
 
     if ($return) {
index af0208b..b0aa35b 100644 (file)
@@ -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 b4cc0ab..e02c99f 100644 (file)
@@ -446,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
                 /**
@@ -473,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;
@@ -649,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
@@ -735,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
@@ -751,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.
@@ -760,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 40e7063..4e75b01 100644 (file)
@@ -394,16 +394,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 +413,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 | 0.00 |
+      | 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
@@ -587,15 +587,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 +606,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 | - |
index 88e45d9..ed10866 100644 (file)
@@ -72,10 +72,10 @@ Feature: Extra credit contributions are normalised when going out of bounds
       | 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                         |
+      | Manual item 4 | 0.00 %            | 90.00  | 0.00 %                       |
 
     Examples:
       | aggregation                         | m1w      | m1c   | m2w      | m2c   | m3w     | m3c   |
-      | Natural                             | 100.00 % | 80.00 | 66.67 %  | 10.00 | 57.14 % | 60.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 |
+      | 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 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 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 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).
old mode 100755 (executable)
new mode 100644 (file)
index 9ce8987..1b0e734 100644 (file)
@@ -779,6 +779,24 @@ class grade_category extends grade_object {
     private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) {
         global $DB;
 
+        // First set them all to weight null and status = 'unknown'.
+        if ($allitems = grade_item::fetch_all(array('categoryid'=>$this->id))) {
+            list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($allitems), SQL_PARAMS_NAMED, 'g');
+
+            $itemlist['userid'] = $userid;
+
+            $DB->set_field_select('grade_grades',
+                                  'aggregationstatus',
+                                  'unknown',
+                                  "itemid $itemsql AND userid = :userid",
+                                  $itemlist);
+            $DB->set_field_select('grade_grades',
+                                  'aggregationweight',
+                                  0,
+                                  "itemid $itemsql AND userid = :userid",
+                                  $itemlist);
+        }
+
         // Included.
         if (!empty($usedweights)) {
             // The usedweights items are updated individually to record the weights.
index 46eea2d..63b9914 100644 (file)
@@ -703,6 +703,8 @@ class grade_grade extends grade_object {
             } else if ($grade_grade->is_hidden()) {
                 $hiddenfound = true;
                 $altered[$grade_grade->itemid] = null;
+                $alteredaggregationstatus[$grade_grade->itemid] = 'dropped';
+                $alteredaggregationweight[$grade_grade->itemid] = 0;
             } else if ($grade_grade->is_locked() or $grade_grade->is_overridden()) {
                 // no need to recalculate locked or overridden grades
             } else {
index 84159cf..b0eb084 100644 (file)
@@ -108,111 +108,131 @@ class core_event_user_graded_testcase extends advanced_testcase {
         $user = $this->getDataGenerator()->create_user();
         $this->getDataGenerator()->enrol_user($user->id, $course->id);
         $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
+        $quizitemparams = array('itemtype' => 'mod', 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id,
+            'courseid' => $course->id);
+        $gradeitem = grade_item::fetch($quizitemparams);
+        $courseitem = grade_item::fetch_course_item($course->id);
 
         // Now mark the quiz using grade_update as this is the function that modules use.
         $grade = array();
         $grade['userid'] = $user->id;
-        $grade['rawgrade'] = 50;
+        $grade['rawgrade'] = 60;
 
         $sink = $this->redirectEvents();
         grade_update('mod/quiz', $course->id, 'mod', 'quiz', $quiz->id, 0, $grade);
         $events = $sink->get_events();
-        $event = reset($events);
         $sink->close();
 
-        // Ensure we have a user_graded event.
-        $this->assertEquals(1, count($events));
-        $this->assertInstanceOf('\core\event\user_graded', $event);
+        // Ensure we have two user_graded events, one for the item, one for the course.
+        $this->assertEquals(2, count($events));
+        $this->assertInstanceOf('\core\event\user_graded', $events[0]);
+        $this->assertEquals($gradeitem->id, $events[0]->other['itemid']);
+        $this->assertInstanceOf('\core\event\user_graded', $events[1]);
+        $this->assertEquals($courseitem->id, $events[1]->other['itemid']);
 
-        // Get the grade item.
-        $gradeitem = grade_item::fetch(array('itemtype' => 'mod', 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id,
-            'courseid' => $course->id));
-
-        // Let's alter the grade in the DB so when we call regrade_final_grades() it is changed and an event is called.
-        $sql = "UPDATE {grade_grades}
-                   SET finalgrade = '2'
-                 WHERE itemid = :itemid
-                   AND userid = :userid";
-        $DB->execute($sql, array('itemid' => $gradeitem->id, 'userid' => $user->id));
-
-        // Now check when we regrade this that there is a user graded event.
-        $sink = $this->redirectEvents();
-        $gradeitem->regrade_final_grades();
-        $events = $sink->get_events();
-        $event = reset($events);
-        $sink->close();
-
-        // Ensure we have a user_graded event.
-        $this->assertEquals(1, count($events));
-        $this->assertInstanceOf('\core\event\user_graded', $event);
-
-        // Remove the grades.
+        // Remove the grades, force the regrading and re-fetch the item. This is needed because the item
+        // will be set as needing an update when the grades are deleted.
         $gradeitem->delete_all_grades();
+        grade_regrade_final_grades($course->id);
+        $gradeitem = grade_item::fetch($quizitemparams);
 
-        // Now, create a grade using update_raw_grade().
+        // Now, create a grade using grade_item::update_final_grade().
         $sink = $this->redirectEvents();
-        $gradeitem->update_raw_grade($user->id, 50);
+        $gradeitem->update_raw_grade($user->id, 10);
         $events = $sink->get_events();
-        $event = reset($events);
         $sink->close();
 
-        // Ensure we have a user_graded event.
-        $this->assertEquals(1, count($events));
-        $this->assertInstanceOf('\core\event\user_graded', $event);
+        // Ensure we have two user_graded events, one for the item, one for the course.
+        $this->assertEquals(2, count($events));
+        $this->assertInstanceOf('\core\event\user_graded', $events[0]);
+        $this->assertEquals($gradeitem->id, $events[0]->other['itemid']);
+        $this->assertInstanceOf('\core\event\user_graded', $events[1]);
+        $this->assertEquals($courseitem->id, $events[1]->other['itemid']);
 
-        // Now, update this grade using update_raw_grade().
+        // Now, update this grade using grade_item::update_raw_grade().
         $sink = $this->redirectEvents();
-        $gradeitem->update_raw_grade($user->id, 100);
+        $gradeitem->update_raw_grade($user->id, 20);
         $events = $sink->get_events();
-        $event = reset($events);
         $sink->close();
 
-        $this->assertEquals(1, count($events));
-        $this->assertInstanceOf('\core\event\user_graded', $event);
+        // Ensure we have two user_graded events, one for the item, one for the course.
+        $this->assertEquals(2, count($events));
+        $this->assertInstanceOf('\core\event\user_graded', $events[0]);
+        $this->assertEquals($gradeitem->id, $events[0]->other['itemid']);
+        $this->assertInstanceOf('\core\event\user_graded', $events[1]);
+        $this->assertEquals($courseitem->id, $events[1]->other['itemid']);
 
-        // Remove the grades.
+        // Remove the grades, force the regrading and re-fetch the item. This is needed because the item
+        // will be set as needing an update when the grades are deleted.
         $gradeitem->delete_all_grades();
+        grade_regrade_final_grades($course->id);
+        $gradeitem = grade_item::fetch($quizitemparams);
 
-        // Now, create a grade using update_final_grade().
+        // Now, create a grade using grade_item::update_final_grade().
         $sink = $this->redirectEvents();
-        $gradeitem->update_final_grade($user->id, 50);
+        $gradeitem->update_final_grade($user->id, 30);
         $events = $sink->get_events();
-        $event = reset($events);
         $sink->close();
 
-        // Ensure we have a user_graded event.
-        $this->assertEquals(1, count($events));
-        $this->assertInstanceOf('\core\event\user_graded', $event);
+        // Ensure we have two user_graded events, one for the item, one for the course.
+        $this->assertEquals(2, count($events));
+        $this->assertInstanceOf('\core\event\user_graded', $events[0]);
+        $this->assertEquals($gradeitem->id, $events[0]->other['itemid']);
+        $this->assertInstanceOf('\core\event\user_graded', $events[1]);
+        $this->assertEquals($courseitem->id, $events[1]->other['itemid']);
 
-        // Now, update this grade using update_final_grade().
+        // Now, update this grade using grade_item::update_final_grade().
         $sink = $this->redirectEvents();
-        $gradeitem->update_final_grade($user->id, 100);
+        $gradeitem->update_final_grade($user->id, 40);
         $events = $sink->get_events();
-        $event = reset($events);
         $sink->close();
 
-        $this->assertEquals(1, count($events));
-        $this->assertInstanceOf('\core\event\user_graded', $event);
+        // Ensure we have two user_graded events, one for the item, one for the course.
+        $this->assertEquals(2, count($events));
+        $this->assertInstanceOf('\core\event\user_graded', $events[0]);
+        $this->assertEquals($gradeitem->id, $events[0]->other['itemid']);
+        $this->assertInstanceOf('\core\event\user_graded', $events[1]);
+        $this->assertEquals($courseitem->id, $events[1]->other['itemid']);
+
+        // Remove the overridden flag from the grade, this was set by grade_item::update_final_grade().
+        $gradegrade = grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $user->id));
+        $gradegrade->set_overridden(false, false);
 
         // Let's change the calculation to anything that won't cause an error.
         $calculation = calc_formula::unlocalize("=3");
         $gradeitem->set_calculation($calculation);
 
-        // Let's alter the grade in the DB so when we call compute() it is changed and an event is called.
-        $sql = "UPDATE {grade_grades}
-                   SET finalgrade = 2, overridden = 0
-                 WHERE itemid = :itemid
-                   AND userid = :userid";
-        $DB->execute($sql, array('itemid' => $gradeitem->id, 'userid' => $user->id));
+        // Now force the computation of the grade.
+        $sink = $this->redirectEvents();
+        grade_regrade_final_grades($course->id);
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Ensure we have two user_graded events, one for the item, one for the course.
+        $this->assertEquals(2, count($events));
+        $this->assertInstanceOf('\core\event\user_graded', $events[0]);
+        $this->assertEquals($gradeitem->id, $events[0]->other['itemid']);
+        $this->assertInstanceOf('\core\event\user_graded', $events[1]);
+        $this->assertEquals($courseitem->id, $events[1]->other['itemid']);
+
+        // Now, let's trick the gradebook, we manually update a grade, and flag the grade item as
+        // needing a regrading, so we can trigger the event in grade_item::regrade_final_grades().
+        $gradeitem = grade_item::fetch($quizitemparams);
+        $gradeitem->set_calculation('');
+        $gradegrade = grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $user->id));
+        $gradegrade->rawgrade = 50;
+        $gradegrade->update();
 
-        // Now check when we compute that there is a user graded event.
         $sink = $this->redirectEvents();
-        $gradeitem->compute();
+        grade_regrade_final_grades($course->id);
         $events = $sink->get_events();
-        $event = reset($events);
         $sink->close();
 
-        $this->assertEquals(1, count($events));
-        $this->assertInstanceOf('\core\event\user_graded', $event);
+        // Ensure we have two user_graded events, one for the item, one for the course.
+        $this->assertEquals(2, count($events));
+        $this->assertInstanceOf('\core\event\user_graded', $events[0]);
+        $this->assertEquals($gradeitem->id, $events[0]->other['itemid']);
+        $this->assertInstanceOf('\core\event\user_graded', $events[1]);
+        $this->assertEquals($courseitem->id, $events[1]->other['itemid']);
     }
 }
index 1b0c2bc..10b4007 100644 (file)
@@ -51,6 +51,13 @@ class core_questionlib_testcase extends advanced_testcase {
         $this->resetAfterTest();
     }
 
+    /**
+     * Tidy up open files that may be left open.
+     */
+    protected function tearDown() {
+        gc_collect_cycles();
+    }
+
     public function test_question_reorder_qtypes() {
         $this->assertEquals(
             array(0 => 't2', 1 => 't1', 2 => 't3'),
index 599c501..27b401a 100644 (file)
@@ -157,6 +157,45 @@ class core_scheduled_task_testcase extends advanced_testcase {
         date_default_timezone_set($currenttimezonephp);
     }
 
+    public function test_reset_scheduled_tasks_for_component() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+        // Remember the defaults.
+        $defaulttasks = \core\task\manager::load_scheduled_tasks_for_component('moodle');
+        $initcount = count($defaulttasks);
+        // Customise a task.
+        $firsttask = reset($defaulttasks);
+        $firsttask->set_minute('1');
+        $firsttask->set_hour('2');
+        $firsttask->set_month('3');
+        $firsttask->set_day_of_week('4');
+        $firsttask->set_day('5');
+        $firsttask->set_customised('1');
+        \core\task\manager::configure_scheduled_task($firsttask);
+        $firsttaskrecord = \core\task\manager::record_from_scheduled_task($firsttask);
+        // We reset this field, because we do not want to compare it.
+        $firsttaskrecord->nextruntime = '0';
+
+        // Now call reset on all the tasks.
+        \core\task\manager::reset_scheduled_tasks_for_component('moodle');
+
+        // Load the tasks again.
+        $defaulttasks = \core\task\manager::load_scheduled_tasks_for_component('moodle');
+        $finalcount = count($defaulttasks);
+        // Compare the first task.
+        $newfirsttask = reset($defaulttasks);
+        $newfirsttaskrecord = \core\task\manager::record_from_scheduled_task($newfirsttask);
+        // We reset this field, because we do not want to compare it.
+        $newfirsttaskrecord->nextruntime = '0';
+
+        // Assert a customised task was not altered by reset.
+        $this->assertEquals($firsttaskrecord, $newfirsttaskrecord);
+
+        // Assert we have the same number of tasks.
+        $this->assertEquals($initcount, $finalcount);
+    }
+
     public function test_get_next_scheduled_task() {
         global $DB;
 
index 6afe93f..207a64b 100644 (file)
@@ -52,6 +52,13 @@ class assignfeedback_editpdf_testcase extends mod_assign_base_testcase {
         parent::setUp();
     }
 
+    /**
+     * Tidy up open files that may be left open.
+     */
+    protected function tearDown() {
+        gc_collect_cycles();
+    }
+
     protected function create_assign_and_submit_pdf() {
         global $CFG;
         $assign = $this->create_instance(array('assignsubmission_onlinetext_enabled' => 1,
index fc5eb63..29239ec 100644 (file)
@@ -55,9 +55,9 @@ class mod_forum_mail_testcase extends advanced_testcase {
         $messages = $helper->mailsink->get_messages();
         $this->assertEquals(0, count($messages));
 
-        // Forcibly reduce the maxeditingtime to a one second to ensure that
-        // messages are sent out.
-        $CFG->maxeditingtime = 1;
+        // Forcibly reduce the maxeditingtime to a second in the past to
+        // ensure that messages are sent out.
+        $CFG->maxeditingtime = -1;
 
         // Ensure that we don't prevent e-mail as this will cause unit test failures.
         $CFG->noemailever = false;
@@ -107,6 +107,7 @@ class mod_forum_mail_testcase extends advanced_testcase {
      * @param array An array containing the discussion object, and the post object
      */
     protected function helper_post_to_forum($forum, $author) {
+        global $DB;
         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
 
         // Create a discussion in the forum, and then add a post to that discussion.
@@ -116,13 +117,8 @@ class mod_forum_mail_testcase extends advanced_testcase {
         $record->forum = $forum->id;
         $discussion = $generator->create_discussion($record);
 
-        $record = new stdClass();
-        $record->course = $forum->course;
-        $record->userid = $author->id;
-        $record->forum = $forum->id;
-        $record->discussion = $discussion->id;
-        $record->mailnow = 1;
-        $post = $generator->create_post($record);
+        // Retrieve the post which was created by create_discussion.
+        $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
 
         return array($discussion, $post);
     }
index 4da9931..9e658dc 100644 (file)
@@ -52,6 +52,18 @@ $capabilities = array(
         )
     ),
 
+    // Grade essay questions.
+    'mod/lesson:grade' => array(
+        'riskbitmask' => RISK_SPAM,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'teacher' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW
+        )
+    ),
+
     'mod/lesson:manage' => array(
 
         'captype' => 'write',
index 3026e20..11e58a4 100644 (file)
@@ -38,7 +38,7 @@ $lesson = new lesson($dblesson);
 
 require_login($course, false, $cm);
 $context = context_module::instance($cm->id);
-require_capability('mod/lesson:edit', $context);
+require_capability('mod/lesson:grade', $context);
 
 $url = new moodle_url('/mod/lesson/essay.php', array('id'=>$id));
 if ($mode !== 'display') {
index 594554f..b0232f9 100644 (file)
@@ -220,6 +220,7 @@ $string['leftduringtimedsession'] = 'You have left during a timed lesson.';
 $string['leftduringtimed'] = 'You have left during a timed lesson.<br />Please click on Continue to restart the lesson.';
 $string['leftduringtimednoretake'] = 'You have left during a timed lesson and you are<br />not allowed to retake or continue the lesson.';
 $string['lesson:addinstance'] = 'Add a new lesson';
+$string['lesson:grade'] = 'Grade lesson essay questions';
 $string['lessonattempted'] = 'Lesson attempted';
 $string['lessonclosed'] = 'This lesson closed on {$a}.';
 $string['lessoncloses'] = 'Lesson closes';
index fe2923d..7bab80f 100644 (file)
@@ -773,12 +773,10 @@ function lesson_supports($feature) {
 function lesson_extend_settings_navigation($settings, $lessonnode) {
     global $PAGE, $DB;
 
-    $canedit = has_capability('mod/lesson:edit', $PAGE->cm->context);
-
     $url = new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id));
     $lessonnode->add(get_string('preview', 'lesson'), $url);
 
-    if ($canedit) {
+    if (has_capability('mod/lesson:edit', $PAGE->cm->context)) {
         $url = new moodle_url('/mod/lesson/edit.php', array('id'=>$PAGE->cm->id));
         $lessonnode->add(get_string('edit', 'lesson'), $url);
     }
@@ -791,7 +789,7 @@ function lesson_extend_settings_navigation($settings, $lessonnode) {
         $reportsnode->add(get_string('detailedstats', 'lesson'), $url);
     }
 
-    if ($canedit) {
+    if (has_capability('mod/lesson:grade', $PAGE->cm->context)) {
         $url = new moodle_url('/mod/lesson/essay.php', array('id'=>$PAGE->cm->id));
         $lessonnode->add(get_string('manualgrading', 'lesson'), $url);
     }
index f804279..89263af 100644 (file)
@@ -51,7 +51,7 @@ $attemptscount = $DB->count_records('lesson_grades', array('lessonid'=>$lesson->
 $row[] = new tabobject('view', "$CFG->wwwroot/mod/lesson/view.php?id=$cm->id", get_string('preview', 'lesson'), get_string('previewlesson', 'lesson', format_string($lesson->name)));
 $row[] = new tabobject('edit', "$CFG->wwwroot/mod/lesson/edit.php?id=$cm->id", get_string('edit', 'lesson'), get_string('edita', 'moodle', format_string($lesson->name)));
 $row[] = new tabobject('reports', "$CFG->wwwroot/mod/lesson/report.php?id=$cm->id", get_string('reports', 'lesson'), get_string('viewreports2', 'lesson', $attemptscount));
-if (has_capability('mod/lesson:edit', $context)) {
+if (has_capability('mod/lesson:grade', $context)) {
     $row[] = new tabobject('essay', "$CFG->wwwroot/mod/lesson/essay.php?id=$cm->id", get_string('manualgrading', 'lesson'));
 }
 if ($lesson->highscores) {
diff --git a/mod/lesson/tests/behat/teacher_grade_essays.feature b/mod/lesson/tests/behat/teacher_grade_essays.feature
new file mode 100644 (file)
index 0000000..e6068b5
--- /dev/null
@@ -0,0 +1,57 @@
+@mod @mod_lesson
+Feature: In a lesson activity, a non editing teacher can grade essay questions
+  As a non editing teacher
+  I need to grade student answers to essay questions in lesson
+
+  @javascript
+  Scenario: non editing teacher grade essay questions
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+      | teacher2 | Teacher | 2 | teacher2@asd.com |
+      | student1 | Student | 1 | student1@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | teacher2 | C1 | teacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I am on homepage
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Lesson" to section "1" and I fill the form with:
+      | Name | Test lesson name |
+      | Description | Test lesson description |
+    And I follow "Test lesson name"
+    And I follow "Add a question page"
+    And I set the field "Select a question type" to "Essay"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title | Essay question |
+      | Page contents | <p>Please write a story about a frog.</p> |
+    And I press "Save page"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I set the field "Your answer" to "<p>Once upon a time there was a little green frog."
+    And I press "Submit"
+    And I log out
+    When I log in as "teacher2"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    Then I should see "Grade essays"
+    And I follow "Grade essays"
+    And I should see "Student 1"
+    And I should see "Essay question"
+    And I follow "Essay question"
+    And I should see "Student 1's response"
+    And I should see "Once upon a time there was a little green frog."
+    And I set the following fields to these values:
+      | Your comments | Well done. |
+      | Essay score | 1 |
+    And I press "Save changes"
+    And I should see "Changes saved"
index 1101874..9c2e2b0 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014100600;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2014101600;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2014050800;    // Requires this Moodle version
 $plugin->component = 'mod_lesson'; // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 61910e8..1fbe2aa 100644 (file)
@@ -867,6 +867,7 @@ ORDER BY
     public function update_question_attempt(question_attempt $qa) {
         $record = new stdClass();
         $record->id = $qa->get_database_id();
+        $record->variant = $qa->get_variant();
         $record->maxmark = $qa->get_max_mark();
         $record->minfraction = $qa->get_min_fraction();
         $record->maxfraction = $qa->get_max_fraction();
@@ -1076,6 +1077,20 @@ ORDER BY
      * @param number $newmaxmark the new max mark to set.
      */
     public function set_max_mark_in_attempts(qubaid_condition $qubaids, $slot, $newmaxmark) {
+        if ($this->db->get_dbfamily() == 'mysql') {
+            // MySQL's query optimiser completely fails to cope with the
+            // set_field_select call below, so we have to give it a clue. See MDL-32616.
+            // TODO MDL-29589 encapsulate this MySQL-specific code with a $DB method.
+            $this->db->execute("
+                    UPDATE " . $qubaids->from_question_attempts('qa') . "
+                       SET qa.maxmark = :newmaxmark
+                     WHERE " . $qubaids->where() . "
+                       AND slot = :slot
+                    ", $qubaids->from_where_params() + array('newmaxmark' => $newmaxmark, 'slot' => $slot));
+            return;
+        }
+
+        // Normal databases.
         $this->db->set_field_select('question_attempts', 'maxmark', $newmaxmark,
                 "questionusageid {$qubaids->usage_id_in()} AND slot = :slot",
                 $qubaids->usage_id_in_params() + array('slot' => $slot));
index 3d1c55b..8a59289 100644 (file)
@@ -1178,6 +1178,9 @@ class question_attempt {
             if ($pendingstep->response_summary_changed()) {
                 $this->responsesummary = $pendingstep->get_new_response_summary();
             }
+            if ($pendingstep->variant_number_changed()) {
+                $this->variant = $pendingstep->get_new_variant_number();
+            }
         }
     }
 
index e18f997..101a37f 100644 (file)
@@ -432,15 +432,23 @@ class question_attempt_step {
 
 
 /**
- * A subclass with a bit of additional funcitonality, for pending steps.
+ * A subclass of {@link question_attempt_step} used when processing a new submission.
+ *
+ * When we are processing some new submitted data, which may or may not lead to
+ * a new step being added to the {@link question_usage_by_activity} we create an
+ * instance of this class. which is then passed to the question behaviour and question
+ * type for processing. At the end of processing we then may, or may not, keep it.
  *
  * @copyright  2010 The Open University
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class question_attempt_pending_step extends question_attempt_step {
-    /** @var string . */
+    /** @var string the new response summary, if there is one. */
     protected $newresponsesummary = null;
 
+    /** @var int the new variant number, if there is one. */
+    protected $newvariant = null;
+
     /**
      * If as a result of processing this step, the response summary for the
      * question attempt should changed, you should call this method to set the
@@ -451,15 +459,48 @@ class question_attempt_pending_step extends question_attempt_step {
         $this->newresponsesummary = $responsesummary;
     }
 
-    /** @return string the new response summary, if any. */
+    /**
+     * Get the new response summary, if there is one.
+     * @return string the new response summary, or null if it has not changed.
+     */
     public function get_new_response_summary() {
         return $this->newresponsesummary;
     }
 
-    /** @return string whether this step changes the response summary. */
+    /**
+     * Whether this processing this step has changed the response summary.
+     * @return bool true if there is a new response summary.
+     */
     public function response_summary_changed() {
         return !is_null($this->newresponsesummary);
     }
+
+    /**
+     * If as a result of processing this step, you identify that this variant of the
+     * question is acutally identical to the another one, you may change the
+     * variant number recorded, in order to give better statistics. For an example
+     * see qbehaviour_opaque.
+     * @param int $variant the new variant number.
+     */
+    public function set_new_variant_number($variant) {
+        $this->newvariant = $variant;
+    }
+
+    /**
+     * Get the new variant number, if there is one.
+     * @return int the new variant number, or null if it has not changed.
+     */
+    public function get_new_variant_number() {
+        return $this->newvariant;
+    }
+
+    /**
+     * Whether this processing this step has changed the variant number.
+     * @return bool true if there is a new variant number.
+     */
+    public function variant_number_changed() {
+        return !is_null($this->newvariant);
+    }
 }
 
 
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * This file contains tests for the autosave code in the question_usage class.
+ * Unit tests for the parts of {@link question_engine_data_mapper} related to reporting.
  *
- * @package    moodlecore
- * @subpackage questionengine
- * @copyright  2013 The Open University
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package   core_question
+ * @category  test
+ * @copyright 2013 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 
@@ -32,17 +32,12 @@ require_once(dirname(__FILE__) . '/helpers.php');
 
 
 /**
- * Unit tests for the autosave parts of the {@link question_usage} class.
- *
- * Note that many of the methods used when attempting questions, like
- * load_questions_usage_by_activity, insert_question_*, delete_steps are
- * tested elsewhere, e.g. by {@link question_usage_autosave_test}. We do not
- * re-test them here.
+ * Unit tests for the parts of {@link question_engine_data_mapper} related to reporting.
  *
  * @copyright 2013 The Open University
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class question_engine_data_mapper_testcase extends qbehaviour_walkthrough_test_base {
+class question_engine_data_mapper_reporting_testcase extends qbehaviour_walkthrough_test_base {
 
     /** @var question_engine_data_mapper */
     protected $dm;
@@ -132,6 +127,9 @@ class question_engine_data_mapper_testcase extends qbehaviour_walkthrough_test_b
         $this->dotest_question_attempt_latest_state_view();
     }
 
+    /**
+     * This test is executed by {@link test_reporting_queries()}.
+     */
     protected function dotest_load_questions_usages_latest_steps() {
         $rawstates = $this->dm->load_questions_usages_latest_steps($this->bothusages, $this->allslots,
                 'qa.id AS questionattemptid, qa.questionusageid, qa.slot, ' .
@@ -178,6 +176,9 @@ class question_engine_data_mapper_testcase extends qbehaviour_walkthrough_test_b
         ), $state);
     }
 
+    /**
+     * This test is executed by {@link test_reporting_queries()}.
+     */
     protected function dotest_load_questions_usages_question_state_summary() {
         $summary = $this->dm->load_questions_usages_question_state_summary(
                 $this->bothusages, $this->allslots);
@@ -206,6 +207,9 @@ class question_engine_data_mapper_testcase extends qbehaviour_walkthrough_test_b
                 ));
     }
 
+    /**
+     * This test is executed by {@link test_reporting_queries()}.
+     */
     protected function dotest_load_questions_usages_where_question_in_state() {
         $this->assertEquals(
                 array(array($this->usageids[0], $this->usageids[1]), 2),
@@ -223,6 +227,9 @@ class question_engine_data_mapper_testcase extends qbehaviour_walkthrough_test_b
                 'needsgrading', $this->allslots[1], null, 'questionusageid'));
     }
 
+    /**
+     * This test is executed by {@link test_reporting_queries()}.
+     */
     protected function dotest_load_average_marks() {
         $averages = $this->dm->load_average_marks($this->bothusages);
 
@@ -240,6 +247,9 @@ class question_engine_data_mapper_testcase extends qbehaviour_walkthrough_test_b
         ), $averages);
     }
 
+    /**
+     * This test is executed by {@link test_reporting_queries()}.
+     */
     protected function dotest_sum_usage_marks_subquery() {
         global $DB;
 
@@ -253,6 +263,9 @@ class question_engine_data_mapper_testcase extends qbehaviour_walkthrough_test_b
         $this->assertEquals(0, $totals[$this->usageids[1]]);
     }
 
+    /**
+     * This test is executed by {@link test_reporting_queries()}.
+     */
     protected function dotest_question_attempt_latest_state_view() {
         global $DB;
 
diff --git a/question/engine/tests/datalib_test.php b/question/engine/tests/datalib_test.php
new file mode 100644 (file)
index 0000000..9279626
--- /dev/null
@@ -0,0 +1,129 @@
+<?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 parts of {@link question_engine_data_mapper}.
+ *
+ * @package   core_question
+ * @category  test
+ * @copyright 2014 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(dirname(__FILE__) . '/../lib.php');
+require_once(dirname(__FILE__) . '/helpers.php');
+
+
+/**
+ * Unit tests for parts of {@link question_engine_data_mapper}.
+ *
+ * Note that many of the methods used when attempting questions, like
+ * load_questions_usage_by_activity, insert_question_*, delete_steps are
+ * tested elsewhere, e.g. by {@link question_usage_autosave_test}. We do not
+ * re-test them here.
+ *
+ * @copyright 2014 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_engine_data_mapper_testcase extends qbehaviour_walkthrough_test_base {
+
+    /**
+     * We create two usages, each with two questions, a short-answer marked
+     * out of 5, and and essay marked out of 10. We just start these attempts.
+     *
+     * Then we change the max mark for the short-answer question in one of the
+     * usages to 20, using a qubaid_list, and verify.
+     *
+     * Then we change the max mark for the essay question in the other
+     * usage to 2, using a qubaid_join, and verify.
+     */
+    public function test_set_max_mark_in_attempts() {
+
+        // Set up some things the tests will need.
+        $this->resetAfterTest();
+        $dm = new question_engine_data_mapper();
+
+        // Create the questions.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $cat = $generator->create_question_category();
+        $sa = $generator->create_question('shortanswer', null,
+                array('category' => $cat->id));
+        $essay = $generator->create_question('essay', null,
+                array('category' => $cat->id));
+
+        // Create the first usage.
+        $q = question_bank::load_question($sa->id);
+        $this->start_attempt_at_question($q, 'interactive', 5);
+
+        $q = question_bank::load_question($essay->id);
+        $this->start_attempt_at_question($q, 'interactive', 10);
+
+        $this->finish();
+        $this->save_quba();
+        $usage1id = $this->quba->get_id();
+
+        // Create the second usage.
+        $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
+                context_system::instance());
+
+        $q = question_bank::load_question($sa->id);
+        $this->start_attempt_at_question($q, 'interactive', 5);
+        $this->process_submission(array('answer' => 'fish'));
+
+        $q = question_bank::load_question($essay->id);
+        $this->start_attempt_at_question($q, 'interactive', 10);
+
+        $this->finish();
+        $this->save_quba();
+        $usage2id = $this->quba->get_id();
+
+        // Test set_max_mark_in_attempts with a qubaid_list.
+        $usagestoupdate = new qubaid_list(array($usage1id));
+        $dm->set_max_mark_in_attempts($usagestoupdate, 1, 20.0);
+        $quba1 = question_engine::load_questions_usage_by_activity($usage1id);
+        $quba2 = question_engine::load_questions_usage_by_activity($usage2id);
+        $this->assertEquals(20, $quba1->get_question_max_mark(1));
+        $this->assertEquals(10, $quba1->get_question_max_mark(2));
+        $this->assertEquals( 5, $quba2->get_question_max_mark(1));
+        $this->assertEquals(10, $quba2->get_question_max_mark(2));
+
+        // Test set_max_mark_in_attempts with a qubaid_join.
+        $usagestoupdate = new qubaid_join('{question_usages} qu', 'qu.id',
+                'qu.id = :usageid', array('usageid' => $usage2id));
+        $dm->set_max_mark_in_attempts($usagestoupdate, 2, 2.0);
+        $quba1 = question_engine::load_questions_usage_by_activity($usage1id);
+        $quba2 = question_engine::load_questions_usage_by_activity($usage2id);
+        $this->assertEquals(20, $quba1->get_question_max_mark(1));
+        $this->assertEquals(10, $quba1->get_question_max_mark(2));
+        $this->assertEquals( 5, $quba2->get_question_max_mark(1));
+        $this->assertEquals( 2, $quba2->get_question_max_mark(2));
+
+        // Test the nothing to do case.
+        $usagestoupdate = new qubaid_join('{question_usages} qu', 'qu.id',
+                'qu.id = :usageid', array('usageid' => -1));
+        $dm->set_max_mark_in_attempts($usagestoupdate, 2, 2.0);
+        $quba1 = question_engine::load_questions_usage_by_activity($usage1id);
+        $quba2 = question_engine::load_questions_usage_by_activity($usage2id);
+        $this->assertEquals(20, $quba1->get_question_max_mark(1));
+        $this->assertEquals(10, $quba1->get_question_max_mark(2));
+        $this->assertEquals( 5, $quba2->get_question_max_mark(1));
+        $this->assertEquals( 2, $quba2->get_question_max_mark(2));
+    }
+}
index c097b67..2d717bb 100644 (file)
@@ -37,7 +37,7 @@ $timefrom   = optional_param('timefrom', 0, PARAM_INT); // how far back to look.
 $action     = optional_param('action', '', PARAM_ALPHA);
 $page       = optional_param('page', 0, PARAM_INT);                     // which page to show
 $perpage    = optional_param('perpage', DEFAULT_PAGE_SIZE, PARAM_INT);  // how many per page
-$currentgroup = optional_param('group', 0, PARAM_INT); // Get the active group.
+$currentgroup = optional_param('group', null, PARAM_INT); // Get the active group.
 
 $url = new moodle_url('/report/participation/index.php', array('id'=>$id));
 if ($roleid !== 0) $url->param('roleid');
@@ -128,8 +128,15 @@ if ($onlyuselegacyreader) {
 // Print first controls.
 report_participation_print_filter_form($course, $timefrom, $minlog, $action, $roleid, $instanceid);
 
-$baseurl =  $CFG->wwwroot.'/report/participation/index.php?id='.$course->id.'&amp;roleid='
-    .$roleid.'&amp;instanceid='.$instanceid.'&amp;timefrom='.$timefrom.'&amp;action='.$action.'&amp;perpage='.$perpage;
+$baseurl = new moodle_url('/report/participation/index.php', array(
+    'id' => $course->id,
+    'roleid' => $roleid,
+    'instanceid' => $instanceid,
+    'timefrom' => $timefrom,
+    'action' => $action,
+    'perpage' => $perpage,
+    'group' => $currentgroup
+));
 $select = groups_allgroups_course_menu($course, $baseurl, true, $currentgroup);
 
 // User cannot see any group.
@@ -341,10 +348,15 @@ if (!empty($instanceid) && !empty($roleid)) {
     $table->print_html();
 
     if ($perpage == SHOW_ALL_PAGE_SIZE) {
-        echo '<div id="showall"><a href="'.$baseurl.'&amp;perpage='.DEFAULT_PAGE_SIZE.'">'.get_string('showperpage', '', DEFAULT_PAGE_SIZE).'</a></div>'."\n";
-    }
-    else if ($matchcount > 0 && $perpage < $matchcount) {
-        echo '<div id="showall"><a href="'.$baseurl.'&amp;perpage='.SHOW_ALL_PAGE_SIZE.'">'.get_string('showall', '', $matchcount).'</a></div>'."\n";
+        $perpageurl = new moodle_url($baseurl, array('perpage' => DEFAULT_PAGE_SIZE));
+        echo html_writer::start_div('', array('id' => 'showall'));
+        echo html_writer::link($perpageurl, get_string('showperpage', '', DEFAULT_PAGE_SIZE));
+        echo html_writer::end_div();
+    } else if ($matchcount > 0 && $perpage < $matchcount) {
+        $perpageurl = new moodle_url($baseurl, array('perpage' => SHOW_ALL_PAGE_SIZE));
+        echo html_writer::start_div('', array('id' => 'showall'));
+        echo html_writer::link($perpageurl, get_string('showall', '', $matchcount));
+        echo html_writer::end_div();
     }
 
     echo '<div class="selectbuttons">';
index 1a4de5b..7b6c009 100644 (file)
@@ -75,7 +75,6 @@ echo $OUTPUT->doctype() ?>
             <div class="breadcrumb-button"><?php echo $OUTPUT->page_heading_button(); ?></div>
         </div>
         <?php echo $OUTPUT->page_heading(); ?>
-        <?php echo $OUTPUT->user_menu(); ?>
         <div id="course-header">
             <?php echo $OUTPUT->course_header(); ?>
         </div>
index 5db6f95..c1adf89 100644 (file)
@@ -410,9 +410,9 @@ list($esql, $params) = get_enrolled_sql($context, null, $currentgroup, true);
 $joins = array("FROM {user} u");
 $wheres = array();
 
-$mainuserfields = user_picture::fields('u', array('username', 'email', 'city', 'country', 'lang', 'timezone', 'maildisplay'));
-$alreadyretrievedfields = explode(',', $mainuserfields);
-$extrasql = get_extra_user_fields_sql($context, 'u', '', $alreadyretrievedfields);
+$userfields = array('username', 'email', 'city', 'country', 'lang', 'timezone', 'maildisplay');
+$mainuserfields = user_picture::fields('u', $userfields);
+$extrasql = get_extra_user_fields_sql($context, 'u', '', $userfields);
 
 if ($isfrontpage) {
     $select = "SELECT $mainuserfields, u.lastaccess$extrasql";
index 23c8919..101b88e 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2014102000.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2014102200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '2.8beta+ (Build: 20141017)'; // Human-friendly version name
+$release  = '2.8beta+ (Build: 20141022)'; // Human-friendly version name
 
 $branch   = '28';                       // This version's branch.
 $maturity = MATURITY_BETA;             // This version's maturity level.