Merge branch 'MDL-47686-master' of git://github.com/damyon/moodle
authorDan Poltawski <dan@moodle.com>
Thu, 16 Oct 2014 09:39:12 +0000 (10:39 +0100)
committerDan Poltawski <dan@moodle.com>
Thu, 16 Oct 2014 09:39:12 +0000 (10:39 +0100)
163 files changed:
admin/cli/mysql_compressed_rows.php
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/monitor/classes/eventlist.php [new file with mode: 0644]
admin/tool/monitor/classes/eventobservers.php [new file with mode: 0644]
admin/tool/monitor/classes/notification_task.php [new file with mode: 0644]
admin/tool/monitor/classes/output/helpicon/renderable.php [new file with mode: 0644]
admin/tool/monitor/classes/output/helpicon/renderer.php [new file with mode: 0644]
admin/tool/monitor/classes/output/managerules/renderable.php [new file with mode: 0644]
admin/tool/monitor/classes/output/managerules/renderer.php [new file with mode: 0644]
admin/tool/monitor/classes/output/managesubs/renderer.php [new file with mode: 0644]
admin/tool/monitor/classes/output/managesubs/rules.php [new file with mode: 0644]
admin/tool/monitor/classes/output/managesubs/subs.php [new file with mode: 0644]
admin/tool/monitor/classes/rule.php [new file with mode: 0644]
admin/tool/monitor/classes/rule_form.php [new file with mode: 0644]
admin/tool/monitor/classes/rule_manager.php [new file with mode: 0644]
admin/tool/monitor/classes/subscription.php [new file with mode: 0644]
admin/tool/monitor/classes/subscription_manager.php [new file with mode: 0644]
admin/tool/monitor/db/access.php [new file with mode: 0644]
admin/tool/monitor/db/events.php [new file with mode: 0644]
admin/tool/monitor/db/install.xml [new file with mode: 0644]
admin/tool/monitor/db/messages.php [new file with mode: 0644]
admin/tool/monitor/edit.php [new file with mode: 0644]
admin/tool/monitor/help.php [new file with mode: 0644]
admin/tool/monitor/help_ajax.php [new file with mode: 0644]
admin/tool/monitor/index.php [new file with mode: 0644]
admin/tool/monitor/lang/en/tool_monitor.php [new file with mode: 0644]
admin/tool/monitor/lib.php [new file with mode: 0644]
admin/tool/monitor/managerules.php [new file with mode: 0644]
admin/tool/monitor/settings.php [new file with mode: 0644]
admin/tool/monitor/tests/behat/rule.feature [new file with mode: 0644]
admin/tool/monitor/tests/behat/subscription.feature [new file with mode: 0644]
admin/tool/monitor/tests/eventobservers_test.php [new file with mode: 0644]
admin/tool/monitor/tests/generator/lib.php [new file with mode: 0644]
admin/tool/monitor/tests/generator_test.php [new file with mode: 0644]
admin/tool/monitor/tests/rule_manager_test.php [new file with mode: 0644]
admin/tool/monitor/version.php [new file with mode: 0644]
admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-debug.js [new file with mode: 0644]
admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-min.js [new file with mode: 0644]
admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown.js [new file with mode: 0644]
admin/tool/monitor/yui/src/dropdown/build.json [new file with mode: 0644]
admin/tool/monitor/yui/src/dropdown/js/dropdown.js [new file with mode: 0644]
admin/tool/monitor/yui/src/dropdown/meta/dropdown.json [new file with mode: 0644]
blocks/badges/db/upgrade.php
blocks/calendar_month/db/upgrade.php
blocks/calendar_upcoming/db/upgrade.php
cache/stores/memcache/lib.php
cache/stores/memcache/tests/memcache_test.php
cache/tests/fixtures/stores.php
grade/edit/tree/outcomeitem.php
grade/edit/tree/outcomeitem_form.php
grade/import/csv/tests/load_data_test.php
grade/lib.php
grade/report/grader/index.php
grade/report/grader/lib.php
grade/report/user/db/upgrade.php [new file with mode: 0644]
grade/report/user/lib.php
grade/report/user/settings.php
grade/report/user/version.php
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_single_item_scales.feature [new file with mode: 0644]
grade/tests/report_graderlib_test.php
install/lang/vi/error.php
lang/en/grades.php
lib/accesslib.php
lib/blocklib.php
lib/classes/plugin_manager.php
lib/csvlib.class.php
lib/db/upgrade.php
lib/editor/atto/lang/en/editor_atto.php
lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-usedfiles/moodle-atto_managefiles-usedfiles-debug.js
lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-usedfiles/moodle-atto_managefiles-usedfiles-min.js
lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-usedfiles/moodle-atto_managefiles-usedfiles.js
lib/editor/atto/plugins/managefiles/yui/src/usedfiles/js/usedfiles.js
lib/editor/tinymce/plugins/managefiles/tinymce/editor_plugin.js
lib/grade/grade_category.php
lib/navigationlib.php
lib/outputrenderers.php
lib/testing/generator/data_generator.php
lib/tests/behat/behat_data_generators.php
lib/tests/cronlib_test.php
lib/tests/filelib_test.php
lib/upgrade.txt
lib/weblib.php
mod/lti/OAuthBody.php
mod/lti/TrivialStore.php
mod/lti/ajax.php
mod/lti/backup/moodle1/lib.php
mod/lti/backup/moodle2/backup_lti_activity_task.class.php
mod/lti/backup/moodle2/backup_lti_stepslib.php
mod/lti/backup/moodle2/restore_lti_activity_task.class.php
mod/lti/backup/moodle2/restore_lti_stepslib.php
mod/lti/basiclti.js [deleted file]
mod/lti/classes/local/ltiservice/resource_base.php [new file with mode: 0644]
mod/lti/classes/local/ltiservice/response.php [new file with mode: 0644]
mod/lti/classes/local/ltiservice/service_base.php [new file with mode: 0644]
mod/lti/classes/plugininfo/ltiservice.php [new file with mode: 0644]
mod/lti/db/access.php
mod/lti/db/install.xml
mod/lti/db/log.php
mod/lti/db/subplugins.php
mod/lti/db/upgrade.php
mod/lti/db/upgradelib.php [new file with mode: 0644]
mod/lti/edit_form.php
mod/lti/grade.php
mod/lti/index.php
mod/lti/instructor_edit_tool_type.php
mod/lti/lang/en/lti.php
mod/lti/launch.php
mod/lti/lib.php
mod/lti/localadminlib.php [deleted file]
mod/lti/locallib.php
mod/lti/mod_form.js
mod/lti/mod_form.php
mod/lti/register.php [new file with mode: 0644]
mod/lti/register_form.php [new file with mode: 0644]
mod/lti/registersettings.php [new file with mode: 0644]
mod/lti/registration.php [new file with mode: 0644]
mod/lti/registrationreturn.php [new file with mode: 0644]
mod/lti/request_tool.php
mod/lti/return.php
mod/lti/service.php
mod/lti/service/profile/classes/local/resource/profile.php [new file with mode: 0644]
mod/lti/service/profile/classes/local/service/profile.php [new file with mode: 0644]
mod/lti/service/profile/lang/en/ltiservice_profile.php [new file with mode: 0644]
mod/lti/service/profile/version.php [new file with mode: 0644]
mod/lti/service/readme.txt [new file with mode: 0644]
mod/lti/service/toolproxy/classes/local/resource/toolproxy.php [new file with mode: 0644]
mod/lti/service/toolproxy/classes/local/service/toolproxy.php [new file with mode: 0644]
mod/lti/service/toolproxy/lang/en/ltiservice_toolproxy.php [new file with mode: 0644]
mod/lti/service/toolproxy/version.php [new file with mode: 0644]
mod/lti/service/toolsettings/classes/local/resource/contextsettings.php [new file with mode: 0644]
mod/lti/service/toolsettings/classes/local/resource/linksettings.php [new file with mode: 0644]
mod/lti/service/toolsettings/classes/local/resource/systemsettings.php [new file with mode: 0644]
mod/lti/service/toolsettings/classes/local/service/toolsettings.php [new file with mode: 0644]
mod/lti/service/toolsettings/lang/en/ltiservice_toolsettings.php [new file with mode: 0644]
mod/lti/service/toolsettings/version.php [new file with mode: 0644]
mod/lti/servicelib.php
mod/lti/services.php [new file with mode: 0644]
mod/lti/settings.php
mod/lti/tests/event/unknown_service_api_called_test.php
mod/lti/tests/generator_test.php
mod/lti/tests/locallib_test.php
mod/lti/tests/upgradelib_test.php [new file with mode: 0644]
mod/lti/toolproxies.php [new file with mode: 0644]
mod/lti/toolssettings.php [new file with mode: 0644]
mod/lti/typessettings.php
mod/lti/upgrade.txt
mod/lti/version.php
mod/lti/view.php
mod/wiki/pagelib.php
mod/wiki/renderer.php
mod/wiki/search.php
mod/wiki/tests/behat/wiki_search.feature
my/tests/behat/add_blocks.feature
question/type/multianswer/module.js
theme/base/style/core.css
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/style/moodle.css
user/tests/behat/table_sorting.feature
user/tests/profilelib_test.php
version.php

index 92aa9db..95af47d 100644 (file)
@@ -52,7 +52,7 @@ $help =
 
 By default InnoDB storage table is using legacy Antelope file format
 which has major restriction on database row size.
-Use this script to detect and fix database tables with potentail data
+Use this script to detect and fix database tables with potential data
 overflow problems.
 
 Options:
index c9758f2..2b32cb0 100644 (file)
@@ -185,6 +185,12 @@ Feature: Set up contextual data for tests
       | url        | Test url name          | Test url description          | C1     | url1        |
       | wiki       | Test wiki name         | Test wiki description         | C1     | wiki1       |
       | workshop   | Test workshop name     | Test workshop description     | C1     | workshop1   |
+    And the following "scales" exist:
+      | name | scale |
+      | Test Scale 1 | Disappointing, Good, Very good, Excellent |
+    And the following "activities" exist:
+      | activity   | name                            | intro                         | course | idnumber    | grade |
+      | assign     | Test assignment name with scale | Test assignment description   | C1     | assign1     | Test Scale 1 |
     When I log in as "admin"
     And I follow "Course 1"
     Then I should see "Test assignment name"
@@ -214,6 +220,10 @@ Feature: Set up contextual data for tests
     And I should see "Test workshop name"
     And I follow "Test assignment name"
     And I should see "Test assignment description"
+    And I follow "C1"
+    And I follow "Test assignment name with scale"
+    And I follow "Edit settings"
+    And the field "Type" matches value "Scale"
 
   @javascript
   Scenario: Add relations between users and groups
@@ -297,13 +307,124 @@ Feature: Set up contextual data for tests
       | Course 1 | C1 |
     And the following "grade categories" exist:
       | fullname | course |
-      | Grade category 1 | C1|
+      | Grade category 1 | C1 |
     And the following "grade categories" exist:
       | fullname | course | gradecategory |
-      | Grade sub category 2 | C1 | Grade category 1|
+      | Grade sub category 2 | C1 | Grade category 1 |
     When I log in as "admin"
     And I follow "Courses"
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     Then I should see "Grade category 1"
     And I should see "Grade sub category 2"
+
+  Scenario: Add a bunch of grade items
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1 |
+    And the following "grade categories" exist:
+      | fullname | course |
+      | Grade category 1 | C1 |
+    And the following "grade categories" exist:
+      | fullname | course | gradecategory |
+      | Grade sub category 2 | C1 | Grade category 1 |
+    And the following "grade items" exist:
+      | itemname    | course |
+      | Test Grade Item 1 | C1 |
+    And the following "grade items" exist:
+      | itemname    | course | gradecategory |
+      | Test Grade Item 2 | C1 | Grade category 1 |
+      | Test Grade Item 3 | C1 | Grade sub category 2 |
+    When I log in as "admin"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I expand "Setup" node
+    And I follow "Categories and items"
+    Then I should see "Test Grade Item 1"
+    And I follow "Edit   Test Grade Item 1"
+    And I expand all fieldsets
+    And I should see "Course 1"
+    And I press "Cancel"
+    And I should see "Grade category 1"
+    And I should see "Test Grade Item 2"
+    And I follow "Edit   Test Grade Item 2"
+    And I expand all fieldsets
+    And I should see "Grade category 1"
+    And I press "Cancel"
+    And I should see "Grade sub category 2"
+    And I should see "Test Grade Item 3"
+    And I follow "Edit   Test Grade Item 3"
+    And I expand all fieldsets
+    And I should see "Grade sub category 2"
+    And I press "Cancel"
+
+  Scenario: Add a bunch of scales
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1 |
+    And the following "scales" exist:
+      | name | scale |
+      | Test Scale 1 | Disappointing, Good, Very good, Excellent |
+    When I log in as "admin"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I follow "Scales"
+    Then I should see "Test Scale 1"
+    And I should see "Disappointing,  Good,  Very good,  Excellent"
+
+  Scenario: Add a bunch of outcomes
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "scales" exist:
+      | name | scale |
+      | Test Scale 1 | Disappointing, Good, Very good, Excellent |
+    And the following "grade outcomes" exist:
+      | fullname        | shortname | scale        |
+      | Grade outcome 1 | OT1       | Test Scale 1 |
+    And the following "grade outcomes" exist:
+      | fullname        | shortname | course | scale        |
+      | Grade outcome 2 | OT2       | C1     | Test Scale 1 |
+    When I log in as "admin"
+    And I set the following administration settings values:
+      | Enable outcomes | 1 |
+    And I follow "Home"
+    And I follow "Course 1"
+    And I follow "Outcomes"
+    Then I should see "Grade outcome 1" in the "#addoutcomes" "css_element"
+    And I should see "Grade outcome 2" in the "#removeoutcomes" "css_element"
+    And I follow "Edit outcomes"
+    And the following should exist in the "generaltable" table:
+      | Full name       | Short name | Scale        |
+      | Grade outcome 2 | OT2        | Test Scale 1 |
+
+  Scenario: Add a bunch of outcome grade items
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "scales" exist:
+      | name         | scale                                     |
+      | Test Scale 1 | Disappointing, Good, Very good, Excellent |
+    And the following "grade outcomes" exist:
+      | fullname        | shortname | course | scale        |
+      | Grade outcome 1 | OT1       | C1     | Test Scale 1 |
+    And the following "grade categories" exist:
+      | fullname         | course |
+      | Grade category 1 | C1     |
+     And the following "grade items" exist:
+       | itemname                  | course | outcome | gradecategory    |
+       | Test Outcome Grade Item 1 | C1     | OT1     | Grade category 1 |
+    When I log in as "admin"
+    And I set the following administration settings values:
+      | Enable outcomes | 1 |
+    And I follow "Home"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I expand "Setup" node
+    And I follow "Categories and items"
+    Then I should see "Test Outcome Grade Item 1"
+    And I follow "Edit   Test Outcome Grade Item 1"
+    And the field "Outcome" matches value "Grade outcome 1"
+    And I expand all fieldsets
+    And "//div[contains(@class, 'fitem')]/div[contains(@class, 'fitemtitle')]/div[contains(@class, fstaticlabel) and contains(., 'Grade category')]/../../div[contains(@class, 'felement') and contains(., 'Grade category 1')]" "xpath_element" should exist
+    And I press "Cancel"
diff --git a/admin/tool/monitor/classes/eventlist.php b/admin/tool/monitor/classes/eventlist.php
new file mode 100644 (file)
index 0000000..989bc3c
--- /dev/null
@@ -0,0 +1,221 @@
+<?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/>.
+
+/**
+ * Event documentation
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for returning event information.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class eventlist {
+    /**
+     * Return all of the core event files.
+     *
+     * @return array Core events.
+     */
+    protected static function get_core_eventlist() {
+        global $CFG;
+
+        // Disable developer debugging as deprecated events will fire warnings.
+        // Setup backup variables to restore the following settings back to what they were when we are finished.
+        $debuglevel          = $CFG->debug;
+        $debugdisplay        = $CFG->debugdisplay;
+        $debugdeveloper      = $CFG->debugdeveloper;
+        $CFG->debug          = 0;
+        $CFG->debugdisplay   = false;
+        $CFG->debugdeveloper = false;
+
+        $eventinformation = array();
+        $directory = $CFG->libdir . '/classes/event';
+        $files = self::get_file_list($directory);
+
+        // Remove exceptional events that will cause problems being displayed.
+        if (isset($files['unknown_logged'])) {
+            unset($files['unknown_logged']);
+        }
+        foreach ($files as $file => $location) {
+            $classname = '\\core\\event\\' . $file;
+            // Check to see if this is actually a valid event.
+            if (method_exists($classname, 'get_static_info')) {
+                $ref = new \ReflectionClass($classname);
+                // Ignore abstracts.
+                if (!$ref->isAbstract() && $file != 'manager') {
+                    $eventinformation[$classname] = $classname::get_name();
+                }
+            }
+        }
+        // Now enable developer debugging as event information has been retrieved.
+        $CFG->debug          = $debuglevel;
+        $CFG->debugdisplay   = $debugdisplay;
+        $CFG->debugdeveloper = $debugdeveloper;
+        return $eventinformation;
+    }
+
+    /**
+     * This function returns an array of all events for the plugins of the system.
+     *
+     * @param bool $withoutcomponent Return an eventlist without associated components.
+     *
+     * @return array A list of events from all plug-ins.
+     */
+    protected static function get_non_core_eventlist($withoutcomponent = false) {
+        global $CFG;
+        // Disable developer debugging as deprecated events will fire warnings.
+        // Setup backup variables to restore the following settings back to what they were when we are finished.
+        $debuglevel          = $CFG->debug;
+        $debugdisplay        = $CFG->debugdisplay;
+        $debugdeveloper      = $CFG->debugdeveloper;
+        $CFG->debug          = 0;
+        $CFG->debugdisplay   = false;
+        $CFG->debugdeveloper = false;
+
+        $noncorepluginlist = array();
+        $plugintypes = \core_component::get_plugin_types();
+        foreach ($plugintypes as $plugintype => $notused) {
+            $pluginlist = \core_component::get_plugin_list($plugintype);
+            foreach ($pluginlist as $plugin => $directory) {
+                $plugindirectory = $directory . '/classes/event';
+                foreach (self::get_file_list($plugindirectory) as $eventname => $notused) {
+                    $plugineventname = '\\' . $plugintype . '_' . $plugin . '\\event\\' . $eventname;
+                    // Check that this is actually an event.
+                    if (method_exists($plugineventname, 'get_static_info')  && $plugin != 'monitor') { // No selfie here.
+                        $ref = new \ReflectionClass($plugineventname);
+                        if (!$ref->isAbstract() && $plugin != 'legacy') {
+                            if ($withoutcomponent) {
+                                $noncorepluginlist[$plugineventname] = $plugineventname::get_name();
+                            } else {
+                                $noncorepluginlist[$plugintype . '_' . $plugin][$plugineventname] = $plugineventname::get_name();
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // Now enable developer debugging as event information has been retrieved.
+        $CFG->debug          = $debuglevel;
+        $CFG->debugdisplay   = $debugdisplay;
+        $CFG->debugdeveloper = $debugdeveloper;
+
+        return $noncorepluginlist;
+    }
+
+    /**
+     * Returns a list of files with a full directory path in a specified directory.
+     *
+     * @param string $directory location of files.
+     * @return array full location of files from the specified directory.
+     */
+    protected static function get_file_list($directory) {
+        global $CFG;
+        $directoryroot = $CFG->dirroot;
+        $finalfiles = array();
+        if (is_dir($directory)) {
+            if ($handle = opendir($directory)) {
+                $files = scandir($directory);
+                foreach ($files as $file) {
+                    if ($file != '.' && $file != '..') {
+                        // Ignore the file if it is external to the system.
+                        if (strrpos($directory, $directoryroot) !== false) {
+                            $location = substr($directory, strlen($directoryroot));
+                            $name = substr($file, 0, -4);
+                            $finalfiles[$name] = $location  . '/' . $file;
+                        }
+                    }
+                }
+            }
+        }
+        return $finalfiles;
+    }
+
+    /**
+     * Get a list of events present in the system.
+     *
+     * @param bool $withoutcomponent Return an eventlist without associated components.
+     *
+     * @return array list of events present in the system.
+     */
+    public static function get_all_eventlist($withoutcomponent = false) {
+        if ($withoutcomponent) {
+            $return = array_merge(self::get_core_eventlist(), self::get_non_core_eventlist($withoutcomponent));
+            array_multisort($return, SORT_NATURAL);
+        } else {
+            $return = array_merge(array('core' => self::get_core_eventlist()),
+                    self::get_non_core_eventlist($withoutcomponent = false));
+        }
+        return $return;
+    }
+
+    /**
+     * Return list of plugins that have events.
+     *
+     * @param array $eventlist a list of events present in the system {@link eventlist::get_all_eventlist}.
+     *
+     * @return array list of plugins with human readable name.
+     */
+    public static function get_plugin_list($eventlist = array()) {
+        if (empty($eventlist)) {
+            $eventlist = self::get_all_eventlist();
+        }
+        $plugins = array_keys($eventlist);
+        $return = array();
+        foreach ($plugins as $plugin) {
+            if ($plugin === 'core') {
+                $return[$plugin] = get_string('core', 'tool_monitor');
+            } else if (get_string_manager()->string_exists('pluginname', $plugin)) {
+                $return[$plugin] = get_string('pluginname', $plugin);
+            } else {
+                $return[$plugin] = $plugin;
+            }
+        }
+
+        return $return;
+    }
+
+    /**
+     * validate if the given event belongs to the given plugin.
+     *
+     * @param string $plugin Frankenstyle name of the plugin.
+     * @param string $eventname Full qualified event name.
+     * @param array $eventlist List of events generated by {@link eventlist::get_all_eventlist}
+     *
+     * @return bool Returns true if the selected event belongs to the selected plugin, false otherwise.
+     */
+    public static function validate_event_plugin($plugin, $eventname, $eventlist = array()) {
+        if (empty($eventlist)) {
+            $eventlist = self::get_all_eventlist();
+        }
+        if (isset($eventlist[$plugin][$eventname])) {
+            return true;
+        }
+
+        return false;
+    }
+}
diff --git a/admin/tool/monitor/classes/eventobservers.php b/admin/tool/monitor/classes/eventobservers.php
new file mode 100644 (file)
index 0000000..27d3d29
--- /dev/null
@@ -0,0 +1,210 @@
+<?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/>.
+
+/**
+ * Observer class containing methods monitoring various events.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Observer class containing methods monitoring various events.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class eventobservers {
+
+    /** @var array $buffer buffer of events. */
+    protected $buffer = array();
+
+    /** @var int Number of entries in the buffer. */
+    protected $count = 0;
+
+    /** @var  eventobservers a reference to a self instance. */
+    protected static $instance;
+
+    /**
+     * Course delete event observer.
+     * This observer monitors course delete event, and when a course is deleted it deletes any rules and subscriptions associated
+     * with it, so no orphan data is left behind.
+     *
+     * @param \core\event\course_deleted $event The course deleted event.
+     */
+    public static function course_deleted(\core\event\course_deleted $event) {
+        $rules = rule_manager::get_rules_by_courseid($event->courseid);
+        foreach ($rules as $rule) {
+            rule_manager::delete_rule($rule->id);
+        }
+    }
+
+    /**
+     * The observer monitoring all the events.
+     *
+     * This observers puts small event objects in buffer for later writing to the database. At the end of the request the buffer
+     * is cleaned up and all data dumped into the tool_monitor_events table.
+     *
+     * @param \core\event\base $event event object
+     */
+    public static function process_event(\core\event\base $event) {
+
+        if (empty(self::$instance)) {
+            self::$instance = new static();
+            // Register shutdown handler - this is useful for buffering, processing events, etc.
+            \core_shutdown_manager::register_function(array(self::$instance, 'process_buffer'));
+        }
+
+        self::$instance->buffer_event($event);
+
+        if (PHPUNIT_TEST) {
+            // Process buffer after every event when unit testing.
+            self::$instance->process_buffer();
+
+        }
+    }
+
+    /**
+     * Api to buffer events to store, to reduce db queries.
+     *
+     * @param \core\event\base $event
+     */
+    protected function buffer_event(\core\event\base $event) {
+
+        $eventdata = $event->get_data();
+        $eventobj = new \stdClass();
+        $eventobj->eventname = $eventdata['eventname'];
+        $eventobj->contextid = $eventdata['contextid'];
+        $eventobj->contextlevel = $eventdata['contextlevel'];
+        $eventobj->contextinstanceid = $eventdata['contextinstanceid'];
+        if ($event->get_url()) {
+            // Get link url if exists.
+            $eventobj->link = $event->get_url()->out();
+        } else {
+            $eventobj->link = '';
+        }
+        $eventobj->courseid = $eventdata['courseid'];
+        $eventobj->timecreated = $eventdata['timecreated'];
+
+        $this->buffer[] = $eventobj;
+        $this->count++;
+    }
+
+    /**
+     * This method process all events stored in the buffer.
+     *
+     * This is a multi purpose api. It does the following:-
+     * 1. Write event data to tool_monitor_events
+     * 2. Find out users that need to be notified about rule completion and schedule a task to send them messages.
+     */
+    public function process_buffer() {
+        global $DB;
+
+        $events = $this->flush(); // Flush data.
+
+        $select = "SELECT COUNT(id) FROM {tool_monitor_events} ";
+        $now = time();
+        $messagestosend = array();
+
+        // Let us now process the events and check for subscriptions.
+        foreach ($events as $eventobj) {
+            $subscriptions = subscription_manager::get_subscriptions_by_event($eventobj);
+            $idstosend = array();
+            foreach ($subscriptions as $subscription) {
+                $starttime = $now - $subscription->timewindow;
+                if ($subscription->courseid == 0) {
+                    // Site level subscription. Count all events.
+                    $where = "eventname = :eventname AND timecreated >  :starttime";
+                    $params = array('eventname' => $eventobj->eventname, 'starttime' => $starttime);
+                } else {
+                    // Course level subscription.
+                    if ($subscription->cmid == 0) {
+                        // All modules.
+                        $where = "eventname = :eventname AND courseid = :courseid AND timecreated > :starttime";
+                        $params = array('eventname' => $eventobj->eventname, 'courseid' => $eventobj->courseid,
+                                'starttime' => $starttime);
+                    } else {
+                        // Specific module.
+                        $where = "eventname = :eventname AND courseid = :courseid AND contextinstanceid = :cmid
+                                AND timecreated > :starttime";
+                        $params = array('eventname' => $eventobj->eventname, 'courseid' => $eventobj->courseid,
+                                'cmid' => $eventobj->contextinstanceid, 'starttime' => $starttime);
+
+                    }
+                }
+                $sql = $select . "WHERE " . $where;
+                $count = $DB->count_records_sql($sql, $params);
+                if (!empty($count) && $count >= $subscription->frequency) {
+                    $idstosend[] = $subscription->id;
+                }
+            }
+            if (!empty($idstosend)) {
+                $messagestosend[] = array('subscriptionids' => $idstosend, 'event' => $eventobj);
+            }
+        }
+
+        // Schedule a task to send notification.
+        if (!empty($messagestosend)) {
+            $adhocktask = new notification_task();
+            $adhocktask->set_custom_data($messagestosend);
+            $adhocktask->set_component('tool_monitor');
+            \core\task\manager::queue_adhoc_task($adhocktask);
+        }
+    }
+
+    /**
+     * Protected method that flushes the buffer of events and writes them to the database.
+     *
+     * @return array a copy of the events buffer.
+     */
+    protected function flush() {
+        global $DB;
+
+        // Flush the buffer to the db.
+        $events = $this->buffer;
+        $DB->insert_records('tool_monitor_events', $events); // Insert the whole chunk into the database.
+        $this->buffer = array();
+        $this->count = 0;
+        return $events;
+    }
+
+    /**
+     * Observer that monitors user deleted event and delete user subscriptions.
+     *
+     * @param \core\event\user_deleted $event the event object.
+     */
+    public static function user_deleted(\core\event\user_deleted $event) {
+        $userid = $event->objectid;
+        subscription_manager::delete_user_subscriptions($userid);
+    }
+
+    /**
+     * Observer that monitors course module deleted event and delete user subscriptions.
+     *
+     * @param \core\event\course_module_deleted $event the event object.
+     */
+    public static function course_module_deleted(\core\event\course_module_deleted $event) {
+        $cmid = $event->contextinstanceid;
+        subscription_manager::delete_cm_subscriptions($cmid);
+    }
+}
diff --git a/admin/tool/monitor/classes/notification_task.php b/admin/tool/monitor/classes/notification_task.php
new file mode 100644 (file)
index 0000000..ffb1f4a
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file defines an adhoc task to send notifications.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Adhock class, used to send notifications to users.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class notification_task extends \core\task\adhoc_task {
+
+    /**
+     * Send out messages.
+     */
+    public function execute() {
+        foreach ($this->get_custom_data() as $data) {
+            $eventobj = $data->event;
+            $subscriptionids = $data->subscriptionids;
+            foreach ($subscriptionids as $id) {
+                if ($message = $this->generate_message($id, $eventobj)) {
+                    message_send($message);
+                }
+            }
+        }
+    }
+
+    /**
+     * Generates the message object for a give subscription and event.
+     *
+     * @param int $subscriptionid Subscription instance
+     * @param \stdClass $eventobj Event data
+     *
+     * @return false|\stdClass message object
+     */
+    protected function generate_message($subscriptionid, \stdClass $eventobj) {
+
+        try {
+            $subscription = subscription_manager::get_subscription($subscriptionid);
+        } catch (\dml_exception $e) {
+            // Race condition, someone deleted the subscription.
+            return false;
+        }
+        $user = \core_user::get_user($subscription->userid);
+        $context = \context_user::instance($user->id, IGNORE_MISSING);
+        if ($context === false) {
+            // User context doesn't exist. Should never happen, nothing to do return.
+            return false;
+        }
+
+        $template = $subscription->template;
+        $template = $this->replace_placeholders($template, $subscription, $eventobj, $context);
+        $msgdata = new \stdClass();
+        $msgdata->component         = 'tool_monitor'; // Your component name.
+        $msgdata->name              = 'notification'; // This is the message name from messages.php.
+        $msgdata->userfrom          = \core_user::get_noreply_user();
+        $msgdata->userto            = $user;
+        $msgdata->subject           = $subscription->get_name($context);
+        $msgdata->fullmessage       = format_text($template, $subscription->templateformat, array('context' => $context));
+        $msgdata->fullmessageformat = $subscription->templateformat;
+        $msgdata->fullmessagehtml   = format_text($template, $subscription->templateformat, array('context' => $context));
+        $msgdata->smallmessage      = '';
+        $msgdata->notification      = 1; // This is only set to 0 for personal messages between users.
+
+        return $msgdata;
+    }
+
+    /**
+     * Replace place holders in the template with respective content.
+     *
+     * @param string $template Message template.
+     * @param subscription $subscription subscription instance
+     * @param \stdclass $eventobj Event data
+     * @param \context $context context object
+     *
+     * @return mixed final template string.
+     */
+    protected function replace_placeholders($template, subscription $subscription, $eventobj, $context) {
+        $template = str_replace('{link}', $eventobj->link, $template);
+        if ($eventobj->contextlevel == CONTEXT_MODULE && !empty($eventobj->contextinstanceid)
+            && (strpos($template, '{modulelink}') !== false)) {
+            $cm = get_fast_modinfo($eventobj->courseid)->get_cm($eventobj->contextinstanceid);
+            $modulelink = $cm->url;
+            $template = str_replace('{modulelink}', $modulelink, $template);
+        }
+        $template = str_replace('{rulename}', $subscription->get_name($context), $template);
+        $template = str_replace('{description}', $subscription->get_description($context), $template);
+        $template = str_replace('{eventname}', $subscription->get_event_name(), $template);
+
+        return $template;
+    }
+}
diff --git a/admin/tool/monitor/classes/output/helpicon/renderable.php b/admin/tool/monitor/classes/output/helpicon/renderable.php
new file mode 100644 (file)
index 0000000..3cb0395
--- /dev/null
@@ -0,0 +1,93 @@
+<?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 file for the renderable class for the tool_monitor help icon.
+ *
+ * @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\output\helpicon;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Renderable class for the tool_monitor help icon.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderable implements \renderable {
+
+    /**
+     * @var string $type the type we are displaying the help icon for (either rule or subscription).
+     */
+    public $type;
+
+    /**
+     * @var int $id the id of the type.
+     */
+    public $id;
+
+    /**
+     * The constructor.
+     *
+     * @param string $type the type we are displaying the help icon for (either rule or subscription).
+     * @param int $id the id of the type.
+     */
+    public function __construct($type, $id) {
+        $this->type = $type;
+        $this->id = $id;
+    }
+
+    /**
+     * Returns the string to display for the help icon.
+     *
+     * @param string $type the type we are displaying the help icon for (either rule or subscription).
+     * @param int $id the id of the type.
+     * @param boolean $ajax Whether this help is called from an AJAX script.
+     *      This is used to influence text formatting and determines which format to output the doclink in.
+     * @return string|object|array $a An object, string or number that can be used within translation strings
+     */
+    public static function get_help_string_parameters($type, $id, $ajax = false) {
+        if ($type == 'rule') {
+            $rule = \tool_monitor\rule_manager::get_rule($id);
+
+            $langstring = new \stdClass();
+            $langstring->eventname = $rule->get_event_name();
+            $langstring->eventcomponent = $rule->get_plugin_name();
+            $langstring->frequency = $rule->frequency;
+            $langstring->minutes = $rule->timewindow / MINSECS;
+
+            return get_formatted_help_string('rulehelp', 'tool_monitor', $ajax, $langstring);
+        }
+
+        // Must be a subscription.
+        $sub = \tool_monitor\subscription_manager::get_subscription($id);
+
+        $langstring = new \stdClass();
+        $langstring->eventname = $sub->get_event_name();
+        $langstring->moduleinstance = $sub->get_instance_name();
+        $langstring->frequency = $sub->frequency;
+        $langstring->minutes = $sub->timewindow / MINSECS;
+
+        return get_formatted_help_string('subhelp', 'tool_monitor', $ajax, $langstring);
+    }
+}
diff --git a/admin/tool/monitor/classes/output/helpicon/renderer.php b/admin/tool/monitor/classes/output/helpicon/renderer.php
new file mode 100644 (file)
index 0000000..99025f2
--- /dev/null
@@ -0,0 +1,79 @@
+<?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 file for the renderer class for the tool_monitor help icon.
+ *
+ * @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\output\helpicon;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Renderer class for tool_monitor help icons.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+    /**
+     * Get the HTML for the help icon.
+     *
+     * @param renderable $renderable renderable widget
+     *
+     * @return string the HTML of the help icon to display.
+     */
+    protected function render_renderable(renderable $renderable) {
+        global $CFG;
+
+        // First get the help image icon.
+        $src = $this->pix_url('help');
+
+        if ($renderable->type == 'rule') {
+            $title = get_string('rulehelp', 'tool_monitor');
+        } else { // Must be a subscription.
+            $title = get_string('subhelp', 'tool_monitor');
+        }
+
+        $alt = get_string('helpwiththis');
+
+        $attributes = array('src' => $src, 'alt' => $alt, 'class' => 'iconhelp');
+        $output = \html_writer::empty_tag('img', $attributes);
+
+        // Now create the link around it - we need https on loginhttps pages.
+        $urlparams = array();
+        $urlparams['type'] = $renderable->type;
+        $urlparams['id'] = $renderable->id;
+        $urlparams['lang'] = current_language();
+        $url = new \moodle_url($CFG->httpswwwroot . '/admin/tool/monitor/help.php', $urlparams);
+
+        // Note: this title is displayed only if JS is disabled, otherwise the link will have the new ajax tooltip.
+        $title = get_string('helpprefix2', '', trim($title, ". \t"));
+
+        $attributes = array('href' => $url, 'title' => $title, 'aria-haspopup' => 'true', 'target' => '_blank');
+        $output = \html_writer::tag('a', $output, $attributes);
+
+        // Now, finally the span.
+        return \html_writer::tag('span', $output, array('class' => 'helptooltip'));
+    }
+}
diff --git a/admin/tool/monitor/classes/output/managerules/renderable.php b/admin/tool/monitor/classes/output/managerules/renderable.php
new file mode 100644 (file)
index 0000000..2c74245
--- /dev/null
@@ -0,0 +1,201 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderable class for manage rules page.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\output\managerules;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/tablelib.php');
+
+/**
+ * Renderable class for manage rules page.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderable extends \table_sql implements \renderable {
+
+    /**
+     * @var int course id.
+     */
+    public $courseid;
+
+    /**
+     * @var \context_course|\context_system context of the page to be rendered.
+     */
+    protected $context;
+
+    /**
+     * @var bool Does the user have capability to manage rules at site context.
+     */
+    protected $hassystemcap;
+
+    /**
+     * Sets up the table_log parameters.
+     *
+     * @param string $uniqueid unique id of form.
+     * @param \moodle_url $url url where this table is displayed.
+     * @param int $courseid course id.
+     * @param int $perpage Number of rules to display per page.
+     */
+    public function __construct($uniqueid, \moodle_url $url, $courseid = 0, $perpage = 100) {
+        parent::__construct($uniqueid);
+
+        $this->set_attribute('class', 'toolmonitor managerules generaltable generalbox');
+        $this->define_columns(array('name', 'description', 'plugin', 'eventname', 'filters', 'manage'));
+        $this->define_headers(array(
+                get_string('name'),
+                get_string('description'),
+                get_string('plugin'),
+                get_string('eventname'),
+                get_string('frequency', 'tool_monitor'),
+                get_string('manage', 'tool_monitor'),
+            )
+        );
+        $this->courseid = $courseid;
+        $this->pagesize = $perpage;
+        $systemcontext = \context_system::instance();
+        $this->context = empty($courseid) ? $systemcontext : \context_course::instance($courseid);
+        $this->hassystemcap = has_capability('tool/monitor:managerules', $systemcontext);
+        $this->collapsible(false);
+        $this->sortable(false);
+        $this->pageable(true);
+        $this->is_downloadable(false);
+        $this->define_baseurl($url);
+    }
+
+    /**
+     * Generate content for name column.
+     *
+     * @param \tool_monitor\rule $rule rule object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_name(\tool_monitor\rule $rule) {
+        return $rule->get_name($this->context);
+    }
+
+    /**
+     * Generate content for description column.
+     *
+     * @param \tool_monitor\rule $rule rule object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_description(\tool_monitor\rule $rule) {
+        return $rule->get_description($this->context);
+    }
+
+    /**
+     * Generate content for plugin column.
+     *
+     * @param \tool_monitor\rule $rule rule object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_plugin(\tool_monitor\rule $rule) {
+        return $rule->get_plugin_name();
+    }
+
+    /**
+     * Generate content for eventname column.
+     *
+     * @param \tool_monitor\rule $rule rule object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_eventname(\tool_monitor\rule $rule) {
+        return $rule->get_event_name();
+    }
+
+    /**
+     * Generate content for filters column.
+     *
+     * @param \tool_monitor\rule $rule rule object
+     *
+     * @return string html used to display the filters column field.
+     */
+    public function col_filters(\tool_monitor\rule $rule) {
+        return $rule->get_filters_description();
+    }
+
+    /**
+     * Generate content for manage column.
+     *
+     * @param \tool_monitor\rule $rule rule object
+     *
+     * @return string html used to display the manage column field.
+     */
+    public function col_manage(\tool_monitor\rule $rule) {
+        global $OUTPUT, $CFG;
+        $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)) {
+            // 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()));
+            $copyurl = new \moodle_url($CFG->wwwroot. '/admin/tool/monitor/managerules.php',
+                    array('ruleid' => $rule->id, 'action' => 'copy', 'courseid' => $this->courseid, 'sesskey' => sesskey()));
+            $deleteurl = new \moodle_url($CFG->wwwroot. '/admin/tool/monitor/managerules.php', array('ruleid' => $rule->id,
+                    'action' => 'delete', 'courseid' => $rule->courseid, 'sesskey' => sesskey()));
+
+            $icon = $OUTPUT->render(new \pix_icon('t/edit', get_string('editrule', 'tool_monitor')));
+            $manage .= \html_writer::link($editurl, $icon, array('class' => 'action-icon'));
+
+            $icon = $OUTPUT->render(new \pix_icon('t/copy', get_string('duplicaterule', 'tool_monitor')));
+            $manage .= \html_writer::link($copyurl, $icon, array('class' => 'action-icon'));
+
+            $a = $rule->get_name($this->context);
+            $action = new \component_action('click', 'M.util.show_confirm_dialog', array('message' => get_string('ruleareyousure',
+                    'tool_monitor', $a)));
+            $icon = $OUTPUT->action_link($deleteurl, new \pix_icon('t/delete', get_string('deleterule', 'tool_monitor')), $action);
+            $manage .= $icon;
+        } else {
+            $manage = '-';
+        }
+        return $manage;
+    }
+
+    /**
+     * Query the reader. Store results in the object for use by build_table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar.
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+
+        $total = \tool_monitor\rule_manager::count_rules_by_courseid($this->courseid);
+        $this->pagesize($pagesize, $total);
+        $rules = \tool_monitor\rule_manager::get_rules_by_courseid($this->courseid, $this->get_page_start(),
+                $this->get_page_size());
+        $this->rawdata = $rules;
+        // Set initial bars.
+        if ($useinitialsbar) {
+            $this->initialbars($total > $pagesize);
+        }
+    }
+}
diff --git a/admin/tool/monitor/classes/output/managerules/renderer.php b/admin/tool/monitor/classes/output/managerules/renderer.php
new file mode 100644 (file)
index 0000000..889817d
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderer class for manage rules page.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\output\managerules;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Renderer class for manage rules page.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+    /**
+     * Get html to display on the page.
+     *
+     * @param renderable $renderable renderable widget
+     *
+     * @return string to display on the mangerules page.
+     */
+    protected function render_renderable(renderable $renderable) {
+        $o = $this->render_table($renderable);
+        $o .= $this->render_add_button($renderable->courseid);
+
+        return $o;
+    }
+
+    /**
+     * Get html to display on the page.
+     *
+     * @param renderable $renderable renderable widget
+     *
+     * @return string to display on the mangerules page.
+     */
+    protected function render_table(renderable $renderable) {
+        $o = '';
+        ob_start();
+        $renderable->out($renderable->pagesize, true);
+        $o = ob_get_contents();
+        ob_end_clean();
+
+        return $o;
+    }
+
+    /**
+     * Html to add a button for adding a new rul.
+     *
+     * @param int $courseid course id.
+     *
+     * @return string html for the button.
+     */
+    protected function render_add_button($courseid) {
+        global $CFG;
+
+        $button = \html_writer::tag('button', get_string('addrule', 'tool_monitor'));
+        $addurl = new \moodle_url($CFG->wwwroot. '/admin/tool/monitor/edit.php', array('courseid' => $courseid));
+        return \html_writer::link($addurl, $button);
+    }
+}
diff --git a/admin/tool/monitor/classes/output/managesubs/renderer.php b/admin/tool/monitor/classes/output/managesubs/renderer.php
new file mode 100644 (file)
index 0000000..fe3f8e0
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderer class for manage subscriptions page.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\output\managesubs;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Renderer class for manage subscriptions page.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+    /**
+     * Get html to display on the page.
+     *
+     * @param subs $renderable renderable widget
+     *
+     * @return string to display on the mangesubs page.
+     */
+    protected function render_subs(subs $renderable) {
+        $o = $this->render_table($renderable);
+        return $o;
+    }
+
+    /**
+     * Get html to display on the page.
+     *
+     * @param rules $renderable renderable widget
+     *
+     * @return string to display on the mangesubs page.
+     */
+    protected function render_rules(rules $renderable) {
+        $o = $this->render_course_select($renderable);
+        if (!empty($renderable->totalcount)) {
+            $o .= $this->render_table($renderable);
+        }
+        return $o;
+    }
+
+    /**
+     * Get html to display on the page for select dropdown..
+     *
+     * @param rules $renderable renderable widget
+     *
+     * @return string to display on the mangesubs page.
+     */
+    protected function render_course_select(rules $renderable) {
+        $select = $renderable->get_user_courses_select();
+        return $this->render($select);;
+    }
+
+    /**
+     * Get html to display on the page.
+     *
+     * @param rules|subs $renderable renderable widget
+     *
+     * @return string to display on the mangesubs page.
+     */
+    protected function render_table($renderable) {
+        $o = '';
+        ob_start();
+        $renderable->out($renderable->pagesize, true);
+        $o = ob_get_contents();
+        ob_end_clean();
+
+        return $o;
+    }
+}
diff --git a/admin/tool/monitor/classes/output/managesubs/rules.php b/admin/tool/monitor/classes/output/managesubs/rules.php
new file mode 100644 (file)
index 0000000..f891e90
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderable class to display a set of rules in the manage subscriptions page.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\output\managesubs;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/tablelib.php');
+
+/**
+ * Renderable class to display a set of rules in the manage subscriptions page.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class rules extends \table_sql implements \renderable {
+
+    /**
+     * @var int course id.
+     */
+    public $courseid;
+
+    /**
+     * @var int total rules present.
+     */
+    public $totalcount = 0;
+
+    /**
+     * @var \context_course|\context_system context of the page to be rendered.
+     */
+    protected $context;
+
+    /**
+     * @var \tool_monitor\output\helpicon\renderer the help icon renderer.
+     */
+    protected $helpiconrenderer;
+
+    /**
+     * Sets up the table_log parameters.
+     *
+     * @param string $uniqueid unique id of form.
+     * @param \moodle_url $url url where this table is displayed.
+     * @param int $courseid course id.
+     * @param int $perpage Number of rules to display per page.
+     */
+    public function __construct($uniqueid, \moodle_url $url, $courseid = 0, $perpage = 100) {
+        global $PAGE;
+
+        parent::__construct($uniqueid);
+
+        $this->set_attribute('class', 'toolmonitor subscriberules generaltable generalbox');
+        $this->define_columns(array('name', 'description', 'select'));
+        $this->define_headers(array(
+                get_string('name'),
+                get_string('description'),
+                get_string('select')
+            )
+        );
+        $this->courseid = $courseid;
+        $this->pagesize = $perpage;
+        $systemcontext = \context_system::instance();
+        $this->context = empty($courseid) ? $systemcontext : \context_course::instance($courseid);
+        $this->collapsible(false);
+        $this->sortable(false);
+        $this->pageable(true);
+        $this->is_downloadable(false);
+        $this->define_baseurl($url);
+        $this->helpiconrenderer = $PAGE->get_renderer('tool_monitor', 'helpicon');
+        $total = \tool_monitor\rule_manager::count_rules_by_courseid($this->courseid);
+        $this->totalcount = $total;
+    }
+
+    /**
+     * Generate content for name column.
+     *
+     * @param \tool_monitor\rule $rule rule object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_name(\tool_monitor\rule $rule) {
+        $name = $rule->get_name($this->context);
+        $helpicon = new \tool_monitor\output\helpicon\renderable('rule', $rule->id);
+        $helpicon = $this->helpiconrenderer->render($helpicon);
+
+        return $name . $helpicon;
+    }
+
+    /**
+     * Generate content for description column.
+     *
+     * @param \tool_monitor\rule $rule rule object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_description(\tool_monitor\rule $rule) {
+        return $rule->get_description($this->context);
+    }
+
+    /**
+     * Generate content for plugin column.
+     *
+     * @param \tool_monitor\rule $rule rule object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_select(\tool_monitor\rule $rule) {
+        global $OUTPUT;
+        $select = $rule->get_module_select($this->courseid);
+        return is_object($select) ? $OUTPUT->render($select) : $select;
+    }
+
+    /**
+     * Query the reader. Store results in the object for use by build_table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar.
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+
+        $total = \tool_monitor\rule_manager::count_rules_by_courseid($this->courseid);
+        $this->pagesize($pagesize, $total);
+        $rules = \tool_monitor\rule_manager::get_rules_by_courseid($this->courseid, $this->get_page_start(),
+                $this->get_page_size());
+        $this->rawdata = $rules;
+        // Set initial bars.
+        if ($useinitialsbar) {
+            $this->initialbars($total > $pagesize);
+        }
+    }
+
+    /**
+     * Gets a list of courses where the current user can subscribe to rules as a dropdown.
+     *
+     * @return \single_select list of courses.
+     */
+    public function get_user_courses_select() {
+        $courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname');
+        $options = array(0 => get_string('site'));
+        $systemcontext = \context_system::instance();
+        foreach ($courses as $course) {
+            $options[$course->id] = format_text($course->fullname, array('context' => $systemcontext));
+        }
+        $url = new \moodle_url('/admin/tool/monitor/index.php');
+        $select = new \single_select($url, 'courseid', $options, $this->courseid);
+        $select->set_label(get_string('selectacourse', 'tool_monitor'));
+        return $select;
+    }
+}
diff --git a/admin/tool/monitor/classes/output/managesubs/subs.php b/admin/tool/monitor/classes/output/managesubs/subs.php
new file mode 100644 (file)
index 0000000..8ba616d
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderable class to display a set of subscriptions in the manage subscriptions page.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor\output\managesubs;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/tablelib.php');
+
+/**
+ * Renderable class to display a set of subscriptions in the manage subscriptions page.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class subs extends \table_sql implements \renderable {
+
+    /**
+     * @var int course id.
+     */
+    public $courseid;
+
+    /**
+     * @var \context_course|\context_system context of the page to be rendered.
+     */
+    protected $context;
+
+    /**
+     * @var \tool_monitor\output\helpicon\renderer the help icon renderer.
+     */
+    protected $helpiconrenderer;
+
+    /**
+     * Sets up the table_log parameters.
+     *
+     * @param string $uniqueid unique id of form.
+     * @param \moodle_url $url url where this table is displayed.
+     * @param int $courseid course id.
+     * @param int $perpage Number of rules to display per page.
+     */
+    public function __construct($uniqueid, \moodle_url $url, $courseid = 0, $perpage = 100) {
+        global $PAGE;
+
+        parent::__construct($uniqueid);
+
+        $this->set_attribute('class', 'toolmonitor subscriptions generaltable generalbox');
+        $this->define_columns(array('name', 'course', 'instance', 'unsubscribe', 'editrule'));
+        $this->define_headers(array(
+                get_string('name'),
+                get_string('course'),
+                get_string('moduleinstance', 'tool_monitor'),
+                get_string('unsubscribe', 'tool_monitor'),
+                get_string('editrule', 'tool_monitor')
+            )
+        );
+        $this->courseid = $courseid;
+        $this->pagesize = $perpage;
+        $systemcontext = \context_system::instance();
+        $this->context = empty($courseid) ? $systemcontext : \context_course::instance($courseid);
+        $this->collapsible(false);
+        $this->sortable(false);
+        $this->pageable(true);
+        $this->is_downloadable(false);
+        $this->define_baseurl($url);
+        $this->helpiconrenderer = $PAGE->get_renderer('tool_monitor', 'helpicon');
+    }
+
+    /**
+     * Generate content for name column.
+     *
+     * @param \tool_monitor\subscription $sub subscription object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_name(\tool_monitor\subscription $sub) {
+        $name = $sub->get_name($this->context);
+        $helpicon = new \tool_monitor\output\helpicon\renderable('subscription', $sub->id);
+        $helpicon = $this->helpiconrenderer->render($helpicon);
+
+        return $name . $helpicon;
+    }
+
+    /**
+     * Generate content for course column.
+     *
+     * @param \tool_monitor\subscription $sub subscription object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_course(\tool_monitor\subscription $sub) {
+       return $sub->get_course_name($this->context);
+    }
+
+    /**
+     * Generate content for description column.
+     *
+     * @param \tool_monitor\subscription $sub subscription object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_instance(\tool_monitor\subscription $sub) {
+        return $sub->get_instance_name();
+    }
+
+    /**
+     * Generate content for manage column.
+     *
+     * @param \tool_monitor\subscription $sub subscription object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_unsubscribe(\tool_monitor\subscription $sub) {
+        global $OUTPUT, $CFG;
+
+        $a = $sub->get_name($this->context);
+        $deleteurl = new \moodle_url($CFG->wwwroot. '/admin/tool/monitor/index.php', array('subscriptionid' => $sub->id,
+                'action' => 'unsubscribe', 'courseid' => $this->courseid, 'sesskey' => sesskey()));
+        $action = new \component_action('click', 'M.util.show_confirm_dialog', array('message' => get_string('subareyousure',
+            'tool_monitor', $a)));
+        $icon = $OUTPUT->action_link($deleteurl,
+                new \pix_icon('t/delete', get_string('deletesubscription', 'tool_monitor')), $action);
+        return $icon;
+    }
+
+    /**
+     * Generate content for edit rule column.
+     *
+     * @param \tool_monitor\subscription $sub subscription object
+     *
+     * @return string html used to display the column field.
+     */
+    public function col_editrule(\tool_monitor\subscription $sub) {
+        if ($sub->can_manage_rule()) {
+            // User can manage rule.
+            $editurl = new \moodle_url('/admin/tool/monitor/edit.php', array('ruleid' => $sub->ruleid,
+                    'courseid' => $sub->rulecourseid));
+            return \html_writer::link($editurl, get_string('editrule', 'tool_monitor'));
+        }
+        return '-';
+    }
+
+    /**
+     * Query the reader. Store results in the object for use by build_table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar.
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+
+        $total = \tool_monitor\subscription_manager::count_user_subscriptions();
+        $this->pagesize($pagesize, $total);
+        $subs = \tool_monitor\subscription_manager::get_user_subscriptions($this->get_page_start(), $this->get_page_size());
+        $this->rawdata = $subs;
+        // Set initial bars.
+        if ($useinitialsbar) {
+            $this->initialbars($total > $pagesize);
+        }
+    }
+}
diff --git a/admin/tool/monitor/classes/rule.php b/admin/tool/monitor/classes/rule.php
new file mode 100644 (file)
index 0000000..81b3aef
--- /dev/null
@@ -0,0 +1,247 @@
+<?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/>.
+
+/**
+ * Class represents a single rule.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class represents a single rule.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class rule {
+
+    /**
+     * @var \stdClass The rule object form database.
+     */
+    protected $rule;
+
+    /**
+     * Constructor.
+     *
+     * @param \stdClass $rule A rule object from database.
+     */
+    public function __construct($rule) {
+        $this->rule = $rule;
+    }
+
+    /**
+     * Can the current user manage this rule?
+     *
+     * @return bool true if the current user can manage this rule, else false.
+     */
+    public function can_manage_rule() {
+        $courseid = $this->courseid;
+        $context = empty($courseid) ? \context_system::instance() : \context_course::instance($this->courseid);
+        return has_capability('tool/monitor:managerules', $context);
+    }
+
+    /**
+     * Api to duplicate a rule in a given courseid.
+     *
+     * @param int $finalcourseid Final course id.
+     */
+    public function duplicate_rule($finalcourseid) {
+        $rule = fullclone($this->rule);
+        unset($rule->id);
+        $rule->courseid = $finalcourseid;
+        $time = time();
+        $rule->timecreated = $time;
+        $rule->timemodified = $time;
+        rule_manager::add_rule($rule);
+    }
+
+    /**
+     * Delete this rule.
+     *
+     * Note: It also removes all associated subscriptions.
+     */
+    public function delete_rule() {
+        rule_manager::delete_rule($this->id);
+    }
+
+    /**
+     * Generate a select drop down with list of possible modules for a given course and rule.
+     *
+     * @param int $courseid course id
+     *
+     * @return \single_select a single select object
+     * @throws \coding_exception
+     */
+    public function get_module_select($courseid) {
+        global $CFG;
+        $options = array();
+        if (strpos($this->plugin, 'mod_') === 0) {
+            $options[0] = get_string('allmodules', 'tool_monitor');
+        } else {
+            $options[0] = get_string('allevents', 'tool_monitor');
+        }
+        if (strpos($this->plugin, 'mod_') === 0) {
+            if ($courseid == 0) {
+                // They need to be in a course to select module instance.
+                return get_string('selectcourse', 'tool_monitor');
+            }
+            // Let them select an instance.
+            $cms = get_fast_modinfo($courseid);
+            $instances = $cms->get_instances_of(str_replace('mod_', '',  $this->plugin));
+            foreach ($instances as $cminfo) {
+                // Don't list instances that are not visible or available to the user.
+                if ($cminfo->uservisible && $cminfo->available) {
+                    $options[$cminfo->id] = $cminfo->get_formatted_name();
+                }
+            }
+        }
+        $url = new \moodle_url($CFG->wwwroot. '/admin/tool/monitor/index.php', array('courseid' => $courseid, 'ruleid' => $this->id,
+                'action' => 'subscribe', 'sesskey' => sesskey()));
+        return new \single_select($url, 'cmid', $options, '', $nothing = array('' => 'choosedots'));
+    }
+
+    /**
+     * Subscribe an user to this rule.
+     *
+     * @param int $courseid Course id.
+     * @param int $cmid Course module id.
+     * @param int $userid User id.
+     *
+     * @throws \coding_exception
+     */
+    public function subscribe_user($courseid, $cmid, $userid = 0) {
+        global $USER;
+
+        if ($this->courseid != $courseid && $this->courseid != 0) {
+            // Trying to subscribe to a rule that belongs to a different course. Should never happen.
+            throw new \coding_exception('Can not subscribe to rules from a different course');
+        }
+        if ($cmid !== 0) {
+            $cms = get_fast_modinfo($courseid);
+            $cminfo = $cms->get_cm($cmid);
+            if (!$cminfo->uservisible || !$cminfo->available) {
+                // Trying to subscribe to a hidden or restricted cm. Should never happen.
+                throw new \coding_exception('You cannot do that');
+            }
+        }
+        $userid = empty($userid) ? $USER->id : $userid;
+
+        subscription_manager::create_subscription($this->id, $courseid, $cmid, $userid);
+    }
+
+    /**
+     * Magic get method.
+     *
+     * @param string $prop property to get.
+     *
+     * @return mixed
+     * @throws \coding_exception
+     */
+    public function __get($prop) {
+        if (property_exists($this->rule, $prop)) {
+            return $this->rule->$prop;
+        }
+        throw new \coding_exception('Property "' . $prop . '" doesn\'t exist');
+    }
+
+    /**
+     * Return the rule data to be used while setting mform.
+     *
+     * @throws \coding_exception
+     */
+    public function get_mform_set_data() {
+        if (!empty($this->rule)) {
+            $rule = fullclone($this->rule);
+            $rule->description = array('text' => $rule->description, 'format' => $rule->descriptionformat);
+            $rule->template = array('text' => $rule->template, 'format' => $rule->templateformat);
+            return $rule;
+        }
+        throw new \coding_exception('Invalid call to get_mform_set_data.');
+    }
+
+    /**
+     * Method to get event name.
+     *
+     * @return string
+     * @throws \coding_exception
+     */
+    public function get_event_name() {
+        $eventclass = $this->eventname;
+        if (class_exists($eventclass)) {
+            return $eventclass::get_name();
+        }
+        return get_string('eventnotfound', 'tool_monitor');
+    }
+
+    /**
+     * Get filter description.
+     *
+     * @return string
+     */
+    public function get_filters_description() {
+        $a = new \stdClass();
+        $a->freq = $this->frequency;
+        $mins = $this->timewindow / MINSECS; // Convert seconds to minutes.
+        $a->mins = $mins;
+        return get_string('freqdesc', 'tool_monitor', $a);
+    }
+
+    /**
+     * Get properly formatted name of the rule associated.
+     *
+     * @param \context $context context where this name would be displayed.
+     *
+     * @return string Formatted name of the rule.
+     */
+    public function get_name(\context $context) {
+        return format_text($this->name, FORMAT_HTML, array('context' => $context));
+    }
+
+    /**
+     * Get properly formatted description of the rule associated.
+     *
+     * @param \context $context context where this description would be displayed.
+     *
+     * @return string Formatted description of the rule.
+     */
+    public function get_description(\context $context) {
+        return format_text($this->description, $this->descriptionformat, array('context' => $context));
+    }
+
+    /**
+     * Get name of the plugin associated with this rule
+     *
+     * @return string Plugin name.
+     */
+    public function get_plugin_name() {
+        if ($this->plugin === 'core') {
+            $string = get_string('core', 'tool_monitor');
+        } else if (get_string_manager()->string_exists('pluginname', $this->plugin)) {
+            $string = get_string('pluginname', $this->plugin);
+        } else {
+            $string = $this->plugin;
+        }
+        return $string;
+    }
+}
diff --git a/admin/tool/monitor/classes/rule_form.php b/admin/tool/monitor/classes/rule_form.php
new file mode 100644 (file)
index 0000000..a069779
--- /dev/null
@@ -0,0 +1,148 @@
+<?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 mform for creating and editing a rule.
+ *
+ * @copyright 2014 onwards Simey Lameze <lameze@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package   tool_monitor
+ */
+
+namespace tool_monitor;
+
+require_once($CFG->dirroot.'/lib/formslib.php');
+
+/**
+ * The mform for creating and editing a rule.
+ *
+ * @since     Moodle 2.8
+ * @copyright 2014 onwards Simey Lameze <lameze@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package   tool_monitor
+ */
+class rule_form extends \moodleform {
+
+    /**
+     * Mform class definition
+     *
+     */
+    public function definition () {
+        $mform = $this->_form;
+        $eventlist = $this->_customdata['eventlist'];
+        $pluginlist = $this->_customdata['pluginlist'];
+        $rule = $this->_customdata['rule'];
+        $courseid = $this->_customdata['courseid'];
+
+        // General section header.
+        $mform->addElement('header', 'general', get_string('general'));
+
+        // Hidden course ID.
+        $mform->addElement('hidden', 'courseid');
+        $mform->setType('courseid', PARAM_INT);
+
+        // We are editing a existing rule.
+        if (!empty($rule->id)) {
+            // Hidden rule id.
+            $mform->addElement('hidden', 'ruleid');
+            $mform->setType('ruleid', PARAM_INT);
+            $mform->setConstant('ruleid', $rule->id);
+
+            // Force course id.
+            $courseid = $rule->courseid;
+        }
+
+        // Make course id a constant.
+        $mform->setConstant('courseid', $courseid);
+
+        if (empty($courseid)) {
+            $context = \context_system::instance();
+        } else {
+            $context = \context_course::instance($courseid);
+        }
+
+        $editoroptions = array(
+            'subdirs' => 0,
+            'maxbytes' => 0,
+            'maxfiles' => 0,
+            'changeformat' => 0,
+            'context' => $context,
+            'noclean' => 0,
+            'trusttext' => 0
+        );
+
+        // Name field.
+        $mform->addElement('text', 'name', get_string('name', 'tool_monitor'), 'size="50"');
+        $mform->addRule('name', get_string('required'), 'required');
+        $mform->setType('name', PARAM_TEXT);
+        $mform->addHelpButton('name', 'name', 'tool_monitor');
+
+        // Plugin field.
+        $mform->addElement('select', 'plugin', get_string('selectplugin', 'tool_monitor'), $pluginlist);
+        $mform->addRule('plugin', get_string('required'), 'required');
+        $mform->addHelpButton('plugin', 'selectplugin', 'tool_monitor');
+
+        // Event field.
+        $mform->addElement('select', 'eventname', get_string('selectevent', 'tool_monitor'), $eventlist);
+        $mform->addRule('eventname', get_string('required'), 'required');
+        $mform->addHelpButton('eventname', 'selectevent', 'tool_monitor');
+
+        // Description field.
+        $mform->addElement('editor', 'description', get_string('description', 'tool_monitor'), $editoroptions);
+        $mform->addHelpButton('description', 'description', 'tool_monitor');
+
+        // Filters.
+        $mform->addElement('header', 'customisefilters', get_string('customisefilters', 'tool_monitor'));
+        $freq = array(1 => 1, 5 => 5, 10 => 10, 20 => 20, 30 => 30, 40 => 40, 50 => 50, 60 => 60, 70 => 70, 80 => 80, 90 => 90,
+                100 => 100, 1000 => 1000);
+        $mform->addElement('select', 'frequency', get_string('selectfrequency', 'tool_monitor'), $freq);
+        $mform->addRule('frequency', get_string('required'), 'required');
+        $mform->addHelpButton('frequency', 'selectfrequency', 'tool_monitor');
+
+        $mins = array(1 => 1, 5 => 5, 10 => 10, 15 => 15, 20 => 20, 25 => 25, 30 => 30, 35 => 35, 40 => 40, 45 => 45, 50 => 50,
+                55 => 55,  60 => 60);
+        $mform->addElement('select', 'minutes', get_string('selectminutes', 'tool_monitor'), $mins);
+        $mform->addRule('minutes', get_string('required'), 'required');
+
+        // Message template.
+        $mform->addElement('header', 'customisemessage', get_string('customisemessage', 'tool_monitor'));
+        $mform->addElement('editor', 'template', get_string('messagetemplate', 'tool_monitor'), $editoroptions);
+        $mform->setDefault('template', get_string('defaultmessagetpl', 'tool_monitor'));
+        $mform->addRule('template', get_string('required'), 'required');
+        $mform->addHelpButton('template', 'messagetemplate', 'tool_monitor');
+
+        // Action buttons.
+        $this->add_action_buttons(false, get_string('savechanges'));
+    }
+
+    /**
+     * Form validation
+     *
+     * @param array $data data from the form.
+     * @param array $files files uploaded.
+     *
+     * @return array of errors.
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        if (!eventlist::validate_event_plugin($data['plugin'], $data['eventname'])) {
+            $errors['eventname'] = get_string('errorincorrectevent', 'tool_monitor');
+        }
+
+        return $errors;
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/monitor/classes/rule_manager.php b/admin/tool/monitor/classes/rule_manager.php
new file mode 100644 (file)
index 0000000..864451c
--- /dev/null
@@ -0,0 +1,200 @@
+<?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/>.
+
+/**
+ * Rule manager class.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Simey Lameze <lameze@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_monitor;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Rule manager class.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Simey Lameze <lameze@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class rule_manager {
+
+    /**
+     * Create a new rule.
+     *
+     * @param \stdClass $ruledata data to insert as new rule entry.
+     *
+     * @return rule An instance of rule class.
+     */
+    public static function add_rule($ruledata) {
+        global $DB;
+
+        $now = time();
+        $ruledata->timecreated = $now;
+        $ruledata->timemodified = $now;
+
+        $ruledata->id = $DB->insert_record('tool_monitor_rules', $ruledata);
+        return new rule($ruledata);
+    }
+
+    /**
+     * Clean data submitted by mform.
+     *
+     * @param \stdClass $mformdata data to insert as new rule entry.
+     *
+     * @return \stdClass Cleaned rule data.
+     */
+    public static function clean_ruledata_form($mformdata) {
+        global $USER;
+
+        $rule = new \stdClass();
+        if (!empty($mformdata->ruleid)) {
+            $rule->id = $mformdata->ruleid;
+        }
+        $rule->userid = empty($mformdata->userid) ? $USER->id : $mformdata->userid;
+        $rule->courseid = $mformdata->courseid;
+        $rule->name = $mformdata->name;
+        $rule->plugin = $mformdata->plugin;
+        $rule->eventname = $mformdata->eventname;
+        $rule->description = $mformdata->description['text'];
+        $rule->descriptionformat = $mformdata->description['format'];
+        $rule->frequency = $mformdata->frequency;
+        $rule->timewindow = $mformdata->minutes * MINSECS;
+        $rule->template = $mformdata->template['text'];
+        $rule->templateformat = $mformdata->template['format'];
+
+        return $rule;
+    }
+
+    /**
+     * Delete a rule and associated subscriptions, by rule id.
+     *
+     * @param int $ruleid id of rule to be deleted.
+     *
+     * @return bool
+     */
+    public static function delete_rule($ruleid) {
+        global $DB;
+
+        subscription_manager::remove_all_subscriptions_for_rule($ruleid);
+        return $DB->delete_records('tool_monitor_rules', array('id' => $ruleid));
+    }
+
+    /**
+     * Get an instance of rule class.
+     *
+     * @param \stdClass|int $ruleorid A rule object from database or rule id.
+     *
+     * @return rule object with rule id.
+     */
+    public static function get_rule($ruleorid) {
+        global $DB;
+        if (!is_object($ruleorid)) {
+            $rule = $DB->get_record('tool_monitor_rules', array('id' => $ruleorid), '*', MUST_EXIST);
+        } else {
+            $rule = $ruleorid;
+        }
+
+        return new rule($rule);
+    }
+
+    /**
+     * Update rule data.
+     *
+     * @throws \coding_exception if $record->ruleid is invalid.
+     * @param object $ruledata rule data to be updated.
+     *
+     * @return bool
+     */
+    public static function update_rule($ruledata) {
+        global $DB;
+        if (!self::get_rule($ruledata->id)) {
+            throw new \coding_exception('Invalid rule ID.');
+        }
+        $ruledata->timemodified = time();
+        return $DB->update_record('tool_monitor_rules', $ruledata);
+    }
+
+    /**
+     * Get rules by course id.
+     *
+     * @param int $courseid course id of the rule.
+     * @param int $limitfrom Limit from which to fetch rules.
+     * @param int $limitto  Limit to which rules need to be fetched.
+     *
+     * @return array List of rules for the given course id, also includes system wide rules.
+     */
+    public static function get_rules_by_courseid($courseid, $limitfrom = 0, $limitto = 0) {
+        global $DB;
+        $select = "courseid = ? OR courseid = ?";
+        return self::get_instances($DB->get_records_select('tool_monitor_rules', $select, array(0, $courseid), null, '*',
+                $limitfrom, $limitto));
+    }
+
+    /**
+     * Get rule count by course id.
+     *
+     * @param int $courseid course id of the rule.
+     *
+     * @return int count of rules present in system visible in the given course id.
+     */
+    public static function count_rules_by_courseid($courseid) {
+        global $DB;
+        $select = "courseid = ? OR courseid = ?";
+        return $DB->count_records_select('tool_monitor_rules', $select, array(0, $courseid));
+    }
+
+    /**
+     * Get rules by plugin name.
+     *
+     * @param string $plugin plugin name of the rule.
+     *
+     * @return array List of rules for the given plugin name.
+     */
+    public static function get_rules_by_plugin($plugin) {
+        global $DB;
+        return self::get_instances($DB->get_records('tool_monitor_rules', array('plugin' => $plugin)));
+    }
+
+    /**
+     * Get rules by event name.
+     *
+     * @param string $eventname event name of the rule.
+     *
+     * @return array List of rules for the given event.
+     */
+    public static function get_rules_by_event($eventname) {
+        global $DB;
+        return self::get_instances($DB->get_records('tool_monitor_rules', array('eventname' => $eventname)));
+    }
+
+    /**
+     * Helper method to convert db records to instances.
+     *
+     * @param array $arr of rules.
+     *
+     * @return array of rules as instances.
+     */
+    protected static function get_instances($arr) {
+        $result = array();
+        foreach ($arr as $key => $sub) {
+            $result[$key] = new rule($sub);
+        }
+        return $result;
+    }
+}
diff --git a/admin/tool/monitor/classes/subscription.php b/admin/tool/monitor/classes/subscription.php
new file mode 100644 (file)
index 0000000..a729dc8
--- /dev/null
@@ -0,0 +1,192 @@
+<?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/>.
+
+/**
+ * Class represents a single subscription.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class represents a single subscription instance (i.e with all the subscription info).
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class subscription {
+    /**
+     * @var \stdClass
+     */
+    protected $subscription;
+
+    /**
+     * Constructor.
+     *
+     * use {@link \tool_monitor\subscription_manager::get_subscription} to get an instance instead of directly calling this method.
+     *
+     * @param \stdClass $subscription
+     */
+    public function __construct($subscription) {
+        $this->subscription = $subscription;
+    }
+
+    /**
+     * Magic get method.
+     *
+     * @param string $prop property to get.
+     *
+     * @return mixed
+     * @throws \coding_exception
+     */
+    public function __get($prop) {
+        if (property_exists($this->subscription, $prop)) {
+            return $this->subscription->$prop;
+        }
+        throw new \coding_exception('Property "' . $prop . '" doesn\'t exist');
+    }
+
+    /**
+     * Get a human readable name for instances associated with this subscription.
+     *
+     * @return string
+     * @throws \coding_exception
+     */
+    public function get_instance_name() {
+        if ($this->plugin === 'core') {
+            $string = get_string('allevents', 'tool_monitor');
+        } else {
+            if ($this->cmid == 0) {
+                $string = get_string('allmodules', 'tool_monitor');
+            } else {
+                $cms = get_fast_modinfo($this->courseid);
+                $cms = $cms->get_cms();
+                if (isset($cms[$this->cmid])) {
+                    $string = $cms[$this->cmid]->get_formatted_name(); // Instance name.
+                } else {
+                    // Something is wrong, instance is not present anymore.
+                    $string = get_string('invalidmodule', 'tool_monitor');
+                }
+            }
+        }
+
+        return $string;
+    }
+
+    /**
+     * Method to get event name.
+     *
+     * @return string
+     * @throws \coding_exception
+     */
+    public function get_event_name() {
+        $eventclass = $this->eventname;
+        if (class_exists($eventclass)) {
+            return $eventclass::get_name();
+        }
+        return get_string('eventnotfound', 'tool_monitor');
+    }
+
+    /**
+     * Get filter description.
+     *
+     * @return string
+     */
+    public function get_filters_description() {
+        $a = new \stdClass();
+        $a->freq = $this->frequency;
+        $mins = $this->timewindow / MINSECS; // Convert seconds to minutes.
+        $a->mins = $mins;
+        return get_string('freqdesc', 'tool_monitor', $a);
+    }
+
+    /**
+     * Get properly formatted name of the rule associated.
+     *
+     * @param \context $context context where this name would be displayed.
+     *
+     * @return string Formatted name of the rule.
+     */
+    public function get_name(\context $context) {
+        return format_text($this->name, FORMAT_HTML, array('context' => $context));
+    }
+
+    /**
+     * Get properly formatted description of the rule associated.
+     *
+     * @param \context $context context where this description would be displayed.
+     *
+     * @return string Formatted description of the rule.
+     */
+    public function get_description(\context $context) {
+        return format_text($this->description, $this->descriptionformat, array('context' => $context));
+    }
+
+    /**
+     * Get name of the plugin associated with this rule
+     *
+     * @return string Plugin name.
+     */
+    public function get_plugin_name() {
+        if ($this->plugin === 'core') {
+            $string = get_string('core', 'tool_monitor');
+        } else if (get_string_manager()->string_exists('pluginname', $this->plugin)) {
+            $string = get_string('pluginname', $this->plugin);
+        } else {
+            $string = $this->plugin;
+        }
+        return $string;
+    }
+
+    /**
+     * Get properly formatted name of the course associated.
+     *
+     * @param \context $context context where this name would be displayed.
+     *
+     * @return string Formatted name of the rule.
+     */
+    public function get_course_name(\context $context) {
+        global $SITE;
+        $courseid = $this->courseid;
+        if (empty($courseid)) {
+            $coursename = format_string($SITE->fullname, true, array('context' => $context));
+        } else {
+            $course = get_course($this->courseid);
+            $link = new \moodle_url('/course/view.php', array('id' => $course->id));
+            $coursename = format_string($course->fullname, true, array('context' => $context));
+            $coursename = \html_writer::link($link, $coursename);
+        }
+        return $coursename;
+    }
+
+    /**
+     * Can the current user manage the rule associate with this subscription?
+     *
+     * @return bool true if the current user can manage this rule, else false.
+     */
+    public function can_manage_rule() {
+        $courseid = $this->rulecourseid;
+        $context = empty($courseid) ? \context_system::instance() : \context_course::instance($courseid);
+        return has_capability('tool/monitor:managerules', $context);
+    }
+}
diff --git a/admin/tool/monitor/classes/subscription_manager.php b/admin/tool/monitor/classes/subscription_manager.php
new file mode 100644 (file)
index 0000000..82796a3
--- /dev/null
@@ -0,0 +1,282 @@
+<?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/>.
+
+/**
+ * Class to manage subscriptions.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_monitor;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to manage subscriptions.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class subscription_manager {
+    /**
+     * Subscribe a user to a given rule.
+     *
+     * @param int $ruleid  Rule id.
+     * @param int $courseid Course id.
+     * @param int $cmid Course module id.
+     * @param int $userid User who is subscribing, defaults to $USER.
+     *
+     * @return bool|int returns id of the created subscription.
+     */
+    public static function create_subscription($ruleid, $courseid, $cmid, $userid = 0) {
+        global $DB, $USER;
+
+        $subscription = new \stdClass();
+        $subscription->ruleid = $ruleid;
+        $subscription->courseid = $courseid;
+        $subscription->cmid = $cmid;
+        $subscription->userid = empty($userid) ? $USER->id : $userid;
+        if ($DB->record_exists('tool_monitor_subscriptions', (array)$subscription)) {
+            // Subscription already exists.
+            return false;
+        }
+
+        $subscription->timecreated = time();
+        return $DB->insert_record('tool_monitor_subscriptions', $subscription);
+    }
+
+    /**
+     * Delete a subscription.
+     *
+     * @param subscription|int $subscriptionorid an instance of subscription class or id.
+     * @param bool $checkuser Check if the subscription belongs to current user before deleting.
+     *
+     * @return bool
+     * @throws \coding_exception if $checkuser is true and the subscription doesn't belong to the current user.
+     */
+    public static function delete_subscription($subscriptionorid, $checkuser = true) {
+        global $DB, $USER;
+        if (is_object($subscriptionorid)) {
+            $subscription = $subscriptionorid;
+        } else {
+            $subscription = self::get_subscription($subscriptionorid);
+        }
+        if ($checkuser && $subscription->userid != $USER->id) {
+            throw new \coding_exception('Invalid subscription supplied');
+        }
+        return $DB->delete_records('tool_monitor_subscriptions', array('id' => $subscription->id));
+    }
+
+    /**
+     * Delete all subscriptions for a user.
+     *
+     * @param int $userid user id.
+     *
+     * @return mixed
+     */
+    public static function delete_user_subscriptions($userid) {
+        global $DB;
+        return $DB->delete_records('tool_monitor_subscriptions', array('userid' => $userid));
+    }
+
+    /**
+     * Delete all subscriptions for a course module.
+     *
+     * @param int $cmid cm id.
+     *
+     * @return mixed
+     */
+    public static function delete_cm_subscriptions($cmid) {
+        global $DB;
+        return $DB->delete_records('tool_monitor_subscriptions', array('cmid' => $cmid));
+    }
+
+    /**
+     * Delete all subscribers for a given rule.
+     *
+     * @param int $ruleid rule id.
+     *
+     * @return bool
+     */
+    public static function remove_all_subscriptions_for_rule($ruleid) {
+        global $DB;
+        return $DB->delete_records('tool_monitor_subscriptions', array('ruleid' => $ruleid));
+    }
+
+    /**
+     * Get a subscription instance for an given subscription id.
+     *
+     * @param subscription|int $subscriptionorid an instance of subscription class or id.
+     *
+     * @return subscription returns a instance of subscription class.
+     */
+    public static function get_subscription($subscriptionorid) {
+        global $DB;
+
+        if (is_object($subscriptionorid)) {
+            return new subscription($subscriptionorid);
+        }
+
+        $sql = self::get_subscription_join_rule_sql();
+        $sql .= "WHERE s.id = :id";
+        $sub = $DB->get_record_sql($sql, array('id' => $subscriptionorid), MUST_EXIST);
+        return new subscription($sub);
+    }
+
+    /**
+     * Get an array of subscriptions for a given user in a given course.
+     *
+     * @param int $courseid course id.
+     * @param int $limitfrom Limit from which to fetch rules.
+     * @param int $limitto  Limit to which rules need to be fetched.
+     * @param int $userid Id of the user for which the subscription needs to be fetched. Defaults to $USER;
+     * @param string $order Order to sort the subscriptions.
+     *
+     * @return array list of subscriptions
+     */
+    public static function get_user_subscriptions_for_course($courseid, $limitfrom = 0, $limitto = 0, $userid = 0,
+            $order = 's.timecreated DESC' ) {
+        global $DB, $USER;
+        if ($userid == 0) {
+            $userid = $USER->id;
+        }
+        $sql = self::get_subscription_join_rule_sql();
+        $sql .= "WHERE s.courseid = :courseid AND s.userid = :userid ORDER BY $order";
+
+        return self::get_instances($DB->get_records_sql($sql, array('courseid' => $courseid, 'userid' => $userid), $limitfrom,
+                $limitto));
+    }
+
+    /**
+     * Get count of subscriptions for a given user in a given course.
+     *
+     * @param int $courseid course id.
+     * @param int $userid Id of the user for which the subscription needs to be fetched. Defaults to $USER;
+     *
+     * @return int number of subscriptions
+     */
+    public static function count_user_subscriptions_for_course($courseid, $userid = 0) {
+        global $DB, $USER;
+        if ($userid == 0) {
+            $userid = $USER->id;
+        }
+        $sql = self::get_subscription_join_rule_sql(true);
+        $sql .= "WHERE s.courseid = :courseid AND s.userid = :userid";
+
+        return $DB->count_records_sql($sql, array('courseid' => $courseid, 'userid' => $userid));
+    }
+
+    /**
+     * Get an array of subscriptions for a given user.
+     *
+     * @param int $limitfrom Limit from which to fetch rules.
+     * @param int $limitto  Limit to which rules need to be fetched.
+     * @param int $userid Id of the user for which the subscription needs to be fetched. Defaults to $USER;
+     * @param string $order Order to sort the subscriptions.
+     *
+     * @return array list of subscriptions
+     */
+    public static function get_user_subscriptions($limitfrom = 0, $limitto = 0, $userid = 0,
+                                                             $order = 's.timecreated DESC' ) {
+        global $DB, $USER;
+        if ($userid == 0) {
+            $userid = $USER->id;
+        }
+        $sql = self::get_subscription_join_rule_sql();
+        $sql .= "WHERE s.userid = :userid ORDER BY $order";
+
+        return self::get_instances($DB->get_records_sql($sql, array('userid' => $userid), $limitfrom, $limitto));
+    }
+
+    /**
+     * Get count of subscriptions for a given user.
+     *
+     * @param int $userid Id of the user for which the subscription needs to be fetched. Defaults to $USER;
+     *
+     * @return int number of subscriptions
+     */
+    public static function count_user_subscriptions($userid = 0) {
+        global $DB, $USER;;
+        if ($userid == 0) {
+            $userid = $USER->id;
+        }
+        $sql = self::get_subscription_join_rule_sql(true);
+        $sql .= "WHERE s.userid = :userid";
+
+        return $DB->count_records_sql($sql, array('userid' => $userid));
+    }
+
+    /**
+     * Return a list of subscriptions for a given event.
+     *
+     * @param \stdClass $event the event object.
+     *
+     * @return array
+     */
+    public static function get_subscriptions_by_event(\stdClass $event) {
+        global $DB;
+
+        $sql = self::get_subscription_join_rule_sql();
+        if ($event->contextlevel == CONTEXT_MODULE && $event->contextinstanceid != 0) {
+            $sql .= "WHERE r.eventname = :eventname AND s.courseid = :courseid AND (s.cmid = :cmid OR s.cmid = 0)";
+            $params = array('eventname' => $event->eventname, 'courseid' => $event->courseid, 'cmid' => $event->contextinstanceid);
+        } else {
+            $sql .= "WHERE r.eventname = :eventname AND (s.courseid = :courseid OR s.courseid = 0)";
+            $params = array('eventname' => $event->eventname, 'courseid' => $event->courseid);
+        }
+        return self::get_instances($DB->get_records_sql($sql, $params));
+    }
+
+    /**
+     * Return sql to join rule and subscription table.
+     *
+     * @param bool $count Weather if this is a count query or not.
+     *
+     * @return string the sql.
+     */
+    protected static function get_subscription_join_rule_sql($count = false) {
+        if ($count) {
+            $select = "SELECT COUNT(s.id) ";
+        } else {
+            $select = "SELECT s.*, r.description, r.descriptionformat, r.name, r.userid as ruleuserid, r.courseid as rulecourseid,
+            r.plugin, r.eventname, r.template, r.templateformat, r.frequency, r.timewindow";
+        }
+        $sql = $select . "
+                  FROM {tool_monitor_rules} r
+                  JOIN {tool_monitor_subscriptions} s
+                        ON r.id = s.ruleid ";
+        return $sql;
+    }
+
+    /**
+     * Helper method to convert db records to instances.
+     *
+     * @param array $arr of subscriptions.
+     *
+     * @return array of subscriptions as instances.
+     */
+    protected static function get_instances($arr) {
+        $result = array();
+        foreach ($arr as $key => $sub) {
+            $result[$key] = new subscription($sub);
+        }
+        return $result;
+    }
+}
diff --git a/admin/tool/monitor/db/access.php b/admin/tool/monitor/db/access.php
new file mode 100644 (file)
index 0000000..d1b687c
--- /dev/null
@@ -0,0 +1,53 @@
+<?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/>.
+
+/**
+ * Capabilities.
+ *
+ * This files lists capabilities related to tool_monitor.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+
+    'tool/monitor:subscribe' => array(
+        'riskbitmask' => RISK_PERSONAL,
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'teacher' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW
+        ),
+    ),
+
+    'tool/monitor:managerules' => array(
+        'riskbitmask' => RISK_XSS,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'teacher' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW
+        ),
+    ),
+);
+
diff --git a/admin/tool/monitor/db/events.php b/admin/tool/monitor/db/events.php
new file mode 100644 (file)
index 0000000..0b1859f
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file definies observers needed by the tool.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// List of observers.
+$observers = array(
+    array(
+        'eventname'   => '\core\event\course_deleted',
+        'priority'    => 1,
+        'callback'    => '\tool_monitor\eventobservers::course_deleted',
+    ),
+    array(
+        'eventname'   => '*',
+        'callback'    => '\tool_monitor\eventobservers::process_event',
+    ),
+    array(
+        'eventname'   => '\core\event\user_deleted',
+        'callback'    => '\tool_monitor\eventobservers::user_deleted',
+    ),
+    array(
+        'eventname'   => '\core\event\course_module_deleted',
+        'callback'    => '\tool_monitor\eventobservers::course_module_deleted',
+    )
+);
diff --git a/admin/tool/monitor/db/install.xml b/admin/tool/monitor/db/install.xml
new file mode 100644 (file)
index 0000000..39d2bb9
--- /dev/null
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<XMLDB PATH="tool/monitor/db" VERSION="20140708" COMMENT="XMLDB file for Moodle tool/monitor"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
+>
+  <TABLES>
+    <TABLE NAME="tool_monitor_rules" COMMENT="Table to store rules">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Description of the rule"/>
+        <FIELD NAME="descriptionformat" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="Description format"/>
+        <FIELD NAME="name" TYPE="char" LENGTH="254" NOTNULL="true" SEQUENCE="false" COMMENT="Name of the rule"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Id of user who created the rule"/>
+        <FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Id of course to which this rule belongs."/>
+        <FIELD NAME="plugin" TYPE="char" LENGTH="254" NOTNULL="true" SEQUENCE="false" COMMENT="Frankenstlye name of the plguin"/>
+        <FIELD NAME="eventname" TYPE="char" LENGTH="254" NOTNULL="true" SEQUENCE="false" COMMENT="Fully qualified name of the event"/>
+        <FIELD NAME="template" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Message template"/>
+        <FIELD NAME="templateformat" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="Template format"/>
+        <FIELD NAME="frequency" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false" COMMENT="Frequency"/>
+        <FIELD NAME="timewindow" TYPE="int" LENGTH="5" NOTNULL="true" SEQUENCE="false" COMMENT="Time window in seconds"/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp when this rule was last modified"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Time stamp of when this rule was created"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="courseanduser" UNIQUE="false" FIELDS="courseid, userid" COMMENT="Index on courseid and userid"/>
+        <INDEX NAME="eventname" UNIQUE="false" FIELDS="eventname" COMMENT="eventname"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="tool_monitor_subscriptions" COMMENT="Table to store user subscriptions to various rules">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Course id of the subscription"/>
+        <FIELD NAME="ruleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Rule id"/>
+        <FIELD NAME="cmid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Course module id"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="User id of the subscriber"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp of when this subscription was created"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="rulekey" TYPE="foreign" FIELDS="ruleid" REFTABLE="tool_monitor_rules" REFFIELDS="id" COMMENT="Foreign key"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="courseanduser" UNIQUE="false" FIELDS="courseid, userid" COMMENT="Course and user"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="tool_monitor_history" COMMENT="Table to store history of message notifications sent">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="sid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Subscription id"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="User to whom this notification was sent"/>
+        <FIELD NAME="timesent" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp of when the message was sent."/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="subscrptionid" TYPE="foreign" FIELDS="sid" REFTABLE="tool_monitor_subscriptions" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="sid_userid_timesent" UNIQUE="true" FIELDS="sid, userid, timesent"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="tool_monitor_events" COMMENT="A table that keeps a log of events related to subscriptions">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="eventname" TYPE="char" LENGTH="254" NOTNULL="true" SEQUENCE="false" COMMENT="Event name"/>
+        <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Context id"/>
+        <FIELD NAME="contextlevel" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Context level"/>
+        <FIELD NAME="contextinstanceid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Context instance id"/>
+        <FIELD NAME="link" TYPE="char" LENGTH="254" NOTNULL="true" SEQUENCE="false" COMMENT="Link to the event location"/>
+        <FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="course id"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Time created"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+    </TABLE>
+  </TABLES>
+</XMLDB>
\ No newline at end of file
diff --git a/admin/tool/monitor/db/messages.php b/admin/tool/monitor/db/messages.php
new file mode 100644 (file)
index 0000000..e80f6ef
--- /dev/null
@@ -0,0 +1,32 @@
+<?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/>.
+
+/**
+ * Message providers list.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$messageproviders = array (
+    // Notify a user that a rule has happened.
+    'notification' => array (
+        'capability'  => 'tool/monitor:subscribe'
+    )
+);
diff --git a/admin/tool/monitor/edit.php b/admin/tool/monitor/edit.php
new file mode 100644 (file)
index 0000000..c69e92e
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file gives an overview of the monitors present in site.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+require(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$ruleid = optional_param('ruleid', 0, PARAM_INT);
+$courseid = optional_param('courseid', 0, PARAM_INT);
+
+// Validate course id.
+if (empty($courseid)) {
+    require_login();
+    $context = context_system::instance();
+    $coursename = format_string($SITE->fullname, true, array('context' => $context));
+    $PAGE->set_context($context);
+} else {
+    $course = get_course($courseid);
+    require_login($course);
+    $context = context_course::instance($course->id);
+    $coursename = format_string($course->fullname, true, array('context' => $context));
+}
+
+// Check for caps.
+require_capability('tool/monitor:managerules', $context);
+
+// Set up the page.
+$a = new stdClass();
+$a->coursename = $coursename;
+$a->reportname = get_string('pluginname', 'tool_monitor');
+$title = get_string('title', 'tool_monitor', $a);
+$url = new moodle_url("/admin/tool/monitor/edit.php", array('courseid' => $courseid, 'ruleid' => $ruleid));
+$manageurl = new moodle_url("/admin/tool/monitor/managerules.php", array('courseid' => $courseid));
+
+$PAGE->set_url($url);
+$PAGE->set_pagelayout('report');
+$PAGE->set_title($title);
+$PAGE->set_heading($title);
+
+// Get data ready for mform.
+$eventlist = tool_monitor\eventlist::get_all_eventlist(true);
+$pluginlist = tool_monitor\eventlist::get_plugin_list();
+$eventlist = array_merge(array('' => get_string('choosedots')), $eventlist);
+$pluginlist = array_merge(array('' => get_string('choosedots')), $pluginlist);
+
+// Set up the yui module.
+$PAGE->requires->yui_module('moodle-tool_monitor-dropdown', 'Y.M.tool_monitor.DropDown.init',
+        array(array('eventlist' => $eventlist)));
+
+// Site level report.
+if (empty($courseid)) {
+    admin_externalpage_setup('toolmonitorrules', '', null, '', array('pagelayout' => 'report'));
+} else {
+    // Course level report.
+    $PAGE->navigation->override_active_url($manageurl);
+}
+
+// Mform setup.
+if (!empty($ruleid)) {
+    $rule = \tool_monitor\rule_manager::get_rule($ruleid)->get_mform_set_data();
+    $rule->minutes = $rule->timewindow / MINSECS;
+} else {
+    $rule = new stdClass();
+}
+
+$mform = new tool_monitor\rule_form(null, array('eventlist' => $eventlist, 'pluginlist' => $pluginlist, 'rule' => $rule,
+        'courseid' => $courseid));
+
+if ($mformdata = $mform->get_data()) {
+    $rule = \tool_monitor\rule_manager::clean_ruledata_form($mformdata);
+
+    if (empty($rule->id)) {
+        \tool_monitor\rule_manager::add_rule($rule);
+    } else {
+        \tool_monitor\rule_manager::update_rule($rule);
+    }
+
+    redirect($manageurl);
+} else {
+    echo $OUTPUT->header();
+    $mform->set_data($rule);
+    $mform->display();
+    echo $OUTPUT->footer();
+}
+
diff --git a/admin/tool/monitor/help.php b/admin/tool/monitor/help.php
new file mode 100644 (file)
index 0000000..572b4ba
--- /dev/null
@@ -0,0 +1,62 @@
+<?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/>.
+
+/**
+ * Displays help on a new page.
+ *
+ * @copyright 2014 Mark Nelson <markn@moodle.com>
+ * @package tool_monitor
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_MOODLE_COOKIES', true);
+
+require_once('../../../config.php');
+
+$type = required_param('type', PARAM_ALPHA);
+$id = required_param('id', PARAM_INT);
+$lang = optional_param('lang', 'en', PARAM_LANG);
+
+// We don't actually modify the session here as we have NO_MOODLE_COOKIES set.
+$SESSION->lang = $lang;
+
+$PAGE->set_url('/admin/tool/monitor/help.php');
+$PAGE->set_pagelayout('popup');
+
+if ($type == 'rule') {
+    $item = \tool_monitor\rule_manager::get_rule($id);
+} else { // Must be a subscription.
+    $item = \tool_monitor\subscription_manager::get_subscription($id);
+}
+
+if ($item->courseid) {
+    $PAGE->set_context(context_course::instance($item->courseid));
+} else { // Must be system context.
+    $PAGE->set_context(context_system::instance());
+}
+
+// Get the help string data.
+$data = tool_monitor\output\helpicon\renderable::get_help_string_parameters($type, $id);
+
+echo $OUTPUT->header();
+if (!empty($data->heading)) {
+    echo $OUTPUT->heading($data->heading, 1, 'helpheading');
+}
+echo $data->text;
+if (isset($data->completedoclink)) {
+    echo $data->completedoclink;
+}
+echo $OUTPUT->footer();
diff --git a/admin/tool/monitor/help_ajax.php b/admin/tool/monitor/help_ajax.php
new file mode 100644 (file)
index 0000000..429ff3b
--- /dev/null
@@ -0,0 +1,50 @@
+<?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/>.
+
+/**
+ * Displays help via AJAX call.
+ *
+ * @copyright 2014 Mark Nelson <markn@moodle.com>
+ * @package tool_monitor
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_MOODLE_COOKIES', true);
+define('AJAX_SCRIPT', true);
+
+require_once('../../../config.php');
+
+$type = required_param('type', PARAM_ALPHA);
+$id = required_param('id', PARAM_INT);
+$lang = optional_param('lang', 'en', PARAM_LANG);
+
+// We don't actually modify the session here as we have NO_MOODLE_COOKIES set.
+$SESSION->lang = $lang;
+$PAGE->set_url('/admin/tool/monitor/help_ajax.php');
+
+if ($type == 'rule') {
+    $item = \tool_monitor\rule_manager::get_rule($id);
+} else { // Must be a subscription.
+    $item = \tool_monitor\subscription_manager::get_subscription($id);
+}
+
+if ($item->courseid) {
+    $PAGE->set_context(context_course::instance($item->courseid));
+} else { // Must be system context.
+    $PAGE->set_context(context_system::instance());
+}
+
+echo json_encode(tool_monitor\output\helpicon\renderable::get_help_string_parameters($type, $id, true));
diff --git a/admin/tool/monitor/index.php b/admin/tool/monitor/index.php
new file mode 100644 (file)
index 0000000..45c7309
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This page lets users to manage rules for a given course.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$courseid = optional_param('courseid', 0, PARAM_INT);
+$action = optional_param('action', '', PARAM_ALPHA);
+$cmid = optional_param('cmid', 0, PARAM_INT);
+$ruleid = optional_param('ruleid', 0, PARAM_INT);
+$subscriptionid = optional_param('subscriptionid', 0, PARAM_INT);
+
+// Validate course id.
+if (empty($courseid)) {
+    require_login();
+} else {
+    // They might want to see rules for this course.
+    $course = get_course($courseid);
+    require_login($course);
+    $coursecontext = context_course::instance($course->id);
+    // Check for caps.
+    require_capability('tool/monitor:subscribe', $coursecontext);
+    $coursename = format_string($course->fullname, true, array('context' => $coursecontext));
+}
+
+// Always build the page in site context.
+$context = context_system::instance();
+$sitename = format_string($SITE->fullname, true, array('context' => $context));
+$PAGE->set_context($context);
+
+// Set up the page.
+$a = new stdClass();
+$a->coursename = $sitename;
+$a->reportname = get_string('pluginname', 'tool_monitor');
+$title = get_string('title', 'tool_monitor', $a);
+$indexurl = new moodle_url("/admin/tool/monitor/index.php", array('courseid' => $courseid));
+
+$PAGE->set_url($indexurl);
+$PAGE->set_pagelayout('report');
+$PAGE->set_title($title);
+$PAGE->set_heading($title);
+
+echo $OUTPUT->header();
+
+// Create/delete subscription if needed.
+if (!empty($action)) {
+    require_sesskey();
+    switch ($action) {
+        case 'subscribe' :
+            $rule = \tool_monitor\rule_manager::get_rule($ruleid);
+            $rule->subscribe_user($courseid, $cmid);
+            echo $OUTPUT->notification(get_string('subcreatesuccess', 'tool_monitor'), 'notifysuccess');
+            break;
+        case 'unsubscribe' :
+            \tool_monitor\subscription_manager::delete_subscription($subscriptionid);
+            echo $OUTPUT->notification(get_string('subdeletesuccess', 'tool_monitor'), 'notifysuccess');
+            break;
+        default:
+    }
+}
+
+// Render the current subscriptions list.
+$totalsubs = \tool_monitor\subscription_manager::count_user_subscriptions();
+$renderer = $PAGE->get_renderer('tool_monitor', 'managesubs');
+if (!empty($totalsubs)) {
+    // Show the subscriptions section only if there are subscriptions.
+    $subs = new \tool_monitor\output\managesubs\subs('toolmonitorsubs', $indexurl, $courseid);
+    echo $OUTPUT->heading(get_string('currentsubscriptions', 'tool_monitor'));
+    echo $renderer->render($subs);
+}
+
+// Render the potential rules list.
+$totalrules = \tool_monitor\rule_manager::count_rules_by_courseid($courseid);
+echo $OUTPUT->heading(get_string('rulescansubscribe', 'tool_monitor'));
+$rules = new \tool_monitor\output\managesubs\rules('toolmonitorrules', $indexurl, $courseid);
+echo $renderer->render($rules);
+if (empty($totalrules)) {
+    // No rules present. Show a link to manage rules page if permissions permit.
+    echo html_writer::start_div();
+    echo html_writer::tag('span', get_string('norules', 'tool_monitor'));
+    if (has_capability('tool/monitor:managerules', $context)) {
+        $manageurl = new moodle_url("/admin/tool/monitor/managerules.php", array('courseid' => $courseid));
+        $a = html_writer::link($manageurl, get_string('managerules', 'tool_monitor'));
+        $link = "&nbsp;";
+        $link .= html_writer::tag('span', get_string('manageruleslink', 'tool_monitor', $a));
+        echo $link;
+    }
+    echo html_writer::end_div();
+}
+echo $OUTPUT->footer();
diff --git a/admin/tool/monitor/lang/en/tool_monitor.php b/admin/tool/monitor/lang/en/tool_monitor.php
new file mode 100644 (file)
index 0000000..bfac799
--- /dev/null
@@ -0,0 +1,91 @@
+<?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/>.
+
+/**
+ * Lang strings.
+ *
+ * This files lists lang strings related to tool_monitor.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['addrule'] = 'Add a new rule';
+$string['allevents'] = 'All events';
+$string['allmodules'] = 'All modules';
+$string['core'] = 'Core';
+$string['customisefilters'] = 'Select the frequency of the events';
+$string['customisemessage'] = 'Customise the notification message';
+$string['currentsubscriptions'] = 'Your current subscriptions';
+$string['description_help'] = "Description is displayed to users when they want to subscribe to this rule. This helps them understand what the rule is about.";
+$string['defaultmessagetpl'] = 'Rule "{rulename}" has happened. You can find further details at {link}';
+$string['deleterule'] = 'Delete rule';
+$string['deletesubscription'] = 'Delete subscription';
+$string['description'] = 'Description:';
+$string['duplicaterule'] = 'Duplicate rule';
+$string['editrule'] = 'Edit rule';
+$string['eventnotfound'] = 'Event not found';
+$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['moduleinstance'] = 'Module instance';
+$string['manage'] = 'Manage';
+$string['managesubscriptions'] = 'Event monitoring';
+$string['managerules'] = 'Event monitoring rules';
+$string['messageheader'] = 'Customise your notification message';
+$string['messageprovider:notification'] = 'Notifications of rule subscriptions';
+$string['messagetemplate'] = 'Message template';
+$string['messagetemplate_help'] = 'This is the content of the message that will be sent to users, when the given conditions of the rule are met. You are allowed to use following templates in this.
+<br /> {link} - Link to the location where the event happened.
+<br /> {modulelink} - Link to the module where the event has happened.
+<br /> {rulename} - Name of this rule.
+<br /> {description} - Rule description.
+<br /> {eventname} - Name of the event associated with the rule.';
+$string['minutes'] = 'in minutes:';
+$string['name'] = 'Name of the rule: ';
+$string['name_help'] = "Choose a name for the rule.";
+$string['pluginname'] = 'Event monitor';
+$string['processevents'] = 'Process events';
+$string['ruleareyousure'] = 'Are you sure you want to delete rule "{$a}"?';
+$string['rulecopysuccess'] = 'Rule successfully duplicated';
+$string['ruledeletesuccess'] = 'Rule successfully deleted';
+$string['rulehelp'] = 'Rule details';
+$string['rulehelp_help'] = 'This rule listens for when the event \'{$a->eventname}\' in \'{$a->eventcomponent}\' has been triggered {$a->frequency} time(s) in {$a->minutes} minute(s).';
+$string['rulenopermissions'] = 'You do not have permissions to "{$a} a rule"';
+$string['rulescansubscribe'] = 'Rules you can subscribe to';
+$string['selectacourse'] = 'Select a course';
+$string['selectcourse'] = 'Visit this report at course level to get a list of possible modules';
+$string['selectevent'] = 'Select an event:';
+$string['selectevent_help'] = "Select an event to monitor. Please note that some events are only triggered for the entire site (e.g. 'course created') and will never trigger when subscribed to from within a course.";
+$string['selectfrequency'] = 'Frequency of events:';
+$string['selectfrequency_help'] = "Frequency defines the denisty of the event occurrence. Select criterias to define how frequently the event should happen to trigger the notification.";
+$string['selectminutes'] = 'in minutes:';
+$string['selectplugin'] = 'Select the plugin type:';
+$string['selectplugin_help'] = "Select a plugin that you are interested in monitoring. The event list below would be updated to display events from the selected plugin.";
+$string['subareyousure'] = 'Are you sure you want to delete this subscription for the rule "{$a}"?';
+$string['subcreatesuccess'] = "Subscription successfully created";
+$string['subdeletesuccess'] = "Subscription successfully removed";
+$string['subhelp'] = 'Subscription details';
+$string['subhelp_help'] = 'This subscription listens for when the event \'{$a->eventname}\' has been triggered in \'{$a->moduleinstance}\' {$a->frequency} time(s) in {$a->minutes} minute(s).';
+$string['title'] = '{$a->coursename} : {$a->reportname}';
+$string['monitor:managerules'] = 'Manage event monitor rules';
+$string['monitor:subscribe'] = 'Subscribe to event monitor rules';
+$string['unsubscribe'] = 'Unsubscribe';
+
diff --git a/admin/tool/monitor/lib.php b/admin/tool/monitor/lib.php
new file mode 100644 (file)
index 0000000..88d2df9
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This page lists public api for tool_monitor plugin.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * This function extends the navigation with the tool items
+ *
+ * @param navigation_node $navigation The navigation node to extend
+ * @param stdClass        $course     The course to object for the tool
+ * @param context         $context    The context of the course
+ */
+function tool_monitor_extend_navigation_course($navigation, $course, $context) {
+
+    if (has_capability('tool/monitor:managerules', $context)) {
+        $url = new moodle_url('/admin/tool/monitor/managerules.php', array('courseid' => $course->id));
+        $settingsnode = navigation_node::create(get_string('managerules', 'tool_monitor'), $url, navigation_node::TYPE_SETTING,
+                null, null, new pix_icon('i/settings', ''));
+        $reportnode = $navigation->get('coursereports');
+
+        if (isset($settingsnode) && !empty($reportnode)) {
+            $reportnode->add_node($settingsnode);
+        }
+    }
+}
+
+/**
+ * This function extends the navigation with the tool items for user settings node.
+ *
+ * @param navigation_node $navigation  The navigation node to extend
+ * @param stdClass        $user        The user object
+ * @param context         $usercontext The context of the user
+ * @param stdClass        $course      The course to object for the tool
+ * @param context         $coursecontext     The context of the course
+ */
+function tool_monitor_extend_navigation_user_settings($navigation, $user, $usercontext, $course, $coursecontext) {
+    global $USER;
+    if (($USER->id == $user->id)) {
+        $url = new moodle_url('/admin/tool/monitor/index.php', array('courseid' => $course->id));
+        $subsnode = navigation_node::create(get_string('managesubscriptions', 'tool_monitor'), $url,
+                navigation_node::TYPE_SETTING, null, null, new pix_icon('i/settings', ''));
+
+        if (isset($subsnode) && !empty($navigation)) {
+            $navigation->add_node($subsnode, 'changepassword');
+        }
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/monitor/managerules.php b/admin/tool/monitor/managerules.php
new file mode 100644 (file)
index 0000000..8259fec
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This page lets users to manage rules for a given course.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$courseid = optional_param('courseid', 0, PARAM_INT);
+$ruleid = optional_param('ruleid', 0, PARAM_INT);
+$action = optional_param('action', '', PARAM_ALPHA);
+
+// Validate course id.
+if (empty($courseid)) {
+    require_login();
+    $context = context_system::instance();
+    $coursename = format_string($SITE->fullname, true, array('context' => $context));
+    $PAGE->set_context($context);
+} else {
+    $course = get_course($courseid);
+    require_login($course);
+    $context = context_course::instance($course->id);
+    $coursename = format_string($course->fullname, true, array('context' => $context));
+}
+
+// Check for caps.
+require_capability('tool/monitor:managerules', $context);
+
+// Set up the page.
+$a = new stdClass();
+$a->coursename = $coursename;
+$a->reportname = get_string('pluginname', 'tool_monitor');
+$title = get_string('title', 'tool_monitor', $a);
+$manageurl = new moodle_url("/admin/tool/monitor/managerules.php", array('courseid' => $courseid));
+
+$PAGE->set_url($manageurl);
+$PAGE->set_pagelayout('report');
+$PAGE->set_title($title);
+$PAGE->set_heading($title);
+
+// Site level report.
+if (empty($courseid)) {
+    admin_externalpage_setup('toolmonitorrules', '', null, '', array('pagelayout' => 'report'));
+}
+
+echo $OUTPUT->header();
+
+// Copy/delete rule if needed.
+if (!empty($action) && $ruleid) {
+    require_sesskey();
+    $rule = \tool_monitor\rule_manager::get_rule($ruleid);
+    if ($rule->can_manage_rule()) {
+        switch ($action) {
+            case 'copy' :
+                $rule->duplicate_rule($courseid);
+                echo $OUTPUT->notification(get_string('rulecopysuccess', 'tool_monitor'), 'notifysuccess');
+                break;
+            case 'delete' :
+                $rule->delete_rule();
+                echo $OUTPUT->notification(get_string('ruledeletesuccess', 'tool_monitor'), 'notifysuccess');
+                break;
+            default:
+        }
+    } else {
+        // User doesn't have permissions. Should never happen for real users.
+        throw new moodle_exception('rulenopermissions', 'tool_monitor', $manageurl, $action);
+    }
+}
+
+// Render the rule list.
+$renderable = new \tool_monitor\output\managerules\renderable('toolmonitorrules', $manageurl, $courseid);
+$renderer = $PAGE->get_renderer('tool_monitor', 'managerules');
+echo $renderer->render($renderable);
+echo $OUTPUT->footer();
diff --git a/admin/tool/monitor/settings.php b/admin/tool/monitor/settings.php
new file mode 100644 (file)
index 0000000..9e32468
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Links and settings
+ *
+ * This file contains links and settings used by tool_monitor
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die;
+
+if ($hassiteconfig) {
+
+    // Manage rules page.
+    $url = new moodle_url('/admin/tool/monitor/managerules.php', array('courseid' => 0));
+    $temp = new admin_externalpage('toolmonitorrules', get_string('managerules', 'tool_monitor'), $url,
+        'tool/monitor:managerules');
+    $ADMIN->add('reports', $temp);
+}
diff --git a/admin/tool/monitor/tests/behat/rule.feature b/admin/tool/monitor/tests/behat/rule.feature
new file mode 100644 (file)
index 0000000..80f74d3
--- /dev/null
@@ -0,0 +1,161 @@
+@javascript @tool @tool_monitor @tool_monitor_rules
+Feature: tool_monitor_rule
+  In order to manage rules
+  As an admin
+  I need to create a rule, edit a rule, duplicate a rule and delete a rule
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And   I log in as "admin"
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    And   I press "Add a new rule"
+    And   I set the following fields to these values:
+      | name              | New rule course level                             |
+      | plugin            | Forum                                             |
+      | eventname         | Post created                                      |
+      | id_description    | I want a rule to monitor posts created on a forum |
+      | frequency         | 1                                                 |
+      | minutes           | 1                                                 |
+      | Message template  | The forum post was created. {modulelink}          |
+    And   I press "Save changes"
+    And   I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    And   I press "Add a new rule"
+    And   I set the following fields to these values:
+      | name              | New rule site level                               |
+      | plugin            | Forum                                             |
+      | eventname         | Post created                                      |
+      | id_description    | I want a rule to monitor posts created on a forum |
+      | frequency         | 1                                                 |
+      | minutes           | 1                                                 |
+      | Message template  | The forum post was created. {modulelink}          |
+    And  I press "Save changes"
+    And  I log out
+
+  Scenario: Add a rule on course level
+    Given I log in as "teacher1"
+    And   I am on homepage
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    When  I press "Add a new rule"
+    And   I set the following fields to these values:
+      | name              | New rule                                          |
+      | plugin            | Forum                                             |
+      | eventname         | Post created                                      |
+      | id_description    | I want a rule to monitor posts created on a forum |
+      | frequency         | 1                                                 |
+      | minutes           | 1                                                 |
+      | Message template  | The forum post was created. {modulelink}          |
+    And   I press "Save changes"
+    Then  I should see "New rule"
+    And   I should see "I want a rule to monitor posts created on a forum"
+    And   I should see "Forum"
+    And   I should see "Post created"
+    And   I should see "1 times in 1 minutes"
+
+  Scenario: Delete a rule on course level
+    Given I log in as "teacher1"
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    When  I click on "Delete rule" "link"
+    Then  I should see "Are you sure you want to delete rule \"New rule course level\"?"
+    And   I press "Yes"
+    And   I should see "Rule successfully deleted"
+    And   I should not see "New rule course level"
+
+  Scenario: Edit a rule on course level
+    Given I log in as "teacher1"
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    When  I click on "Edit rule" "link"
+    And   I set the following fields to these values:
+      | name              | New rule quiz                                  |
+      | plugin            | Quiz                                           |
+      | eventname         | Quiz attempt deleted                           |
+      | id_description    | I want a rule to monitor quiz attempts deleted |
+      | frequency         | 5                                              |
+      | minutes           | 5                                              |
+      | Message template  | Quiz attempt deleted. {modulelink}             |
+    And   I press "Save changes"
+    Then  I should see "New rule quiz"
+    And   I should see "I want a rule to monitor quiz attempts deleted"
+    And   I should see "Quiz attempt deleted"
+    And   I should see "5 times in 5 minutes"
+
+  Scenario: Duplicate a rule on course level
+    Given I log in as "teacher1"
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    When  I click on "Duplicate rule" "link"
+    Then  I should see "Rule successfully duplicated"
+    And   "#toolmonitorrules_r1" "css_element" should appear before "#toolmonitorrules_r2" "css_element"
+    And   I should see "New rule"
+    And   I should see "I want a rule to monitor posts created on a forum"
+    And   I should see "Forum"
+    And   I should see "Post created"
+    And   I should see "1 times in 1 minutes"
+
+  Scenario: Add a rule on site level
+    Given I log in as "admin"
+    And   I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    When  I press "Add a new rule"
+    And   I set the following fields to these values:
+      | name              | New rule                                          |
+      | plugin            | Forum                                             |
+      | eventname         | Post created                                      |
+      | id_description    | I want a rule to monitor posts created on a forum |
+      | frequency         | 1                                                 |
+      | minutes           | 1                                                 |
+      | Message template  | The forum post was created. {modulelink}          |
+    And   I press "Save changes"
+    Then  I should see "New rule"
+    And   I should see "I want a rule to monitor posts created on a forum"
+    And   I should see "Forum"
+    And   I should see "Post created"
+    And   I should see "1 times in 1 minutes"
+
+  Scenario: Delete a rule on site level
+    Given I log in as "admin"
+    And   I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    When  I click on "Delete rule" "link"
+    Then  I should see "Are you sure you want to delete rule \"New rule site level\"?"
+    And   I press "Yes"
+    And   I should see "Rule successfully deleted"
+    And   I should not see "New rule site level"
+
+  Scenario: Edit a rule on site level
+    Given I log in as "admin"
+    And   I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    When  I click on "Edit rule" "link"
+    And   I set the following fields to these values:
+      | name              | New Rule Quiz                                  |
+      | plugin            | Quiz                                           |
+      | eventname         | Quiz attempt deleted                           |
+      | id_description    | I want a rule to monitor quiz attempts deleted |
+      | frequency         | 5                                              |
+      | minutes           | 5                                              |
+      | Message template  | Quiz attempt deleted. {modulelink}             |
+    And   I press "Save changes"
+    Then  I should see "New Rule Quiz"
+    And   I should see "I want a rule to monitor quiz attempts deleted"
+    And   I should see "Quiz attempt deleted"
+    And   I should see "5 times in 5 minutes"
+
+  Scenario: Duplicate a rule on site level
+    Given I log in as "admin"
+    And   I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    When  I click on "Duplicate rule" "link"
+    Then  I should see "Rule successfully duplicated"
+    And   "#toolmonitorrules_r2" "css_element" should appear after "#toolmonitorrules_r1" "css_element"
+    And   I should see "I want a rule to monitor posts created on a forum"
+    And   I should see "Forum"
+    And   I should see "Post created"
+    And   I should see "1 times in 1 minutes"
diff --git a/admin/tool/monitor/tests/behat/subscription.feature b/admin/tool/monitor/tests/behat/subscription.feature
new file mode 100644 (file)
index 0000000..8d78a0a
--- /dev/null
@@ -0,0 +1,125 @@
+@javascript @tool @tool_monitor @tool_monitor_subscriptions
+Feature: tool_monitor_subscriptions
+  In order to monitor events and receive notifications
+  As an user
+  I need to create a new rule, subscribe to it, receive notification and delete subscription
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And   I log in as "admin"
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    And   I press "Add a new rule"
+    And   I set the following fields to these values:
+      | name              | New rule course level                             |
+      | plugin            | Core                                              |
+      | eventname         | Course viewed                                     |
+      | id_description    | I want a rule to monitor when a course is viewed. |
+      | frequency         | 1                                                 |
+      | minutes           | 1                                                 |
+      | Message template  | The course was viewed. {modulelink}               |
+    And   I press "Save changes"
+    And   I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    And   I press "Add a new rule"
+    And   I set the following fields to these values:
+      | name              | New rule site level                               |
+      | plugin            | Core                                              |
+      | eventname         | Course viewed                                     |
+      | id_description    | I want a rule to monitor when a course is viewed. |
+      | frequency         | 1                                                 |
+      | minutes           | 1                                                 |
+      | Message template  | The course was viewed. {modulelink}               |
+    And  I press "Save changes"
+    And  I log out
+
+  Scenario: Subscribe to a rule on course level
+    Given I log in as "teacher1"
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring" node in "My profile settings"
+    And   I set the field "courseid" to "Course 1"
+    When  I set the field "cmid" to "All events"
+    Then  I should see "Subscription successfully created"
+    And   "#toolmonitorsubs_r0" "css_element" should exist
+
+  Scenario: Delete a subscription on course level
+    Given I log in as "teacher1"
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring" node in "My profile settings"
+    And   I set the field "courseid" to "Course 1"
+    And   I set the field "cmid" to "All events"
+    And   I should see "Subscription successfully created"
+    When  I click on "Delete subscription" "link"
+    And   I should see "Are you sure you want to delete this subscription for the rule \"New rule course level\"?"
+    And   I press "Yes"
+    Then  I should see "Subscription successfully removed"
+    And   "#toolmonitorsubs_r0" "css_element" should not exist
+
+  Scenario: Subscribe to a rule on site level
+    Given I log in as "admin"
+    And   I navigate to "Event monitoring" node in "My profile settings"
+    And   I set the field "courseid" to "Site"
+    When  I set the field "cmid" to "All events"
+    Then  I should see "Subscription successfully created"
+    And   "#toolmonitorsubs_r0" "css_element" should exist
+
+  Scenario: Delete a subscription on site level
+    Given I log in as "admin"
+    And   I navigate to "Event monitoring" node in "My profile settings"
+    And   I set the field "courseid" to "Site"
+    And   I set the field "cmid" to "All events"
+    And   I should see "Subscription successfully created"
+    And   "#toolmonitorsubs_r0" "css_element" should exist
+    When  I click on "Delete subscription" "link"
+    And   I should see "Are you sure you want to delete this subscription for the rule \"New rule site level\"?"
+    And   I press "Yes"
+    Then  I should see "Subscription successfully removed"
+    And   "#toolmonitorsubs_r0" "css_element" should not exist
+
+  Scenario: Receiving notification on site level
+    Given I log in as "admin"
+    And   I navigate to "Messaging" node in "My profile settings"
+    And   I click on "input[name^=tool_monitor_notification_loggedin]" "css_element"
+    And   I press "Update profile"
+    And   I am on homepage
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring" node in "My profile settings"
+    And   I set the field "courseid" to "Site"
+    And   I set the field "cmid" to "All events"
+    And   I should see "Subscription successfully created"
+    And   "#toolmonitorsubs_r0" "css_element" should exist
+    And   I am on homepage
+    And   I trigger cron
+    And   I am on homepage
+    And   I expand "My profile" node
+    When  I follow "Messages"
+    And   I follow "Do not reply to this email (1)"
+    Then  I should see "The course was viewed."
+
+  Scenario: Receiving notification on course level
+    Given I log in as "teacher1"
+    And   I navigate to "Messaging" node in "My profile settings"
+    And   I click on "input[name^=tool_monitor_notification_loggedin]" "css_element"
+    And   I press "Update profile"
+    And   I am on homepage
+    And   I follow "Course 1"
+    And   I navigate to "Event monitoring" node in "My profile settings"
+    And   I set the field "courseid" to "Course 1"
+    And   I set the field "cmid" to "All events"
+    And   I should see "Subscription successfully created"
+    And   "#toolmonitorsubs_r0" "css_element" should exist
+    And   I am on homepage
+    And   I follow "Course 1"
+    And   I trigger cron
+    And   I am on homepage
+    And   I expand "My profile" node
+    When  I follow "Messages"
+    And   I follow "Do not reply to this email (1)"
+    Then  I should see "The course was viewed."
diff --git a/admin/tool/monitor/tests/eventobservers_test.php b/admin/tool/monitor/tests/eventobservers_test.php
new file mode 100644 (file)
index 0000000..b8d178a
--- /dev/null
@@ -0,0 +1,482 @@
+<?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 event observers.
+ *
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/blog/locallib.php');
+require_once($CFG->dirroot . '/blog/lib.php');
+
+/**
+ * Class tool_monitor_eventobservers_testcase
+ *
+ * Tests for event observers
+ */
+class tool_monitor_eventobservers_testcase extends advanced_testcase {
+
+    /**
+     * Test observer for course delete event.
+     */
+    public function test_course_deleted() {
+        global $DB;
+
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        $rule = new stdClass();
+        $rule->userid = $user->id;
+        $rule->courseid = $course->id;
+        $rule->plugin = 'test';
+
+        $sub = new stdClass();
+        $sub->courseid = $course->id;
+        $sub->userid = $user->id;
+
+        // Add 10 rules for this course with subscriptions.
+        for ($i = 0; $i < 10; $i++) {
+            $createdrule = $monitorgenerator->create_rule($rule);
+            $sub->ruleid = $createdrule->id;
+            $monitorgenerator->create_subscription($sub);
+        }
+
+        // Add 10 random rules for random courses.
+        for ($i = 0; $i < 10; $i++) {
+            $rule->courseid = rand(10000000, 50000000);
+            $createdrule = $monitorgenerator->create_rule($rule);
+            $sub->courseid = $rule->courseid;
+            $sub->ruleid = $createdrule->id;
+            $monitorgenerator->create_subscription($sub);
+        }
+
+        // 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);
+        $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);
+        $this->assertCount(10, $coursesubs);
+
+        // Let us delete the course now.
+        delete_course($course->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);
+        $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);
+        $this->assertCount(0, $coursesubs); // Making sure all subscriptions are deleted.
+    }
+
+    /**
+     * This tests if writing of the events to the table tool_monitor_events is working fine.
+     */
+    public function test_flush() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create events and verify data is fine.
+        $course = $this->getDataGenerator()->create_course();
+
+        $initialevents = $DB->get_records('tool_monitor_events');
+        $initalcount = count($initialevents);
+        $event = \mod_book\event\course_module_instance_list_viewed::create_from_course($course);
+        $event->trigger();
+
+        $events = $DB->get_records('tool_monitor_events');
+        $count = count($events);
+        $this->assertEquals($initalcount + 1, $count);
+        $monitorevent = array_pop($events);
+
+        // Match values.
+        $this->assertEquals($event->eventname, $monitorevent->eventname);
+        $this->assertEquals($event->contextid, $monitorevent->contextid);
+        $this->assertEquals($event->contextlevel, $monitorevent->contextlevel);
+        $this->assertEquals($event->get_url()->out(), $monitorevent->link);
+        $this->assertEquals($event->courseid, $monitorevent->courseid);
+        $this->assertEquals($event->timecreated, $monitorevent->timecreated);
+    }
+
+    /**
+     * Test the notification sending features.
+     */
+    public function test_process_event() {
+
+        global $DB, $USER;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        $msgsink = $this->redirectMessages();
+
+        // Generate data.
+        $course = $this->getDataGenerator()->create_course();
+        $toolgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        $rulerecord = new stdClass();
+        $rulerecord->courseid = $course->id;
+        $rulerecord->eventname = '\mod_book\event\course_module_instance_list_viewed';
+        $rulerecord->frequency = 1;
+
+        $rule = $toolgenerator->create_rule($rulerecord);
+
+        $subrecord = new stdClass();
+        $subrecord->courseid = $course->id;
+        $subrecord->ruleid = $rule->id;
+        $subrecord->userid = $USER->id;
+        $toolgenerator->create_subscription($subrecord);
+
+        $recordexists = $DB->record_exists('task_adhoc', array('component' => 'tool_monitor'));
+        $this->assertFalse($recordexists);
+
+        // Now let us trigger the event.
+        $event = \mod_book\event\course_module_instance_list_viewed::create_from_course($course);
+        $event->trigger();
+
+        $this->verify_processed_data($msgsink);
+
+        // Clean up.
+        \tool_monitor\rule_manager::delete_rule($rule->id);
+        $DB->delete_records('tool_monitor_events');
+
+        // Let us create a rule with more than 1 frequency.
+        $rulerecord->frequency = 5;
+        $rule = $toolgenerator->create_rule($rulerecord);
+        $subrecord->ruleid = $rule->id;
+        $toolgenerator->create_subscription($subrecord);
+
+        // Let us trigger events.
+        for ($i = 0; $i < 5; $i++) {
+            $event = \mod_book\event\course_module_instance_list_viewed::create_from_course($course);
+            $event->trigger();
+            if ($i != 4) {
+                $this->verify_message_not_sent_yet($msgsink);
+            }
+        }
+
+        $this->verify_processed_data($msgsink);
+
+        // Clean up.
+        \tool_monitor\rule_manager::delete_rule($rule->id);
+        $DB->delete_records('tool_monitor_events');
+
+        // Now let us create a rule specific to a module instance.
+        $cm = new stdClass();
+        $cm->course = $course->id;
+        $book = $this->getDataGenerator()->create_module('book', $cm);
+        $rulerecord->eventname = '\mod_book\event\course_module_viewed';
+        $rulerecord->cmid = $book->cmid;
+        $rule = $toolgenerator->create_rule($rulerecord);
+        $subrecord->ruleid = $rule->id;
+        $toolgenerator->create_subscription($subrecord);
+
+        // Let us trigger events.
+        $params = array(
+            'context' => context_module::instance($book->cmid),
+            'objectid' => $book->id
+        );
+        for ($i = 0; $i < 5; $i++) {
+            $event = \mod_book\event\course_module_viewed::create($params);
+            $event->trigger();
+            if ($i != 4) {
+                $this->verify_message_not_sent_yet($msgsink);
+            }
+        }
+
+        $this->verify_processed_data($msgsink);
+
+        // Clean up.
+        \tool_monitor\rule_manager::delete_rule($rule->id);
+        $DB->delete_records('tool_monitor_events');
+
+        // Now let us create a rule for event that happens in category context events.
+        $rulerecord->eventname = '\core\event\course_category_created';
+        $rulerecord->courseid = 0;
+        $rule = $toolgenerator->create_rule($rulerecord);
+        $subrecord->courseid = 0;
+        $subrecord->ruleid = $rule->id;
+        $toolgenerator->create_subscription($subrecord);
+
+        // Let us trigger events.
+        for ($i = 0; $i < 5; $i++) {
+            $this->getDataGenerator()->create_category();
+            if ($i != 4) {
+                $this->verify_message_not_sent_yet($msgsink);
+            }
+        }
+        $this->verify_processed_data($msgsink);
+
+        // Clean up.
+        \tool_monitor\rule_manager::delete_rule($rule->id);
+        $DB->delete_records('tool_monitor_events');
+
+        // Now let us create a rule at site level.
+        $rulerecord->eventname = '\core\event\blog_entry_created';
+        $rulerecord->courseid = 0;
+        $rule = $toolgenerator->create_rule($rulerecord);
+        $subrecord->courseid = 0;
+        $subrecord->ruleid = $rule->id;
+        $toolgenerator->create_subscription($subrecord);
+
+        // Let us trigger events.
+        $blog = new blog_entry();
+        $blog->subject = "Subject of blog";
+        $blog->userid = $USER->id;
+        $states = blog_entry::get_applicable_publish_states();
+        $blog->publishstate = reset($states);
+        for ($i = 0; $i < 5; $i++) {
+            $newblog = fullclone($blog);
+            $newblog->add();
+            if ($i != 4) {
+                $this->verify_message_not_sent_yet($msgsink);
+            }
+        }
+
+        $this->verify_processed_data($msgsink);
+    }
+
+    /**
+     * Run adhoc tasks.
+     */
+    protected function run_adhock_tasks() {
+        while ($task = \core\task\manager::get_next_adhoc_task(time())) {
+            $task->execute();
+            \core\task\manager::adhoc_task_complete($task);
+        }
+    }
+
+    /**
+     * Verify that task was scheduled and a message was sent as expected.
+     *
+     * @param phpunit_message_sink $msgsink Message sink
+     */
+    protected function verify_processed_data(phpunit_message_sink $msgsink) {
+        global $DB, $USER;
+
+        $recordexists = $DB->count_records('task_adhoc', array('component' => 'tool_monitor'));
+        $this->assertEquals(1, $recordexists); // We should have an adhock task now to send notifications.
+        $this->run_adhock_tasks();
+        $this->assertEquals(1, $msgsink->count());
+        $msgs = $msgsink->get_messages();
+        $msg = array_pop($msgs);
+        $this->assertEquals($USER->id, $msg->useridto);
+        $this->assertEquals(1, $msg->notification);
+        $msgsink->clear();
+    }
+
+    /**
+     * Verify that a message was not sent.
+     *
+     * @param phpunit_message_sink $msgsink Message sink
+     */
+    protected function verify_message_not_sent_yet(phpunit_message_sink $msgsink) {
+        $msgs = $msgsink->get_messages();
+        $this->assertCount(0, $msgs);
+        $msgsink->clear();
+    }
+
+    /**
+     * Tests for replace_placeholders method.
+     */
+    public function test_replace_placeholders() {
+        global $USER;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        $msgsink = $this->redirectMessages();
+
+        // Generate data.
+        $course = $this->getDataGenerator()->create_course();
+        $toolgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $context = \context_user::instance($USER->id, IGNORE_MISSING);
+
+        // Creating book.
+        $cm = new stdClass();
+        $cm->course = $course->id;
+        $book = $this->getDataGenerator()->create_module('book', $cm);
+
+        // Creating rule.
+        $rulerecord = new stdClass();
+        $rulerecord->courseid = $course->id;
+        $rulerecord->eventname = '\mod_book\event\course_module_viewed';
+        $rulerecord->cmid = $book->cmid;
+        $rulerecord->frequency = 1;
+        $rulerecord->template = '{link} {modulelink} {rulename} {description} {eventname}';
+
+        $rule = $toolgenerator->create_rule($rulerecord);
+
+        // Creating subscription.
+        $subrecord = new stdClass();
+        $subrecord->courseid = $course->id;
+        $subrecord->ruleid = $rule->id;
+        $subrecord->userid = $USER->id;
+        $toolgenerator->create_subscription($subrecord);
+
+        // Now let us trigger the event.
+        $params = array(
+            'context' => context_module::instance($book->cmid),
+            'objectid' => $book->id
+        );
+
+        $event = \mod_book\event\course_module_viewed::create($params);
+        $event->trigger();
+        $this->run_adhock_tasks();
+        $msgs = $msgsink->get_messages();
+        $msg = array_pop($msgs);
+
+        $modurl = new moodle_url('/mod/book/view.php', array('id' => $book->cmid));
+        $expectedmsg = $event->get_url()->out() . ' ' .
+                        $modurl->out()  . ' ' .
+                        $rule->get_name($context) . ' ' .
+                        $rule->get_description($context) . ' ' .
+                        $rule->get_event_name();
+
+        $this->assertEquals($expectedmsg, $msg->fullmessage);
+    }
+
+    /**
+     * Test observer for user delete event.
+     */
+    public function test_user_deleted() {
+        global $DB;
+
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        $rule = new stdClass();
+        $rule->userid = $user->id;
+        $rule->courseid = $course->id;
+        $rule->plugin = 'test';
+
+        $sub = new stdClass();
+        $sub->courseid = $course->id;
+        $sub->userid = $user->id;
+
+        // Add 10 rules for this course with subscriptions.
+        for ($i = 0; $i < 10; $i++) {
+            $createdrule = $monitorgenerator->create_rule($rule);
+            $sub->ruleid = $createdrule->id;
+            $monitorgenerator->create_subscription($sub);
+        }
+
+        // Add 10 random rules for random courses.
+        for ($i = 0; $i < 10; $i++) {
+            $rule->courseid = rand(10000000, 50000000);
+            $createdrule = $monitorgenerator->create_rule($rule);
+            $sub->courseid = $rule->courseid;
+            $sub->ruleid = $createdrule->id;
+            $monitorgenerator->create_subscription($sub);
+        }
+
+        // Verify data before user delete.
+        $totalrules = \tool_monitor\rule_manager::get_rules_by_plugin('test');
+        $this->assertCount(20, $totalrules);
+        $totalsubs = $DB->get_records('tool_monitor_subscriptions');
+        $this->assertCount(20, $totalsubs);
+
+        // Let us delete the user now.
+        delete_user($user);
+
+        // Verify data after course delete.
+        $totalrules = \tool_monitor\rule_manager::get_rules_by_plugin('test');
+        $this->assertCount(20, $totalrules);
+        $totalsubs = $DB->get_records('tool_monitor_subscriptions');
+        $this->assertCount(0, $totalsubs); // Make sure all subscriptions are deleted.
+    }
+
+    /**
+     * Test observer for course module delete event.
+     */
+    public function test_course_module_deleted() {
+        global $DB;
+
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $course = $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;
+        $book = $this->getDataGenerator()->create_module('book', $cm);
+
+        $rule = new stdClass();
+        $rule->userid = $user->id;
+        $rule->courseid = $course->id;
+        $rule->plugin = 'test';
+
+        $sub = new stdClass();
+        $sub->courseid = $course->id;
+        $sub->userid = $user->id;
+        $sub->cmid = $book->cmid;
+
+        // Add 10 rules for this course with subscriptions for this module.
+        for ($i = 0; $i < 10; $i++) {
+            $createdrule = $monitorgenerator->create_rule($rule);
+            $sub->ruleid = $createdrule->id;
+            $monitorgenerator->create_subscription($sub);
+        }
+
+        // Add 10 random rules for random courses.
+        for ($i = 0; $i < 10; $i++) {
+            $rule->courseid = rand(10000000, 50000000);
+            $createdrule = $monitorgenerator->create_rule($rule);
+            $sub->courseid = $rule->courseid;
+            $sub->ruleid = $createdrule->id;
+            $sub->cmid = 0;
+            $monitorgenerator->create_subscription($sub);
+        }
+
+        // Verify data before module delete.
+        $totalrules = \tool_monitor\rule_manager::get_rules_by_plugin('test');
+        $this->assertCount(20, $totalrules);
+        $totalsubs = $DB->get_records('tool_monitor_subscriptions');
+        $this->assertCount(20, $totalsubs);
+
+        // Let us delete the user now.
+        course_delete_module($book->cmid);
+
+        // Verify data after course delete.
+        $totalrules = \tool_monitor\rule_manager::get_rules_by_plugin('test');
+        $this->assertCount(20, $totalrules);
+        $totalsubs = $DB->get_records('tool_monitor_subscriptions');
+        $this->assertCount(10, $totalsubs); // Make sure only relevant subscriptions are deleted.
+    }
+
+}
diff --git a/admin/tool/monitor/tests/generator/lib.php b/admin/tool/monitor/tests/generator/lib.php
new file mode 100644 (file)
index 0000000..26c973d
--- /dev/null
@@ -0,0 +1,200 @@
+<?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/>.
+
+/**
+ * Event monitor data generator
+ *
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2014 onwards Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event monitor data generator class
+ *
+ * @since       Moodle 2.8
+ * @package     tool_monitor
+ * @category    test
+ * @copyright   2014 onwards Simey Lameze <simey@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_monitor_generator extends testing_module_generator {
+
+    /**
+     * @var int keep track of how many rules have been created.
+     */
+    protected $rulecount;
+
+    /**
+     * Function to generate rule data.
+     *
+     * @param \stdClass|array $record data to insert as rule entry.
+     *
+     * @return \tool_monitor\rule An instance of rule class.
+     */
+    public function create_rule($record = null) {
+        global $USER;
+
+        $this->rulecount++;
+        $i = $this->rulecount;
+        $now = time();
+        $record = (object)(array)$record;
+
+        if (!isset($record->userid)) {
+            $record->userid = $USER->id;
+        }
+        if (!isset($record->courseid)) {
+            $record->courseid = 0;
+        }
+        if (!isset($record->name)) {
+            $record->name = 'Test rule ' . $i;
+        }
+        if (!isset($record->description)) {
+            $record->description = 'Rule description ' . $i;
+        }
+        if (!isset($record->descriptionformat)) {
+            $record->descriptionformat = FORMAT_HTML;
+        }
+        if (!isset($record->frequency)) {
+            $record->frequency = 5;
+        }
+        if (!isset($record->minutes)) {
+            $record->minutes = 5;
+        }
+        if (!isset($record->template)) {
+            $record->template = 'Rule message template ' . $i;
+        }
+        if (!isset($record->templateformat)) {
+            $record->templateformat = FORMAT_HTML;
+        }
+        if (!isset($record->timewindow)) {
+            $record->timewindow = $record->minutes * 60;
+        }
+        if (!isset($record->timecreated)) {
+            $record->timecreated = $now;
+        }
+        if (!isset($record->timemodified)) {
+            $record->timemodified = $now;
+        }
+        if (!isset($record->plugin)) {
+            $record->plugin = 'core';
+        }
+        if (!isset($record->eventname)) {
+            $record->eventname = '\core\event\blog_entry_created';
+        }
+
+        unset($record->minutes); // Remove the minutes shortcut to the timewindow.
+        return \tool_monitor\rule_manager::add_rule($record);
+    }
+
+    /**
+     * Function to generate subscription data.
+     *
+     * @throws coding_exception if $record->ruleid or $record->userid not present.
+     * @param \stdClass|array $record data to insert as subscription entry.
+     *
+     * @return \tool_monitor\subscription An instance of the subscription class.
+     */
+    public function create_subscription($record = null) {
+
+        if (!isset($record->timecreated)) {
+            $record->timecreated = time();
+        }
+        if (!isset($record->courseid)) {
+            $record->courseid = 0;
+        }
+        if (!isset($record->ruleid)) {
+            throw new coding_exception('$record->ruleid must be present in tool_monitor_generator::create_subscription()');
+        }
+        if (!isset($record->cmid)) {
+            $record->cmid = 0;
+        }
+        if (!isset($record->userid)) {
+            throw new coding_exception('$record->userid must be present in tool_monitor_generator::create_subscription()');
+        }
+
+        $sid = \tool_monitor\subscription_manager::create_subscription($record->ruleid, $record->courseid,
+                $record->cmid, $record->userid);
+        return \tool_monitor\subscription_manager::get_subscription($sid);
+    }
+
+    /**
+     * Function to generate event entries.
+     *
+     * @param \stdClass|array $record data to insert as event entry.
+     *
+     * @return \stdClass $record An object representing the newly created event entry.
+     */
+    public function create_event_entries($record = null) {
+        global $DB, $CFG;
+
+        $record = (object)(array)$record;
+        $context = \context_system::instance();
+
+        if (!isset($record->eventname)) {
+            $record->eventname = '\core\event\user_loggedin';
+        }
+        if (!isset($record->contextid)) {
+            $record->contextid = $context->id;
+        }
+        if (!isset($record->contextlevel)) {
+            $record->contextlevel = $context->contextlevel;
+        }
+        if (!isset($record->contextinstanceid)) {
+            $record->contextinstanceid = $context->instanceid;
+        }
+        if (!isset($record->link)) {
+            $record->link = $CFG->wwwroot . '/user/profile.php';
+        }
+        if (!isset($record->courseid)) {
+            $record->courseid = 0;
+        }
+        if (!isset($record->timecreated)) {
+            $record->timecreated = time();
+        }
+        $record->id = $DB->insert_record('tool_monitor_events', $record, true);
+
+        return $record;
+    }
+
+    /**
+     * Function to generate history data.
+     *
+     * @throws coding_exception if $record->sid or $record->userid not present.
+     * @param \stdClass|array $record data to insert as history entry.
+     *
+     * @return \stdClass $record An object representing the newly created history entry.
+     */
+    public function create_history($record = null) {
+        global $DB;
+        $record = (object)(array)$record;
+        if (!isset($record->sid)) {
+            throw new coding_exception('subscription ID must be present in tool_monitor_generator::create_history() $record');
+        }
+        if (!isset($record->userid)) {
+            throw new coding_exception('user ID must be present in tool_monitor_generator::create_history() $record');
+        }
+        if (!isset($record->timesent)) {
+            $record->timesent = time();
+        }
+        $record->id = $DB->insert_record('tool_monitor_history', $record, true);
+
+        return $record;
+    }
+}
diff --git a/admin/tool/monitor/tests/generator_test.php b/admin/tool/monitor/tests/generator_test.php
new file mode 100644 (file)
index 0000000..7e6403e
--- /dev/null
@@ -0,0 +1,133 @@
+<?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/>.
+
+/**
+ * PHPUnit data generator tests.
+ *
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2014 onwards Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * PHPUnit data generator test case.
+ *
+ * @since      Moodle 2.8
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2014 onwards Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_monitor_generator_testcase extends advanced_testcase {
+
+    /**
+     * Test create_rule data generator.
+     */
+    public function test_create_rule() {
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_user();
+
+        $rulegenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $record->userid = $user->id;
+
+        $rule = $rulegenerator->create_rule($record);
+        $this->assertInstanceOf('tool_monitor\rule', $rule);
+        $this->assertEquals($rule->userid, $record->userid);
+        $this->assertEquals($rule->courseid, $record->courseid);
+    }
+
+    /**
+     * Test create_subscription data generator.
+     */
+    public function test_create_subscription() {
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $rule = $monitorgenerator->create_rule();
+
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $record->userid = $user->id;
+        $record->ruleid = $rule->id;
+
+        $subscription = $monitorgenerator->create_subscription($record);
+        $this->assertEquals($record->courseid, $subscription->courseid);
+        $this->assertEquals($record->ruleid, $subscription->ruleid);
+        $this->assertEquals($record->userid, $subscription->userid);
+        $this->assertEquals(0, $subscription->cmid);
+
+        // Make sure rule id is always required.
+        $this->setExpectedException('coding_exception');
+        unset($record->ruleid);
+        $monitorgenerator->create_subscription($record);
+    }
+
+    /**
+     * Test create_event data generator.
+     */
+    public function test_create_event_entries() {
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+        $context = \context_system::instance();
+
+        // Default data generator values.
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        // First create and assertdata using default values.
+        $eventdata = $monitorgenerator->create_event_entries();
+        $this->assertEquals('\core\event\user_loggedin', $eventdata->eventname);
+        $this->assertEquals($context->id, $eventdata->contextid);
+        $this->assertEquals($context->contextlevel, $eventdata->contextlevel);
+    }
+
+    /**
+     * Test create_history data generator.
+     */
+    public function test_create_history() {
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+        $user = $this->getDataGenerator()->create_user();
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $rule = $monitorgenerator->create_rule();
+
+        $record = new \stdClass();
+        $record->userid = $user->id;
+        $record->ruleid = $rule->id;
+        $sid = $monitorgenerator->create_subscription($record)->id;
+        $record->sid = $sid;
+        $historydata = $monitorgenerator->create_history($record);
+        $this->assertEquals($record->userid, $historydata->userid);
+        $this->assertEquals($record->sid, $historydata->sid);
+
+        // Test using default values.
+        $record->userid = 1;
+        $record->sid = 1;
+        $historydata = $monitorgenerator->create_history($record);
+        $this->assertEquals(1, $historydata->userid);
+        $this->assertEquals(1, $historydata->sid);
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/monitor/tests/rule_manager_test.php b/admin/tool/monitor/tests/rule_manager_test.php
new file mode 100644 (file)
index 0000000..f9e05dc
--- /dev/null
@@ -0,0 +1,184 @@
+<?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 rule manager api.
+ *
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2014 onwards Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Tests for rule manager.
+ *
+ * Class tool_monitor_rule_manager_testcase
+ */
+class tool_monitor_rule_manager_testcase extends advanced_testcase {
+
+    /**
+     * Test add_rule method.
+     */
+    public function test_add_rule() {
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $now = time();
+
+        $rule = new stdClass();
+        $rule->userid = $user->id;
+        $rule->courseid = $course->id;
+        $rule->name = 'test rule 1';
+        $rule->plugin = 'core';
+        $rule->eventname = '\core\event\course_updated';
+        $rule->description = 'test description 1';
+        $rule->descriptionformat = FORMAT_HTML;
+        $rule->frequency = 15;
+        $rule->template = 'test template message';
+        $rule->templateformat = FORMAT_HTML;
+        $rule->timewindow = 300;
+        $rule->timecreated = $now;
+        $rule->timemodified = $now;
+
+        $ruledata = \tool_monitor\rule_manager::add_rule($rule);
+        foreach ($rule as $prop => $value) {
+            $this->assertEquals($ruledata->$prop, $value);
+        }
+    }
+
+    /**
+     * Test get_rule method.
+     */
+    public function test_get_rule() {
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $rule = $monitorgenerator->create_rule();
+        $rules1 = \tool_monitor\rule_manager::get_rule($rule->id);
+        $this->assertInstanceOf('tool_monitor\rule', $rules1);
+        $this->assertEquals($rules1, $rule);
+    }
+
+    /**
+     * Test update_rule method.
+     */
+    public function test_update_rule() {
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $rule = $monitorgenerator->create_rule();
+
+        $ruledata = new stdClass;
+        $ruledata->id = $rule->id;
+        $ruledata->frequency = 25;
+
+        \tool_monitor\rule_manager::update_rule($ruledata);
+        $this->assertEquals(25, $ruledata->frequency);
+
+    }
+
+    /**
+     * Test get_rules_by_courseid method.
+     */
+    public function test_get_rules_by_courseid() {
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        $record = new stdClass();
+        $record->courseid = 3;
+
+        $record2 = new stdClass();
+        $record2->courseid = 4;
+
+        $ruleids = array();
+        for ($i = 0; $i < 10; $i++) {
+            $rule = $monitorgenerator->create_rule($record);
+            $ruleids[] = $rule->id;
+            $rule = $monitorgenerator->create_rule(); // Create some site level rules.
+            $ruleids[] = $rule->id;
+            $rule = $monitorgenerator->create_rule($record2); // Create rules in a different course.
+        }
+        $ruledata = \tool_monitor\rule_manager::get_rules_by_courseid(3);
+        $this->assertEquals($ruleids, array_keys($ruledata));
+        $this->assertCount(20, $ruledata);
+    }
+
+    /**
+     * Test get_rules_by_plugin method.
+     */
+    public function test_get_rules_by_plugin() {
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        $record = new stdClass();
+        $record->plugin = 'core';
+
+        $record2 = new stdClass();
+        $record2->plugin = 'mod_assign';
+
+        $ruleids = array();
+        for ($i = 0; $i < 10; $i++) {
+            $rule = $monitorgenerator->create_rule($record);
+            $ruleids[] = $rule->id;
+            $rule = $monitorgenerator->create_rule($record2); // Create rules in a different plugin.
+        }
+
+        $ruledata = \tool_monitor\rule_manager::get_rules_by_plugin('core');
+        $this->assertEquals($ruleids, array_keys($ruledata));
+        $this->assertCount(10, $ruledata);
+    }
+
+    /**
+     * Test get_rules_by_event method.
+     */
+    public function test_get_rules_by_event() {
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $rule = $monitorgenerator->create_rule();
+
+        $record = new stdClass();
+        $record->eventname = '\core\event\calendar_event_created';
+
+        $record2 = new stdClass();
+        $record2->eventname = '\core\event\calendar_event_updated';
+
+        $ruleids = array();
+        for ($i = 0; $i < 10; $i++) {
+            $rule = $monitorgenerator->create_rule($record);
+            $ruleids[] = $rule->id;
+            $rule = $monitorgenerator->create_rule($record2); // Create rules in a different plugin.
+        }
+
+        $ruledata = \tool_monitor\rule_manager::get_rules_by_event('\core\event\calendar_event_created');
+        $this->assertEmpty(array_diff(array_keys($ruledata), $ruleids));
+        $this->assertCount(10, $ruledata);
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/monitor/version.php b/admin/tool/monitor/version.php
new file mode 100644 (file)
index 0000000..dba3696
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Version info
+ *
+ * This file contains version information about tool_monitor.
+ *
+ * @package    tool_monitor
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$plugin->version   = 2014061900;       // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2014061900;       // Requires this Moodle version.
+$plugin->component = 'tool_monitor'; // Full name of the plugin (used for diagnostics).
diff --git a/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-debug.js b/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-debug.js
new file mode 100644 (file)
index 0000000..79184dd
Binary files /dev/null and b/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-debug.js differ
diff --git a/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-min.js b/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-min.js
new file mode 100644 (file)
index 0000000..c62a013
Binary files /dev/null and b/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-min.js differ
diff --git a/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown.js b/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown.js
new file mode 100644 (file)
index 0000000..79184dd
Binary files /dev/null and b/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown.js differ
diff --git a/admin/tool/monitor/yui/src/dropdown/build.json b/admin/tool/monitor/yui/src/dropdown/build.json
new file mode 100644 (file)
index 0000000..66630f6
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "name": "moodle-tool_monitor-dropdown",
+    "builds": {
+        "moodle-tool_monitor-dropdown": {
+            "jsfiles": [
+                "dropdown.js"
+            ]
+        }
+    }
+}
diff --git a/admin/tool/monitor/yui/src/dropdown/js/dropdown.js b/admin/tool/monitor/yui/src/dropdown/js/dropdown.js
new file mode 100644 (file)
index 0000000..ff839b6
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ * A module to manage dropdowns on the rule add/edit form.
+ *
+ * @module moodle-tool_monitor-dropdown
+ */
+
+/**
+ * A module to manage dependent selects on the edit page.
+ *
+ * @since Moodle 2.8
+ * @class moodle-tool_monitor.dropdown
+ * @extends Base
+ * @constructor
+ */
+function DropDown() {
+    DropDown.superclass.constructor.apply(this, arguments);
+}
+
+
+var SELECTORS = {
+        PLUGIN: '#id_plugin',
+        EVENTNAME: '#id_eventname',
+        OPTION: 'option',
+        CHOOSE: 'option[value=""]'
+    };
+
+Y.extend(DropDown, Y.Base, {
+
+    /**
+     * Reference to the plugin node.
+     *
+     * @property plugin
+     * @type Object
+     * @default null
+     * @protected
+     */
+    plugin: null,
+
+    /**
+     * Reference to the plugin node.
+     *
+     * @property eventname
+     * @type Object
+     * @default null
+     * @protected
+     */
+    eventname: null,
+
+    /**
+     * Initializer.
+     * Basic setup and delegations.
+     *
+     * @method initializer
+     */
+    initializer: function() {
+        this.plugin = Y.one(SELECTORS.PLUGIN);
+        this.eventname = Y.one(SELECTORS.EVENTNAME);
+        this.plugin.on('change', this.updateEventsList, this);
+    },
+
+    /**
+     * Method to update the events list drop down when plugin list drop down is changed.
+     *
+     * @method updateEventsList
+     */
+    updateEventsList: function() {
+        var node, options, choosenode;
+        var plugin = this.plugin.get('value'); // Get component name.
+        var namespace = '\\' + plugin + '\\';
+        this.eventname.all(SELECTORS.OPTION).remove(true); // Delete all nodes.
+        options = this.get('eventlist');
+
+        // Mark the default choose node as visible and selected.
+        choosenode = Y.Node.create('<option value="">' + options[''] + '</option>');
+        choosenode.set('selected', 'selected');
+        this.eventname.appendChild(choosenode);
+
+        Y.Object.each(options, function(value, key) {
+            // Make sure we highlight only nodes with correct namespace.
+            if (key.substring(0, namespace.length) === namespace) {
+                node = Y.Node.create('<option value="' + key + '">' + value + '</option>');
+                this.eventname.appendChild(node);
+            }
+        }, this);
+
+    }
+}, {
+    NAME: 'dropDown',
+    ATTRS: {
+        /**
+         * A list of events with components.
+         *
+         * @attribute eventlist
+         * @default null
+         * @type Object
+         */
+        eventlist: null
+    }
+});
+
+Y.namespace('M.tool_monitor.DropDown').init = function(config) {
+    return new DropDown(config);
+};
diff --git a/admin/tool/monitor/yui/src/dropdown/meta/dropdown.json b/admin/tool/monitor/yui/src/dropdown/meta/dropdown.json
new file mode 100644 (file)
index 0000000..34fa63f
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "moodle-tool_monitor-dropdown": {
+    "requires": [
+        "base",
+        "event",
+        "node"
+    ]
+  }
+}
index 3be12fd..3b3ec4d 100644 (file)
@@ -49,20 +49,25 @@ function xmldb_block_badges_upgrade($oldversion, $block) {
         // Add this block the default blocks on /my.
         $blockname = 'badges';
 
-        $page = new moodle_page();
-        $page->set_context(context_system::instance());
+        // Do not try to add the block if we cannot find the default my_pages entry.
+        // Private => 1 refers to MY_PAGE_PRIVATE.
+        if ($systempage = $DB->get_record('my_pages', array('userid' => null, 'private' => 1))) {
+            $page = new moodle_page();
+            $page->set_context(context_system::instance());
 
-        // Check to see if this block is already on the default /my page.
-        $criteria = array(
-            'blockname' => $blockname,
-            'parentcontextid' => $page->context->id,
-            'pagetypepattern' => 'my-index'
-        );
+            // Check to see if this block is already on the default /my page.
+            $criteria = array(
+                'blockname' => $blockname,
+                'parentcontextid' => $page->context->id,
+                'pagetypepattern' => 'my-index',
+                'subpagepattern' => $systempage->id,
+            );
 
-        if (!$DB->record_exists('block_instances', $criteria)) {
-            // Add the block to the default /my.
-            $page->blocks->add_region(BLOCK_POS_RIGHT);
-            $page->blocks->add_block($blockname, BLOCK_POS_RIGHT, 0, false, 'my-index');
+            if (!$DB->record_exists('block_instances', $criteria)) {
+                // Add the block to the default /my.
+                $page->blocks->add_region(BLOCK_POS_RIGHT);
+                $page->blocks->add_block($blockname, BLOCK_POS_RIGHT, 0, false, 'my-index', $systempage->id);
+            }
         }
 
         upgrade_block_savepoint(true, 2014062600, $blockname);
index d4ae9b1..a03a0e1 100644 (file)
@@ -49,20 +49,25 @@ function xmldb_block_calendar_month_upgrade($oldversion, $block) {
         // Add this block the default blocks on /my.
         $blockname = 'calendar_month';
 
-        $page = new moodle_page();
-        $page->set_context(context_system::instance());
+        // Do not try to add the block if we cannot find the default my_pages entry.
+        // Private => 1 refers to MY_PAGE_PRIVATE.
+        if ($systempage = $DB->get_record('my_pages', array('userid' => null, 'private' => 1))) {
+            $page = new moodle_page();
+            $page->set_context(context_system::instance());
 
-        // Check to see if this block is already on the default /my page.
-        $criteria = array(
-            'blockname' => $blockname,
-            'parentcontextid' => $page->context->id,
-            'pagetypepattern' => 'my-index'
-        );
+            // Check to see if this block is already on the default /my page.
+            $criteria = array(
+                'blockname' => $blockname,
+                'parentcontextid' => $page->context->id,
+                'pagetypepattern' => 'my-index',
+                'subpagepattern' => $systempage->id,
+            );
 
-        if (!$DB->record_exists('block_instances', $criteria)) {
-            // Add the block to the default /my.
-            $page->blocks->add_region(BLOCK_POS_RIGHT);
-            $page->blocks->add_block($blockname, BLOCK_POS_RIGHT, 0, false, 'my-index');
+            if (!$DB->record_exists('block_instances', $criteria)) {
+                // Add the block to the default /my.
+                $page->blocks->add_region(BLOCK_POS_RIGHT);
+                $page->blocks->add_block($blockname, BLOCK_POS_RIGHT, 0, false, 'my-index', $systempage->id);
+            }
         }
 
         upgrade_block_savepoint(true, 2014062600, $blockname);
index 947d37c..ecce6e4 100644 (file)
@@ -49,20 +49,25 @@ function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) {
         // Add this block the default blocks on /my.
         $blockname = 'calendar_upcoming';
 
-        $page = new moodle_page();
-        $page->set_context(context_system::instance());
+        // Do not try to add the block if we cannot find the default my_pages entry.
+        // Private => 1 refers to MY_PAGE_PRIVATE.
+        if ($systempage = $DB->get_record('my_pages', array('userid' => null, 'private' => 1))) {
+            $page = new moodle_page();
+            $page->set_context(context_system::instance());
 
-        // Check to see if this block is already on the default /my page.
-        $criteria = array(
-            'blockname' => $blockname,
-            'parentcontextid' => $page->context->id,
-            'pagetypepattern' => 'my-index'
-        );
+            // Check to see if this block is already on the default /my page.
+            $criteria = array(
+                'blockname' => $blockname,
+                'parentcontextid' => $page->context->id,
+                'pagetypepattern' => 'my-index',
+                'subpagepattern' => $systempage->id,
+            );
 
-        if (!$DB->record_exists('block_instances', $criteria)) {
-            // Add the block to the default /my.
-            $page->blocks->add_region(BLOCK_POS_RIGHT);
-            $page->blocks->add_block($blockname, BLOCK_POS_RIGHT, 0, false, 'my-index');
+            if (!$DB->record_exists('block_instances', $criteria)) {
+                // Add the block to the default /my.
+                $page->blocks->add_region(BLOCK_POS_RIGHT);
+                $page->blocks->add_block($blockname, BLOCK_POS_RIGHT, 0, false, 'my-index', $systempage->id);
+            }
         }
 
         upgrade_block_savepoint(true, 2014062600, $blockname);
index 4cedf01..9179ee5 100644 (file)
@@ -105,6 +105,12 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      */
     protected $setconnections = array();
 
+    /**
+     * If true data going in and out will be encoded.
+     * @var bool
+     */
+    protected $encode = true;
+
     /**
      * Default prefix for key names.
      * @var string
@@ -202,6 +208,24 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
         }
         $this->definition = $definition;
         $this->isinitialised = true;
+        $this->encode = self::require_encoding();
+    }
+
+    /**
+     * Tests if encoding is going to be required.
+     *
+     * Prior to memcache 3.0.3 scalar data types were not preserved.
+     * For earlier versions of the memcache extension we need to encode and decode scalar types
+     * to ensure that it is preserved.
+     *
+     * @param string $version The version to check, if null it is fetched from PHP.
+     * @return bool
+     */
+    public static function require_encoding($version = null) {
+        if (!$version) {
+            $version = phpversion('memcache');
+        }
+        return (version_compare($version, '3.0.3', '<'));
     }
 
     /**
@@ -292,7 +316,11 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return mixed The data that was associated with the key, or false if the key did not exist.
      */
     public function get($key) {
-        return $this->connection->get($this->parse_key($key));
+        $result = $this->connection->get($this->parse_key($key));
+        if ($this->encode && $result !== false) {
+            return @unserialize($result);
+        }
+        return $result;
     }
 
     /**
@@ -319,6 +347,9 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
                 $return[$key] = false;
             } else {
                 $return[$key] = $result[$mkey];
+                if ($this->encode && $return[$key] !== false) {
+                    $return[$key] = @unserialize($return[$key]);
+                }
             }
         }
         return $return;
@@ -332,6 +363,11 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return bool True if the operation was a success false otherwise.
      */
     public function set($key, $data) {
+        if ($this->encode) {
+            // We must serialise this data.
+            $data = serialize($data);
+        }
+
         if ($this->clustered) {
             $status = true;
             foreach ($this->setconnections as $connection) {
index 63d366c..0cba975 100644 (file)
@@ -264,4 +264,30 @@ class cachestore_memcache_test extends cachestore_tests {
             }
         }
     }
+
+    /**
+     * Test our checks for encoding.
+     */
+    public function test_require_encoding() {
+        $this->assertTrue(cachestore_memcache::require_encoding('dev'));
+        $this->assertTrue(cachestore_memcache::require_encoding('1.0'));
+        $this->assertTrue(cachestore_memcache::require_encoding('1.0.0'));
+        $this->assertTrue(cachestore_memcache::require_encoding('2.0'));
+        $this->assertTrue(cachestore_memcache::require_encoding('2.0.8'));
+        $this->assertTrue(cachestore_memcache::require_encoding('2.2.8'));
+        $this->assertTrue(cachestore_memcache::require_encoding('3.0'));
+        $this->assertTrue(cachestore_memcache::require_encoding('3.0-dev'));
+        $this->assertTrue(cachestore_memcache::require_encoding('3.0.0'));
+        $this->assertTrue(cachestore_memcache::require_encoding('3.0.1'));
+        $this->assertTrue(cachestore_memcache::require_encoding('3.0.2-dev'));
+        $this->assertTrue(cachestore_memcache::require_encoding('3.0.2'));
+        $this->assertTrue(cachestore_memcache::require_encoding('3.0.3-dev'));
+        $this->assertFalse(cachestore_memcache::require_encoding('3.0.3'));
+        $this->assertFalse(cachestore_memcache::require_encoding('3.0.4'));
+        $this->assertFalse(cachestore_memcache::require_encoding('3.0.4-dev'));
+        $this->assertFalse(cachestore_memcache::require_encoding('3.0.8'));
+        $this->assertFalse(cachestore_memcache::require_encoding('3.1.0'));
+        $this->assertFalse(cachestore_memcache::require_encoding('3.1.2'));
+
+    }
 }
index ee5f373..8d4f11c 100644 (file)
@@ -86,19 +86,39 @@ abstract class cachestore_tests extends advanced_testcase {
      */
     public function run_tests(cache_store $instance) {
 
-        // Test set.
+        // Test set with a string.
         $this->assertTrue($instance->set('test1', 'test1'));
         $this->assertTrue($instance->set('test2', 'test2'));
+        $this->assertTrue($instance->set('test3', '3'));
 
-        // Test get.
-        $this->assertEquals('test1', $instance->get('test1'));
-        $this->assertEquals('test2', $instance->get('test2'));
+        // Test get with a string.
+        $this->assertSame('test1', $instance->get('test1'));
+        $this->assertSame('test2', $instance->get('test2'));
+        $this->assertSame('3', $instance->get('test3'));
+
+        // Test set with an int.
+        $this->assertTrue($instance->set('test1', 1));
+        $this->assertTrue($instance->set('test2', 2));
+
+        // Test get with an int.
+        $this->assertSame(1, $instance->get('test1'));
+        $this->assertInternalType('int', $instance->get('test1'));
+        $this->assertSame(2, $instance->get('test2'));
+        $this->assertInternalType('int', $instance->get('test2'));
+
+        // Test set with a bool.
+        $this->assertTrue($instance->set('test1', true));
+
+        // Test get with an bool.
+        $this->assertSame(true, $instance->get('test1'));
+        $this->assertInternalType('boolean', $instance->get('test1'));
 
         // Test delete.
         $this->assertTrue($instance->delete('test1'));
+        $this->assertTrue($instance->delete('test3'));
         $this->assertFalse($instance->delete('test3'));
         $this->assertFalse($instance->get('test1'));
-        $this->assertEquals('test2', $instance->get('test2'));
+        $this->assertSame(2, $instance->get('test2'));
         $this->assertTrue($instance->set('test1', 'test1'));
 
         // Test purge.
@@ -114,16 +134,16 @@ abstract class cachestore_tests extends advanced_testcase {
             array('key' => 'many4', 'value' => 'many4'),
             array('key' => 'many5', 'value' => 'many5')
         ));
-        $this->assertEquals(5, $outcome);
-        $this->assertEquals('many1', $instance->get('many1'));
-        $this->assertEquals('many5', $instance->get('many5'));
+        $this->assertSame(5, $outcome);
+        $this->assertSame('many1', $instance->get('many1'));
+        $this->assertSame('many5', $instance->get('many5'));
         $this->assertFalse($instance->get('many6'));
 
         // Test get_many.
         $result = $instance->get_many(array('many1', 'many3', 'many5', 'many6'));
         $this->assertInternalType('array', $result);
         $this->assertCount(4, $result);
-        $this->assertEquals(array(
+        $this->assertSame(array(
             'many1' => 'many1',
             'many3' => 'many3',
             'many5' => 'many5',
@@ -131,7 +151,7 @@ abstract class cachestore_tests extends advanced_testcase {
         ), $result);
 
         // Test delete_many.
-        $this->assertEquals(3, $instance->delete_many(array('many2', 'many3', 'many4')));
-        $this->assertEquals(2, $instance->delete_many(array('many1', 'many5', 'many6')));
+        $this->assertSame(3, $instance->delete_many(array('many2', 'many3', 'many4')));
+        $this->assertSame(2, $instance->delete_many(array('many1', 'many5', 'many6')));
     }
 }
\ No newline at end of file
index 6c5c330..52bc618 100644 (file)
@@ -104,6 +104,7 @@ if (empty($parent_category)) {
     $item->aggregationcoef = 0;
 } else if ($parent_category->aggregation == GRADE_AGGREGATE_SUM) {
     $item->aggregationcoef = $item->aggregationcoef > 0 ? 1 : 0;
+    $item->aggregationcoef2 = format_float($item->aggregationcoef2 * 100.0);
 } else {
     $item->aggregationcoef = format_float($item->aggregationcoef, 4);
 }
@@ -131,12 +132,15 @@ if ($data = $mform->get_data()) {
     unset($data->locked);
     unset($data->locktime);
 
-    $convert = array('gradepass', 'aggregationcoef');
+    $convert = array('gradepass', 'aggregationcoef', 'aggregationcoef2');
     foreach ($convert as $param) {
         if (property_exists($data, $param)) {
             $data->$param = unformat_float($data->$param);
         }
     }
+    if (isset($data->aggregationcoef2) && $parent_category->aggregation == GRADE_AGGREGATE_SUM) {
+        $data->aggregationcoef2 = $data->aggregationcoef2 / 100.0;
+    }
 
     $grade_item = new grade_item(array('id'=>$id, 'courseid'=>$courseid));
     grade_item::set_properties($grade_item, $data);
index 8ae803f..9428e5b 100644 (file)
@@ -73,6 +73,14 @@ class edit_outcomeitem_form extends moodleform {
         $mform->addHelpButton('cmid', 'linkedactivity', 'grades');
         $mform->setDefault('cmid', 0);
 
+        $mform->addElement('advcheckbox', 'weightoverride', get_string('adjustedweight', 'grades'));
+        $mform->addHelpButton('weightoverride', 'weightoverride', 'grades');
+
+        $mform->addElement('text', 'aggregationcoef2', get_string('weight', 'grades'));
+        $mform->addHelpButton('aggregationcoef2', 'weight', 'grades');
+        $mform->setType('aggregationcoef2', PARAM_RAW);
+        $mform->disabledIf('aggregationcoef2', 'weightoverride');
+
         /// hiding
         /// advcheckbox is not compatible with disabledIf !!
         $mform->addElement('checkbox', 'hidden', get_string('hidden', 'grades'));
@@ -125,8 +133,9 @@ class edit_outcomeitem_form extends moodleform {
         }
 
         if ($coefstring !== '') {
-            if ($coefstring == 'aggregationcoefextrasum') {
+            if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
                 // advcheckbox is not compatible with disabledIf!
+                $coefstring = 'aggregationcoefextrasum';
                 $mform->addElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
             } else {
                 $mform->addElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
@@ -193,7 +202,7 @@ class edit_outcomeitem_form extends moodleform {
 
                 $parent_category->apply_forced_settings();
 
-                if (!$parent_category->is_aggregationcoef_used() or $parent_category->aggregation == GRADE_AGGREGATE_SUM) {
+                if (!$parent_category->is_aggregationcoef_used()) {
                     if ($mform->elementExists('aggregationcoef')) {
                         $mform->removeElement('aggregationcoef');
                     }
@@ -219,6 +228,15 @@ class edit_outcomeitem_form extends moodleform {
                         $mform->addHelpButton('aggregationcoef', $aggcoef, 'grades');
                     }
                 }
+                // Remove fields used by natural weighting if the parent category is not using natural weighting.
+                if ($parent_category->aggregation != GRADE_AGGREGATE_SUM) {
+                    if ($mform->elementExists('weightoverride')) {
+                        $mform->removeElement('weightoverride');
+                    }
+                    if ($mform->elementExists('aggregationcoef2')) {
+                        $mform->removeElement('aggregationcoef2');
+                    }
+                }
             }
 
         }
index 790f889..1d57c7d 100644 (file)
@@ -59,6 +59,10 @@ Bobby,Bunce,,"Moodle HQ","Rock on!",student5@mail.com,75.00,75.00';
     /** @var array $columns The first row of the csv file. These are the columns of the import file.*/
     protected $columns;
 
+    public function tearDown() {
+        $this->csvimport = null;
+    }
+
     /**
      * Load up the above text through the csv import.
      *
index 50b1c55..d33be86 100644 (file)
@@ -464,7 +464,7 @@ function grade_get_graded_users_select($report, $course, $userid, $groupid, $inc
  * @param int $courseid The current course id.
  */
 function hide_natural_aggregation_upgrade_notice($courseid) {
-    set_config('show_sumofgrades_upgrade_' . $courseid, false);
+    unset_config('show_sumofgrades_upgrade_' . $courseid);
 }
 
 /**
index 1174375..2e7022b 100644 (file)
@@ -191,6 +191,7 @@ if ($USER->gradeediting[$course->id] && ($report->get_pref('showquickfeedback')
     echo '<div>';
     echo '<input type="hidden" value="'.s($courseid).'" name="id" />';
     echo '<input type="hidden" value="'.sesskey().'" name="sesskey" />';
+    echo '<input type="hidden" value="'.time().'" name="timepageload" />';
     echo '<input type="hidden" value="grader" name="report"/>';
     echo '<input type="hidden" value="'.$page.'" name="page"/>';
     echo $reporthtml;
index f9feea8..32577b8 100644 (file)
@@ -185,6 +185,7 @@ class grade_report_grader extends grade_report {
 
         // Were any changes made?
         $changedgrades = false;
+        $timepageload = clean_param($data->timepageload, PARAM_INT);
 
         foreach ($data as $varname => $students) {
 
@@ -259,8 +260,15 @@ class grade_report_grader extends grade_report {
                         }
 
                         $errorstr = '';
-                        // Warn if the grade is out of bounds.
-                        if (!is_null($finalgrade)) {
+                        $skip = false;
+
+                        $dategraded = $oldvalue->get_dategraded();
+                        if (!empty($dategraded) && $timepageload < $dategraded) {
+                            // Warn if the grade was updated while we were editing this form.
+                            $errorstr = 'gradewasmodifiedduringediting';
+                            $skip = true;
+                        } else if (!is_null($finalgrade)) {
+                            // Warn if the grade is out of bounds.
                             $bounded = $gradeitem->bounded_grade($finalgrade);
                             if ($bounded > $finalgrade) {
                                 $errorstr = 'lessthanmin';
@@ -268,6 +276,7 @@ class grade_report_grader extends grade_report {
                                 $errorstr = 'morethanmax';
                             }
                         }
+
                         if ($errorstr) {
                             $userfields = 'id, ' . get_all_user_name_fields(true);
                             $user = $DB->get_record('user', array('id' => $userid), $userfields);
@@ -275,6 +284,10 @@ class grade_report_grader extends grade_report {
                             $gradestr->username = fullname($user);
                             $gradestr->itemname = $gradeitem->get_name();
                             $warnings[] = get_string($errorstr, 'grades', $gradestr);
+                            if ($skip) {
+                                // Skipping the update of this grade it failed the tests above.
+                                continue;
+                            }
                         }
 
                     } else if ($datatype == 'feedback') {
@@ -729,7 +742,6 @@ class grade_report_grader extends grade_report {
         $strftimedatetimeshort = get_string('strftimedatetimeshort');
         $strexcludedgrades = get_string('excluded', 'grades');
         $strerror = get_string('error');
-        $strtypescale = get_string('typescale', 'grades');
 
         foreach ($this->gtree->get_levels() as $key => $row) {
             $headingrow = new html_table_row();
@@ -978,7 +990,9 @@ class grade_report_grader extends grade_report {
                                 $nogradestr = $this->get_lang_string('nooutcome', 'grades');
                             }
                             $attributes = array('tabindex' => $tabindices[$item->id]['grade'], 'id'=>'grade_'.$userid.'_'.$item->id);
-                            $itemcell->text .= html_writer::label($strtypescale, $attributes['id'], false,
+                            $gradelabel = $fullname . ' ' . $item->itemname;
+                            $itemcell->text .= html_writer::label(
+                                get_string('useractivitygrade', 'gradereport_grader', $gradelabel), $attributes['id'], false,
                                     array('class' => 'accesshide'));
                             $itemcell->text .= html_writer::select($scaleopt, 'grade['.$userid.']['.$item->id.']', $gradeval, array(-1=>$nogradestr), $attributes);
                         } else if (!empty($scale)) {
diff --git a/grade/report/user/db/upgrade.php b/grade/report/user/db/upgrade.php
new file mode 100644 (file)
index 0000000..38bb08a
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+/**
+ * Gradereport user plugin upgrade code
+ *
+ * @package    gradereport_user
+ * @copyright  2014 Zachary Durber
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * @param int $oldversion the version we are upgrading from
+ * @return bool result
+ */
+function xmldb_gradereport_user_upgrade($oldversion) {
+
+    if ($oldversion < 2014101500) {
+        // Need to always show weight and contribution to course total.
+        set_config('grade_report_user_showweight', 1);
+
+        // User savepoint reached.
+        upgrade_plugin_savepoint(true, 2014101500, 'gradereport', 'user');
+    }
+    return true;
+}
index 9c0660c..39d7988 100644 (file)
@@ -118,10 +118,10 @@ class grade_report_user extends grade_report {
     public $showfeedback = true;
 
     /**
-     * Show grade weighting in the report, default false
+     * Show grade weighting in the report, default true.
      * @var bool
      */
-    public $showweight = false;
+    public $showweight = true;
 
     /**
      * Show letter grades in the report, default false
@@ -133,7 +133,7 @@ class grade_report_user extends grade_report {
      * Show the calculated contribution to the course total column.
      * @var bool
      */
-    public $showcontributiontocoursetotal = false;
+    public $showcontributiontocoursetotal = true;
 
     /**
      * Show average grades in the report, default false.
@@ -205,8 +205,13 @@ class grade_report_user extends grade_report {
         $this->showgrade       = grade_get_setting($this->courseid, 'report_user_showgrade',       !empty($CFG->grade_report_user_showgrade));
         $this->showrange       = grade_get_setting($this->courseid, 'report_user_showrange',       !empty($CFG->grade_report_user_showrange));
         $this->showfeedback    = grade_get_setting($this->courseid, 'report_user_showfeedback',    !empty($CFG->grade_report_user_showfeedback));
-        $this->showweight      = grade_get_setting($this->courseid, 'report_user_showweight',      !empty($CFG->grade_report_user_showweight));
-        $this->showcontributiontocoursetotal      = grade_get_setting($this->courseid, 'report_user_showcontributiontocoursetotal',      !empty($CFG->grade_report_user_showcontributiontocoursetotal));
+
+        $this->showweight = grade_get_setting($this->courseid, 'report_user_showweight',
+            !empty($CFG->grade_report_user_showweight));
+
+        $this->showcontributiontocoursetotal = grade_get_setting($this->courseid, 'report_user_showcontributiontocoursetotal',
+            !empty($CFG->grade_report_user_showcontributiontocoursetotal));
+
         $this->showlettergrade = grade_get_setting($this->courseid, 'report_user_showlettergrade', !empty($CFG->grade_report_user_showlettergrade));
         $this->showaverage     = grade_get_setting($this->courseid, 'report_user_showaverage',     !empty($CFG->grade_report_user_showaverage));
 
@@ -712,12 +717,12 @@ class grade_report_user extends grade_report {
                 // Normalise the gradeval.
                 $gradecat = $grade_object->load_parent_category();
                 if ($gradecat->aggregation == GRADE_AGGREGATE_SUM) {
-                    // Natural aggregation/Sum of grades does not consider the mingrade.
+                    // Natural aggregation/Sum of grades does not consider the mingrade, cannot traditionnally normalise it.
                     $graderange = $this->aggregationhints[$itemid]['grademax'];
                     $gradeval = $this->aggregationhints[$itemid]['grade'] / $graderange;
                 } else {
-                    $graderange = $this->aggregationhints[$itemid]['grademax'] - $this->aggregationhints[$itemid]['grademin'];
-                    $gradeval = ($this->aggregationhints[$itemid]['grade'] - $this->aggregationhints[$itemid]['grademin']) / $graderange;
+                    $gradeval = grade_grade::standardise_score($this->aggregationhints[$itemid]['grade'],
+                        $this->aggregationhints[$itemid]['grademin'], $this->aggregationhints[$itemid]['grademax'], 0, 1);
                 }
 
                 // Multiply the normalised value by the weight
index c85f4c8..134cb18 100644 (file)
@@ -31,7 +31,10 @@ if ($ADMIN->fulltree) {
     $settings->add(new admin_setting_configcheckbox('grade_report_user_showgrade', get_string('showgrade', 'grades'), get_string('showgrade_help', 'grades'), 1));
     $settings->add(new admin_setting_configcheckbox('grade_report_user_showfeedback', get_string('showfeedback', 'grades'), get_string('showfeedback_help', 'grades'), 1));
     $settings->add(new admin_setting_configcheckbox('grade_report_user_showrange', get_string('showrange', 'grades'), get_string('showrange_help', 'grades'), 1));
-    $settings->add(new admin_setting_configcheckbox('grade_report_user_showweight', get_string('showweight', 'grades'), get_string('showweight_help', 'grades'), 0));
+
+    $settings->add(new admin_setting_configcheckbox('grade_report_user_showweight',
+        get_string('showweight', 'grades'), get_string('showweight_help', 'grades'), 1));
+
     $settings->add(new admin_setting_configcheckbox('grade_report_user_showaverage', get_string('showaverage', 'grades'), get_string('showaverage_help', 'grades'), 0));
     $settings->add(new admin_setting_configcheckbox('grade_report_user_showlettergrade', get_string('showlettergrade', 'grades'), get_string('showlettergrade_help', 'grades'), 0));
     $settings->add(new admin_setting_configselect('grade_report_user_rangedecimals', get_string('rangedecimals', 'grades'),
@@ -47,5 +50,7 @@ if ($ADMIN->fulltree) {
                                                       array(GRADE_REPORT_HIDE_TOTAL_IF_CONTAINS_HIDDEN => get_string('hide'),
                                                             GRADE_REPORT_SHOW_TOTAL_IF_CONTAINS_HIDDEN => get_string('hidetotalshowexhiddenitems', 'grades'),
                                                             GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN => get_string('hidetotalshowinchiddenitems', 'grades'))));
-    $settings->add(new admin_setting_configcheckbox('grade_report_user_showcontributiontocoursetotal', get_string('showcontributiontocoursetotal', 'grades'), get_string('showcontributiontocoursetotal_help', 'grades'), 0));
+
+    $settings->add(new admin_setting_configcheckbox('grade_report_user_showcontributiontocoursetotal',
+        get_string('showcontributiontocoursetotal', 'grades'), get_string('showcontributiontocoursetotal_help', 'grades'), 1));
 }
index e9a88f7..e6b9299 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2014101500;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2014050800;        // Requires this Moodle version
 $plugin->component = 'gradereport_user'; // Full name of the plugin (used for diagnostics)
index 6dffd30..40e7063 100644 (file)
@@ -246,6 +246,126 @@ Feature: We can use calculated grade totals
     And I set the field "Grade report" to "Overview report"
     And I should see "50.00 (50.00 %)" in the "overview-grade" "table"
 
+  @javascript
+  Scenario: Natural aggregation on outcome items with natural weights
+    And I log out
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | Enable outcomes | 1 |
+    And the following "scales" exist:
+      | name       | scale                                     |
+      | Test Scale | Disappointing, Good, Very good, Excellent |
+    And the following "grade outcomes" exist:
+      | fullname  | shortname | course | scale      |
+      | Outcome 1 | OT1       | C1     | Test Scale |
+    And the following "grade items" exist:
+      | itemname              | course | outcome | gradetype | scale      |
+      | Test outcome item one | C1     | OT1     | Scale     | Test Scale |
+    And I log out
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I expand "Setup" node
+    And I follow "Categories and items"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation                     | Natural |
+      | Include outcomes in aggregation | 1       |
+      | Exclude empty grades            | 0       |
+    And I follow "Grader report"
+    And I turn editing mode on
+    And I press "Save changes"
+    And I give the grade "Excellent" to the user "Student 1" for the grade item "Test outcome item one"
+    And I press "Save changes"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
+    And I set the field "report_overview_showtotalsifcontainhidden" to "Show totals excluding hidden items"
+    And I set the field "report_user_showtotalsifcontainhidden" to "Show totals excluding hidden items"
+    And I press "Save changes"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    Then "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)"
+    And I set the field "Grade report" to "Overview report"
+    And I should see "114.82 (18.27 %)" in the "overview-grade" "table"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I expand "Setup" node
+    And I follow "Categories and items"
+    And I set the following settings for grade item "Test outcome item one":
+     | Extra credit     | 1   |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    Then "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)"
+    And I set the field "Grade report" to "Overview report"
+    And I should see "114.00 (18.39 %)" in the "overview-grade" "table"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I expand "Setup" node
+    And I follow "Categories and items"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation                     | Natural |
+      | Include outcomes in aggregation | 0       |
+    And I set the following settings for grade item "Test outcome item one":
+     | Extra credit     | 0   |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    Then "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)"
+    And I set the field "Grade report" to "Overview report"
+    And I should see "110.00 (17.74 %)" in the "overview-grade" "table"
+
+  @javascript
+  Scenario: Natural aggregation on outcome items with modified weights
+    And I log out
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | Enable outcomes | 1 |
+    And the following "scales" exist:
+      | name       | scale                                     |
+      | Test Scale | Disappointing, Good, Very good, Excellent |
+    And the following "grade outcomes" exist:
+      | fullname  | shortname | course | scale      |
+      | Outcome 1 | OT1       | C1     | Test Scale |
+    And the following "grade items" exist:
+      | itemname              | course | outcome | gradetype | scale      |
+      | Test outcome item one | C1     | OT1     | Scale     | Test Scale |
+    And I log out
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I expand "Setup" node
+    And I follow "Categories and items"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation                     | Natural |
+      | Include outcomes in aggregation | 1       |
+      | Exclude empty grades            | 0       |
+    And I set the following settings for grade item "Test outcome item one":
+     | Weight adjusted  | 1   |
+     | aggregationcoef2 | 100 |
+    And I follow "Grader report"
+    And I turn editing mode on
+    And I press "Save changes"
+    And I give the grade "Excellent" to the user "Student 1" for the grade item "Test outcome item one"
+    And I press "Save changes"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
+    And I set the field "report_overview_showtotalsifcontainhidden" to "Show totals excluding hidden items"
+    And I set the field "report_user_showtotalsifcontainhidden" to "Show totals excluding hidden items"
+    And I press "Save changes"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    Then "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)"
+    And I set the field "Grade report" to "Overview report"
+    And I should see "4.00 (100.00 %)" in the "overview-grade" "table"
+
   @javascript
   Scenario: Natural aggregation
     And I set the following settings for grade item "Sub category 1":
index 1421da7..c884ba8 100644 (file)
@@ -63,6 +63,11 @@ Feature: View gradebook when scales are used
     And I press "Save changes"
     And I follow "Course 1"
     And I follow "Grades"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
+    And I set the field "Show weightings" to "Show"
+    And I set the field "Show contribution to course total" to "Show"
+    And I press "Save changes"
+    And I follow "Grader report"
     And I turn editing mode on
 
   @javascript
@@ -83,10 +88,10 @@ Feature: View gradebook when scales are used
     And I set the field "Select all or one user" to "Student 3"
     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 |
-      | Test assignment one | C     | F–A   | 50.00 %    |
-      | Category total      | 3.00  | 0–5   | 60.00 %    |
-      | Course total        | 3.00  | 0–5   | 60.00 %    |
+      | Grade item          | Grade | Range | Percentage | Contribution to course total |
+      | Test assignment one | C     | F–A   | 50.00 %    | 3.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"
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
@@ -98,10 +103,10 @@ Feature: View gradebook when scales are used
     And I follow "Course 1"
     And I follow "Grades"
     And the following should exist in the "user-grade" table:
-      | Grade item          | Grade | Range | Percentage |
-      | Test assignment one | B     | F–A   | 75.00 %    |
-      | Category total      | 4.00  | 0–5   | 80.00 %    |
-      | Course total        | 4.00  | 0–5   | 80.00 %    |
+      | Grade item          | Grade | Range | Percentage | Contribution to course total |
+      | Test assignment one | B     | F–A   | 75.00 %    | 4.00                         |
+      | Category total      | 4.00  | 0–5   | 80.00 %    | -                            |
+      | Course total        | 4.00  | 0–5   | 80.00 %    | -                            |
 
   @javascript
   Scenario Outline: Test displaying scales in gradebook in all other aggregation methods
@@ -131,10 +136,10 @@ Feature: View gradebook when scales are used
     And I set the field "Select all or one user" to "Student 3"
     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    |
-      | Test assignment one | C              | F–A   | 50.00 %       |
-      | Category total<aggregation>.      | 3.00           | 1–5   | 50.00 %       |
-      | Course total<aggregation>.        | <coursetotal3> | 0–100 | <courseperc3> |
+      | Grade item                   | Grade          | Range | Percentage    | Contribution to course total |
+      | Test assignment one          | C              | F–A   | 50.00 %       | <contrib3>                   |
+      | Category total<aggregation>. | 3.00           | 1–5   | 50.00 %       | -                            |
+      | Course total<aggregation>.   | <coursetotal3> | 0–100 | <courseperc3> | -                            |
     And I set the field "jump" to "Categories and items"
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
@@ -146,18 +151,18 @@ Feature: View gradebook when scales are used
     And I follow "Course 1"
     And I follow "Grades"
     And the following should exist in the "user-grade" table:
-      | Grade item          | Grade          | Range | Percentage    |
-      | Test assignment one | B              | F–A   | 75.00 %       |
-      | Category total<aggregation>.      | 4.00           | 1–5   | 75.00 %       |
-      | Course total<aggregation>.        | <coursetotal2> | 0–100 | <courseperc2> |
+      | Grade item                   | Grade          | Range | Percentage    | Contribution to course total |
+      | Test assignment one          | B              | F–A   | 75.00 %       | <contrib2>                   |
+      | Category total<aggregation>. | 4.00           | 1–5   | 75.00 %       | -                            |
+      | Course total<aggregation>.   | <coursetotal2> | 0–100 | <courseperc2> | -                            |
 
-  Examples:
-      | aggregation                         | coursetotal1 | coursetotal2 | coursetotal3 | coursetotal4 | coursetotal5 |overallavg | courseperc2 | courseperc3 |
-      | Mean of grades                      | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     |
-      | Weighted mean of grades             | -            | -            | -            | -            | -            | -         | -           | -           |
-      | Simple weighted mean of grades      | 100.00       | 75.00        | 50.00        | 25.00        | 0.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 %     |
-      | Median of grades                    | 100.00       | 75.00        | 50.00        | 25.00        | 0.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 %     |
-      | Highest grade                       | 100.00       | 75.00        | 50.00        | 25.00        | 0.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 %     |
+    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    |
diff --git a/grade/tests/behat/grade_single_item_scales.feature b/grade/tests/behat/grade_single_item_scales.feature
new file mode 100644 (file)
index 0000000..6692c49
--- /dev/null
@@ -0,0 +1,134 @@
+@core @core_grades
+Feature: View gradebook when single item scales are used
+  In order to use single item scales to grade activities
+  As an teacher
+  I need to be able to view gradebook with single item scales
+
+  Background:
+    Given I log in as "admin"
+    And I set the following administration settings values:
+      | grade_report_showranges    | 1 |
+      | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
+    And I navigate to "Scales" node in "Site administration > Grades"
+    And I press "Add a new scale"
+    And I set the following fields to these values:
+      | Name  | Singleitem |
+      | Scale | Ace!       |
+    And I press "Save changes"
+    And I log out
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "users" exist:
+      | username | firstname | lastname | email            | idnumber |
+      | teacher1 | Teacher   | 1        | teacher1@asd.com | t1       |
+      | student1 | Student   | 1        | student1@asd.com | s1       |
+      | student2 | Student   | 2        | student2@asd.com | s2       |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+      | student2 | C1     | student        |
+    And the following "grade categories" exist:
+      | fullname       | course |
+      | Sub category 1 | C1     |
+    And the following "activities" exist:
+      | activity | course | idnumber | name                | intro             | gradecategory  |
+      | assign   | C1     | a1       | Test assignment one | Submit something! | Sub category 1 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Test assignment one"
+    And I follow "Edit settings"
+    And I expand all fieldsets
+    And I set the field "grade[modgrade_type]" to "Scale"
+    And I set the field "grade[modgrade_scale]" to "Singleitem"
+    And I press "Save and display"
+    And I follow "View/grade all submissions"
+    And I click on "Grade Student 1" "link" in the "Student 1" "table_row"
+    And I set the field "Grade" to "A"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
+    And I set the field "Show weightings" to "Show"
+    And I set the field "Show contribution to course total" to "Show"
+    And I press "Save changes"
+    And I follow "Grader report"
+    And I turn editing mode on
+
+  @javascript
+  Scenario: Test displaying single item scales in gradebook in aggregation method Natural
+    When I turn editing mode off
+    Then the following should exist in the "user-grades" table:
+      | -1-                | -4-       | -5-            | -6-          |
+      | Student 1          | Ace!      | 1.00           | 1.00         |
+    And the following should exist in the "user-grades" table:
+      | -1-                | -2-       | -3-            | -4-          |
+      | Range              | Ace!–Ace! | 0.00–1.00      | 0.00–1.00    |
+      | Overall average    | Ace!      | 1.00           | 1.00         |
+    And I follow "User report"
+    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                         |
+      | 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                         |
+      | Category total      | -     | 0–1       | -                            |
+      | Course total        | -     | 0–1       | -                            |
+    And I set the field "jump" to "Categories and items"
+    And the following should exist in the "grade_edit_tree_table" table:
+      | Name                | Max grade |
+      | Test assignment one | 1.00      |
+      | Category total      | 1.00      |
+      | Course total        | 1.00      |
+
+  @javascript
+  Scenario Outline: Test displaying single item scales in gradebook in all other aggregation methods
+    When I follow "Edit   Course 1"
+    And I set the field "Aggregation" to "<aggregation>"
+    And I press "Save changes"
+    And I follow "Edit   Sub category 1"
+    And I expand all fieldsets
+    And I set the field "Aggregation" to "<aggregation>"
+    And I set the field "Category name" to "Sub category (<aggregation>)"
+    # And I set the field "Maximum grade" to "5"
+    # And I set the field "Minimum grade" to "1"
+    And I press "Save changes"
+    And I turn editing mode off
+    Then the following should exist in the "user-grades" table:
+      | -1-                | -4-       | -5-            | -6-            |
+      | Student 1          | Ace!      | <cattotal1>    | <coursetotal1> |
+      | Student 2          | -         | -              | -              |
+    And the following should exist in the "user-grades" table:
+      | -1-                | -2-       | -3-            | -4-            |
+      | Range              | Ace!–Ace! | 0.00–100.0     | 0.00–100.00    |
+      | Overall average    | Ace!      | <catavg>       | <overallavg>   |
+    And I follow "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    And I click on "Select all or one user" "select"
+    And the following should exist in the "user-grade" table:
+      | Grade item                        | Grade          | Range       | Contribution to course total |
+      | Test assignment one               | Ace!           | Ace!–Ace!   | <contrib1>                   |
+      | Category total<aggregation>.      | <cattotal1>    | 0–100       | -                            |
+      | Course total<aggregation>.        | <coursetotal1> | 0–100       | -                            |
+    And I set the field "jump" to "Categories and items"
+    And the following should exist in the "grade_edit_tree_table" table:
+      | Name                         | Max grade |
+      | Test assignment one          | Ace! (1)  |
+      | Category total<aggregation>. | 100.00    |
+      | Course total<aggregation>.   | 100.00    |
+
+    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     |
index 8c39066..cf3d498 100644 (file)
@@ -65,6 +65,7 @@ class core_grade_report_graderlib_testcase extends advanced_testcase {
         $data = new stdClass();
         $data->id = $course->id;
         $data->report = 'grader';
+        $data->timepageload = time();
 
         $data->grade = array();
         $data->grade[$student->id] = array();
index 7dbf84a..acee612 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['cannotcreatedboninstall'] = '<p>Không thể tạo CSDL.</p>
+<p>CSDL được chỉ định không tồn tại và người dùng không được phép tạo CSDL.</p>
+<p>Quản trị trang nên xác minh thiết lập CSDL.</p>';
+$string['cannotcreatelangdir'] = 'Không thể tạo thư mục lang';
 $string['cannotcreatetempdir'] = 'Không thể tạo thư mục tạm thời';
 $string['cannotdownloadcomponents'] = 'Không thể tải component';
 $string['cannotdownloadzipfile'] = 'Không thể tải tập tin ZIP';
 $string['cannotfindcomponent'] = 'Không thấy component';
+$string['cannotsavemd5file'] = 'Không thể lưu tệp md5';
 $string['cannotsavezipfile'] = 'Không thể lưu tập tin ZIP';
 $string['cannotunzipfile'] = 'Không thể giải nén tập tin';
 $string['componentisuptodate'] = 'Component đang ở tình trạng mới nhất';
+$string['dmlexceptiononinstall'] = '<p>Lỗi CSDL xảy ra [{$a->errorcode}].<br />{$a->debuginfo}</p>';
+$string['downloadedfilecheckfailed'] = 'Kiểm tra tệp tải về bất thành';
+$string['invalidmd5'] = 'Biến kiểm tra sai - thử lại';
+$string['missingrequiredfield'] = 'Thiếu một số mục bắt buộc';
+$string['remotedownloaderror'] = '<p>Tải về thành phần máy chủ của bạn bất thành. Hãy xác minh các thiết lập proxy; bộ mở rộng PHP cURL được khuyến nghị.<p>
+<p>Bạn phải tải tệp <a href="{$a->url}">{$a->url}</a> thủ công, chép nó vào "{$a->dest}" trong máy chủ của mình và giải nén nó.</p>';
+$string['wrongdestpath'] = 'Đường dẫn sai';
+$string['wrongsourcebase'] = 'Nền URL nguồn sai';
+$string['wrongzipfilename'] = 'Tên tệp ZIP sai';
index 118d366..16212a9 100644 (file)
@@ -322,6 +322,7 @@ $string['gradetype_help'] = 'There are 4 grade types:
 Only value and scale grade types may be aggregated. The grade type for an activity-based grade item is set on the activity settings page.';
 $string['gradevaluetoobig'] = 'One of the grade values is larger than the allowed grade maximum of {$a}';
 $string['gradeview'] = 'View grade';
+$string['gradewasmodifiedduringediting'] = 'The grade entered for {$a->itemname} for {$a->username} was ignored because it was more recently updated by someone else.';
 $string['gradeweighthelp'] = 'Grade weight help';
 $string['groupavg'] = 'Group average';
 $string['hidden'] = 'Hidden';
@@ -670,8 +671,8 @@ $string['studentsperpagereduced'] = 'Reduced maximum students per page from {$a-
 $string['subcategory'] = 'Normal category';
 $string['submissions'] = 'Submissions';
 $string['submittedon'] = 'Submitted: {$a}';
-$string['sumofgradesupgradedgrades'] = 'A recent upgrade has changed the aggregation method "Sum of grades" to "Natural". Please review the grades in this course as it was previously using "Sum of grades".';
-$string['sumofgradesupgradedgradeshidemessage'] = 'Got it';
+$string['sumofgradesupgradedgrades'] = 'Note: The aggregation method "Sum of grades" has been changed to "Natural" as part of a site upgrade. Since "Sum of grades" was previously used in this course, it is recommended that you review this change in the gradebook.';
+$string['sumofgradesupgradedgradeshidemessage'] = 'OK';
 $string['switchtofullview'] = 'Switch to full view';
 $string['switchtosimpleview'] = 'Switch to simple view';
 $string['tabs'] = 'Tabs';
index b3ed02a..abd8fdb 100644 (file)
@@ -3110,10 +3110,6 @@ function get_user_roles_in_course($userid, $courseid) {
         $context = context_course::instance($courseid);
     }
 
-    if (empty($CFG->profileroles)) {
-        return array();
-    }
-
     list($rallowed, $params) = $DB->get_in_or_equal(explode(',', $CFG->profileroles), SQL_PARAMS_NAMED, 'a');
     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
     $params = array_merge($params, $cparams);
index 9c9dc65..f8d12bf 100644 (file)
@@ -2178,7 +2178,13 @@ function blocks_add_default_system_blocks() {
     $page->blocks->add_blocks(array(BLOCK_POS_LEFT => array('navigation', 'settings')), '*', null, true);
     $page->blocks->add_blocks(array(BLOCK_POS_LEFT => array('admin_bookmarks')), 'admin-*', null, null, 2);
 
+    if ($defaultmypage = $DB->get_record('my_pages', array('userid' => null, 'name' => '__default', 'private' => 1))) {
+        $subpagepattern = $defaultmypage->id;
+    } else {
+        $subpagepattern = null;
+    }
+
     $newblocks = array('private_files', 'online_users', 'badges', 'calendar_month', 'calendar_upcoming');
     $newcontent = array('course_overview');
-    $page->blocks->add_blocks(array(BLOCK_POS_RIGHT => $newblocks, 'content' => $newcontent), 'my-index');
+    $page->blocks->add_blocks(array(BLOCK_POS_RIGHT => $newblocks, 'content' => $newcontent), 'my-index', $subpagepattern);
 }
index 334de7c..818832d 100644 (file)
@@ -1052,6 +1052,10 @@ class core_plugin_manager {
                 'database', 'legacy', 'standard',
             ),
 
+            'ltiservice' => array(
+                'profile', 'toolproxy', 'toolsettings'
+            ),
+
             'message' => array(
                 'airnotifier', 'email', 'jabber', 'popup'
             ),
@@ -1137,7 +1141,7 @@ class core_plugin_manager {
             'tool' => array(
                 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
                 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
-                'langimport', 'log', 'messageinbound', 'multilangupgrade', 'phpunit', 'profiling',
+                'langimport', 'log', 'messageinbound', 'multilangupgrade', 'monitor', 'phpunit', 'profiling',
                 'replace', 'spamcleaner', 'task', 'timezoneimport',
                 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
             ),
index 229d568..f159d5a 100644 (file)
@@ -35,26 +35,31 @@ defined('MOODLE_INTERNAL') || die();
  * @package   moodlecore
  */
 class csv_import_reader {
+
     /**
      * @var int import identifier
      */
-    var $_iid;
+    private $_iid;
+
     /**
      * @var string which script imports?
      */
-    var $_type;
+    private $_type;
+
     /**
      * @var string|null Null if ok, error msg otherwise
      */
-    var $_error;
+    private $_error;
+
     /**
      * @var array cached columns
      */
-    var $_columns;
+    private $_columns;
+
     /**
      * @var object file handle used during import
      */
-    var $_fp;
+    private $_fp;
 
     /**
      * Contructor
@@ -62,24 +67,29 @@ class csv_import_reader {
      * @param int $iid import identifier
      * @param string $type which script imports?
      */
-    function csv_import_reader($iid, $type) {
+    public function __construct($iid, $type) {
         $this->_iid  = $iid;
         $this->_type = $type;
     }
 
+    /**
+     * Make sure the file is closed when this object is discarded.
+     */
+    public function __destruct() {
+        $this->close();
+    }
+
     /**
      * Parse this content
      *
-     * @global object
-     * @global object
-     * @param string $content passed by ref for memory reasons, unset after return
+     * @param string $content the content to parse.
      * @param string $encoding content encoding
      * @param string $delimiter_name separator (comma, semicolon, colon, cfg)
      * @param string $column_validation name of function for columns validation, must have one param $columns
      * @param string $enclosure field wrapper. One character only.
      * @return bool false if error, count of data lines if ok; use get_error() to get error string
      */
-    function load_csv_content(&$content, $encoding, $delimiter_name, $column_validation=null, $enclosure='"') {
+    public function load_csv_content($content, $encoding, $delimiter_name, $column_validation=null, $enclosure='"') {
         global $USER, $CFG;
 
         $this->close();
@@ -182,7 +192,7 @@ class csv_import_reader {
      *
      * @return array
      */
-    function get_columns() {
+    public function get_columns() {
         if (isset($this->_columns)) {
             return $this->_columns;
         }
@@ -210,7 +220,7 @@ class csv_import_reader {
      * @global object
      * @return bool Success
      */
-    function init() {
+    public function init() {
         global $CFG, $USER;
 
         if (!empty($this->_fp)) {
@@ -232,7 +242,7 @@ class csv_import_reader {
      *
      * @return mixed false, or an array of values
      */
-    function next() {
+    public function next() {
         if (empty($this->_fp) or feof($this->_fp)) {
             return false;
         }
@@ -248,7 +258,7 @@ class csv_import_reader {
      *
      * @return void
      */
-    function close() {
+    public function close() {
         if (!empty($this->_fp)) {
             fclose($this->_fp);
             $this->_fp = null;
@@ -260,7 +270,7 @@ class csv_import_reader {
      *
      * @return string error text of null if none
      */
-    function get_error() {
+    public function get_error() {
         return $this->_error;
     }
 
@@ -271,7 +281,7 @@ class csv_import_reader {
      * @global object
      * @param boolean $full true means do a full cleanup - all sessions for current user, false only the active iid
      */
-    function cleanup($full=false) {
+    public function cleanup($full=false) {
         global $USER, $CFG;
 
         if ($full) {
@@ -286,7 +296,7 @@ class csv_import_reader {
      *
      * @return array suitable for selection box
      */
-    static function get_delimiter_list() {
+    public static function get_delimiter_list() {
         global $CFG;
         $delimiters = array('comma'=>',', 'semicolon'=>';', 'colon'=>':', 'tab'=>'\\t');
         if (isset($CFG->CSV_DELIMITER) and strlen($CFG->CSV_DELIMITER) === 1 and !in_array($CFG->CSV_DELIMITER, $delimiters)) {
@@ -301,7 +311,7 @@ class csv_import_reader {
      * @param string separator name
      * @return string delimiter char
      */
-    static function get_delimiter($delimiter_name) {
+    public static function get_delimiter($delimiter_name) {
         global $CFG;
         switch ($delimiter_name) {
             case 'colon':     return ':';
@@ -320,7 +330,7 @@ class csv_import_reader {
      * @param string separator name
      * @return string encoded delimiter char
      */
-    static function get_encoded_delimiter($delimiter_name) {
+    public static function get_encoded_delimiter($delimiter_name) {
         global $CFG;
         if ($delimiter_name == 'cfg' and isset($CFG->CSV_ENCODE)) {
             return $CFG->CSV_ENCODE;
@@ -336,7 +346,7 @@ class csv_import_reader {
      * @param string who imports?
      * @return int iid
      */
-    static function get_new_iid($type) {
+    public static function get_new_iid($type) {
         global $USER;
 
         $filename = make_temp_directory('csvimport/'.$type.'/'.$USER->id);
@@ -351,6 +361,7 @@ class csv_import_reader {
     }
 }
 
+
 /**
  * Utitily class for exporting of CSV files.
  * @copyright 2012 Adrian Greeve
index 7f8cbc7..ec35ffa 100644 (file)
@@ -3980,5 +3980,43 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2014100800.00);
     }
 
+    if ($oldversion < 2014101001.00) {
+        // Some blocks added themselves to the my/ home page, but they did not declare the
+        // subpage of the default my home page. While the upgrade script has been fixed, this
+        // upgrade script will fix the data that was wrongly added.
+
+        // We only proceed if we can find the right entry from my_pages. Private => 1 refers to
+        // the constant value MY_PAGE_PRIVATE.
+        if ($systempage = $DB->get_record('my_pages', array('userid' => null, 'private' => 1))) {
+
+            // Select the blocks there could have been automatically added. showinsubcontexts is hardcoded to 0
+            // because it is possible for administrators to have forced it on the my/ page by adding it to the
+            // system directly rather than updating the default my/ page.
+            $blocks = array('course_overview', 'private_files', 'online_users', 'badges', 'calendar_month', 'calendar_upcoming');
+            list($blocksql, $blockparams) = $DB->get_in_or_equal($blocks, SQL_PARAMS_NAMED);
+            $select = "parentcontextid = :contextid
+                    AND pagetypepattern = :page
+                    AND showinsubcontexts = 0
+                    AND subpagepattern IS NULL
+                    AND blockname $blocksql";
+            $params = array(
+                'contextid' => context_system::instance()->id,
+                'page' => 'my-index'
+            );
+            $params = array_merge($params, $blockparams);
+
+            $DB->set_field_select(
+                'block_instances',
+                'subpagepattern',
+                $systempage->id,
+                $select,
+                $params
+            );
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2014101001.00);
+    }
+
     return true;
 }
index c151c0a..ebbca5f 100644 (file)
@@ -31,7 +31,7 @@ $string['errorgroupisusedtwice'] = 'The group \'{$a}\' is defined twice; group n
 $string['errornopluginsorgroupsfound'] = 'No plugins or groups found; please add some groups and plugins.';
 $string['errorpluginnotfound'] = 'The plugin \'{$a}\' cannot be used; it does not appear to be installed.';
 $string['errorpluginisusedtwice'] = 'The plugin \'{$a}\' is used twice; plugins can only be defined once.';
-$string['errortextrecovery'] = 'The draft version of this text was unable to be restored.';
+$string['errortextrecovery'] = 'Unfortunately the draft version could not be restored.';
 $string['pluginname'] = 'Atto HTML editor';
 $string['subplugintype_atto'] = 'Atto plugin';
 $string['subplugintype_atto_plural'] = 'Atto plugins';
index 69a3695..91cdb17 100644 (file)
Binary files a/lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-usedfiles/moodle-atto_managefiles-usedfiles-debug.js and b/lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-usedfiles/moodle-atto_managefiles-usedfiles-debug.js differ
index 3894cba..016fc2c 100644 (file)
Binary files a/lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-usedfiles/moodle-atto_managefiles-usedfiles-min.js and b/lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-usedfiles/moodle-atto_managefiles-usedfiles-min.js differ
index 30447d6..ed1b8db 100644 (file)
Binary files a/lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-usedfiles/moodle-atto_managefiles-usedfiles.js and b/lib/editor/atto/plugins/managefiles/yui/build/moodle-atto_managefiles-usedfiles/moodle-atto_managefiles-usedfiles.js differ
index 79bfc2f..92778d0 100644 (file)
@@ -153,7 +153,7 @@ M.atto_managefiles.usedfiles = M.atto_managefiles.usedfiles || {
             usedFiles = {};
 
         while ((match = pattern.exec(content.get('innerHTML'))) !== null) {
-            filename = unescape(match[1]);
+            filename = decodeURI(match[1]);
             usedFiles[filename] = true;
         }
 
index f3bb3e9..5b3c102 100644 (file)
@@ -83,7 +83,7 @@
                     patt = new RegExp(base.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + "(.+?)[\\?\"']", 'gm'),
                     arr = [], match, filename;
                 while ((match = patt.exec(text)) !== null) {
-                    filename = unescape(match[1]);
+                    filename = decodeURI(match[1]);
                     if (arr.indexOf(filename) === -1) {
                         arr[arr.length] = filename;
                     }
index 32fd6f5..3f64375 100644 (file)
@@ -628,6 +628,7 @@ class grade_category extends grade_object {
             $grade->finalgrade = null;
 
             if (!is_null($oldfinalgrade)) {
+                $grade->timemodified = time();
                 $success = $grade->update('aggregation');
 
                 // If successful trigger a user_graded event.
@@ -712,6 +713,7 @@ class grade_category extends grade_object {
             $grade->finalgrade = null;
 
             if (!is_null($oldfinalgrade)) {
+                $grade->timemodified = time();
                 $success = $grade->update('aggregation');
 
                 // If successful trigger a user_graded event.
@@ -754,6 +756,7 @@ class grade_category extends grade_object {
         if (grade_floats_different($grade->finalgrade, $oldfinalgrade) ||
                 grade_floats_different($grade->rawgrademax, $oldrawgrademax) ||
                 grade_floats_different($grade->rawgrademin, $oldrawgrademin)) {
+            $grade->timemodified = time();
             $success = $grade->update('aggregation');
 
             // If successful trigger a user_graded event.
@@ -966,15 +969,14 @@ class grade_category extends grade_object {
                 $sum       = 0;
 
                 foreach ($grade_values as $itemid=>$grade_value) {
-
+                    if ($weights !== null) {
+                        $weights[$itemid] = $items[$itemid]->aggregationcoef;
+                    }
                     if ($items[$itemid]->aggregationcoef <= 0) {
                         continue;
                     }
                     $weightsum += $items[$itemid]->aggregationcoef;
                     $sum       += $items[$itemid]->aggregationcoef * $grade_value;
-                    if ($weights !== null) {
-                        $weights[$itemid] = $items[$itemid]->aggregationcoef;
-                    }
                 }
                 if ($weightsum == 0) {
                     $agg_grade = null;
index 5359b56..d21c5b1 100644 (file)
@@ -2092,7 +2092,7 @@ class global_navigation extends navigation_node {
         $featuresfunc = $cm->modname.'_supports';
         if (function_exists($featuresfunc) && $featuresfunc(FEATURE_ADVANCED_GRADING)) {
             require_once($CFG->dirroot.'/grade/grading/lib.php');
-            $gradingman = get_grading_manager($cm->context, $cm->modname);
+            $gradingman = get_grading_manager($cm->context,  'mod_'.$cm->modname);
             $gradingman->extend_navigation($this, $activity);
         }
 
@@ -3753,7 +3753,7 @@ class settings_navigation extends navigation_node {
 
         // View course reports.
         if (has_capability('moodle/site:viewreports', $coursecontext)) { // Basic capability for listing of reports.
-            $reportnav = $coursenode->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null,
+            $reportnav = $coursenode->add(get_string('reports'), null, self::TYPE_CONTAINER, null, 'coursereports',
                     new pix_icon('i/stats', ''));
             $coursereports = core_component::get_plugin_list('coursereport');
             foreach ($coursereports as $report => $dir) {
@@ -3891,6 +3891,13 @@ class settings_navigation extends navigation_node {
                 $switchroles->add($name, $url, self::TYPE_SETTING, null, $key, new pix_icon('i/switchrole', ''));
             }
         }
+
+        // Let admin tools hook into course navigation.
+        $tools = get_plugin_list_with_function('tool', 'extend_navigation_course', 'lib.php');
+        foreach ($tools as $toolfunction) {
+            $toolfunction($coursenode, $course, $coursecontext);
+        }
+
         // Return we are done
         return $coursenode;
     }
@@ -3970,7 +3977,7 @@ class settings_navigation extends navigation_node {
         $featuresfunc = $this->page->activityname.'_supports';
         if (function_exists($featuresfunc) && $featuresfunc(FEATURE_ADVANCED_GRADING) && has_capability('moodle/grade:managegradingforms', $this->page->cm->context)) {
             require_once($CFG->dirroot.'/grade/grading/lib.php');
-            $gradingman = get_grading_manager($this->page->cm->context, $this->page->activityname);
+            $gradingman = get_grading_manager($this->page->cm->context, 'mod_'.$this->page->activityname);
             $gradingman->extend_settings_navigation($this, $modulenode);
         }
 
@@ -4205,7 +4212,7 @@ class settings_navigation extends navigation_node {
             if (empty($passwordchangeurl)) {
                 $passwordchangeurl = new moodle_url('/login/change_password.php', array('id'=>$course->id));
             }
-            $usersetting->add(get_string("changepassword"), $passwordchangeurl, self::TYPE_SETTING);
+            $usersetting->add(get_string("changepassword"), $passwordchangeurl, self::TYPE_SETTING, null, 'changepassword');
         }
 
         // View the roles settings
@@ -4323,6 +4330,12 @@ class settings_navigation extends navigation_node {
             $usersetting->add(get_string('loginas'), $url, self::TYPE_SETTING);
         }
 
+        // Let admin tools hook into user settings navigation.
+        $tools = get_plugin_list_with_function('tool', 'extend_navigation_user_settings', 'lib.php');