Merge branch 'MDL-33099-master' of git://github.com/andrewnicols/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 17 Feb 2014 06:40:36 +0000 (14:40 +0800)
committerDan Poltawski <dan@moodle.com>
Mon, 17 Feb 2014 06:40:36 +0000 (14:40 +0800)
102 files changed:
admin/settings/appearance.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
cohort/locallib.php
course/renderer.php
grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php [new file with mode: 0644]
grade/grading/form/rubric/tests/behat/edit_rubric.feature [new file with mode: 0644]
grade/grading/form/rubric/tests/behat/publish_rubric_templates.feature [new file with mode: 0644]
grade/grading/form/rubric/tests/behat/reuse_own_rubrics.feature [new file with mode: 0644]
grade/grading/tests/behat/behat_grading.php [new file with mode: 0644]
grade/import/csv/index.php
lang/en/admin.php
lib/behat/classes/behat_selectors.php
lib/setuplib.php
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-debug.js
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-min.js
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip.js
lib/yui/src/dragdrop/js/dragdrop.js
lib/yui/src/notification/js/dialogue.js
lib/yui/src/tooltip/js/tooltip.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/data/classes/event/course_module_instance_list_viewed.php [new file with mode: 0644]
mod/data/classes/event/course_module_viewed.php [new file with mode: 0644]
mod/data/classes/event/field_created.php [new file with mode: 0644]
mod/data/classes/event/field_deleted.php [new file with mode: 0644]
mod/data/classes/event/field_updated.php [new file with mode: 0644]
mod/data/classes/event/record_created.php [new file with mode: 0644]
mod/data/classes/event/record_deleted.php [new file with mode: 0644]
mod/data/classes/event/record_updated.php [new file with mode: 0644]
mod/data/classes/event/template_updated.php [new file with mode: 0644]
mod/data/classes/event/template_viewed.php [new file with mode: 0644]
mod/data/edit.php
mod/data/field.php
mod/data/index.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/templates.php
mod/data/tests/events_test.php [new file with mode: 0644]
mod/data/view.php
mod/forum/classes/event/course_module_instance_list_viewed.php [new file with mode: 0644]
mod/forum/classes/event/course_searched.php [new file with mode: 0644]
mod/forum/classes/event/discussion_created.php [new file with mode: 0644]
mod/forum/classes/event/discussion_deleted.php [new file with mode: 0644]
mod/forum/classes/event/discussion_moved.php [new file with mode: 0644]
mod/forum/classes/event/discussion_updated.php [new file with mode: 0644]
mod/forum/classes/event/discussion_viewed.php [new file with mode: 0644]
mod/forum/classes/event/forum_viewed.php [new file with mode: 0644]
mod/forum/classes/event/post_created.php [new file with mode: 0644]
mod/forum/classes/event/post_deleted.php [new file with mode: 0644]
mod/forum/classes/event/post_updated.php [new file with mode: 0644]
mod/forum/classes/event/readtracking_disabled.php [new file with mode: 0644]
mod/forum/classes/event/readtracking_enabled.php [new file with mode: 0644]
mod/forum/classes/event/subscribers_viewed.php [new file with mode: 0644]
mod/forum/classes/event/subscription_created.php [new file with mode: 0644]
mod/forum/classes/event/subscription_deleted.php [new file with mode: 0644]
mod/forum/classes/event/userreport_viewed.php [new file with mode: 0644]
mod/forum/discuss.php
mod/forum/index.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/markposts.php
mod/forum/post.php
mod/forum/search.php
mod/forum/settracking.php
mod/forum/subscribe.php
mod/forum/subscribers.php
mod/forum/tests/events_test.php [new file with mode: 0644]
mod/forum/user.php
mod/forum/version.php
mod/forum/view.php
mod/glossary/lib.php
mod/quiz/lib.php
question/format.php
question/format/xml/format.php
question/format/xml/tests/xmlformat_test.php
question/type/match/renderer.php
question/type/randomsamatch/backup/moodle1/lib.php [new file with mode: 0644]
question/type/randomsamatch/backup/moodle2/backup_qtype_randomsamatch_plugin.class.php
question/type/randomsamatch/backup/moodle2/restore_qtype_randomsamatch_plugin.class.php
question/type/randomsamatch/db/install.xml
question/type/randomsamatch/db/upgrade.php [new file with mode: 0644]
question/type/randomsamatch/db/upgradelib.php [new file with mode: 0644]
question/type/randomsamatch/edit_randomsamatch_form.php
question/type/randomsamatch/lang/en/qtype_randomsamatch.php
question/type/randomsamatch/lib.php [new file with mode: 0644]
question/type/randomsamatch/question.php [new file with mode: 0644]
question/type/randomsamatch/questiontype.php
question/type/randomsamatch/renderer.php [new file with mode: 0644]
question/type/randomsamatch/tests/helper.php [new file with mode: 0644]
question/type/randomsamatch/tests/question_test.php [new file with mode: 0644]
question/type/randomsamatch/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/randomsamatch/tests/walkthrough_test.php [new file with mode: 0644]
question/type/randomsamatch/version.php

index 418ed40..1df0a09 100644 (file)
@@ -191,6 +191,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // sp
     // "documentation" settingpage
     $temp = new admin_settingpage('documentation', new lang_string('moodledocs'));
     $temp->add(new admin_setting_configtext('docroot', new lang_string('docroot', 'admin'), new lang_string('configdocroot', 'admin'), 'http://docs.moodle.org', PARAM_URL));
+    $ltemp = array('' => get_string('forceno'));
+    $ltemp += get_string_manager()->get_list_of_translations(true);
+    $temp->add(new admin_setting_configselect('doclang', get_string('doclang', 'admin'), get_string('configdoclang', 'admin'), '', $ltemp));
     $temp->add(new admin_setting_configcheckbox('doctonewwindow', new lang_string('doctonewwindow', 'admin'), new lang_string('configdoctonewwindow', 'admin'), 0));
     $ADMIN->add('appearance', $temp);
 
index 4e79cc5..5ec6bad 100644 (file)
@@ -216,7 +216,7 @@ abstract class backup_questions_activity_structure_step extends backup_activity_
 
         $qas = new backup_nested_element($nameprefix . 'question_attempts');
         $qa = new backup_nested_element($nameprefix . 'question_attempt', array('id'), array(
-                'slot', 'behaviour', 'questionid', 'maxmark', 'minfraction', 'maxfraction',
+                'slot', 'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction',
                 'flagged', 'questionsummary', 'rightanswer', 'responsesummary',
                 'timemodified'));
 
index 1045d94..68c24d6 100644 (file)
@@ -4069,6 +4069,9 @@ abstract class restore_questions_activity_structure_step extends restore_activit
 
         $data->questionusageid = $this->get_new_parentid($nameprefix . 'question_usage');
         $data->questionid      = $question->newitemid;
+        if (!property_exists($data, 'variant')) {
+            $data->variant = 1;
+        }
         $data->timemodified    = $this->apply_date_offset($data->timemodified);
 
         if (!property_exists($data, 'maxfraction')) {
index 23fa8a6..62dcfdf 100644 (file)
@@ -126,7 +126,7 @@ class cohort_existing_selector extends user_selector_base {
 
         if (!$this->is_validating()) {
             $potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params);
-            if ($potentialmemberscount > 100) {
+            if ($potentialmemberscount > $this->maxusersperpage) {
                 return $this->too_many_results($search, $potentialmemberscount);
             }
         }
index b28a98b..aab4174 100644 (file)
@@ -1583,8 +1583,6 @@ class core_course_renderer extends plugin_renderer_base {
                 $classes[] = 'with_children';
                 $classes[] = 'collapsed';
             }
-            // Make sure JS file to expand category content is included.
-            $this->coursecat_include_js();
         } else {
             // load category content
             $categorycontent = $this->coursecat_category_content($chelper, $coursecat, $depth);
@@ -1593,6 +1591,10 @@ class core_course_renderer extends plugin_renderer_base {
                 $classes[] = 'with_children';
             }
         }
+
+        // Make sure JS file to expand category content is included.
+        $this->coursecat_include_js();
+
         $content = html_writer::start_tag('div', array(
             'class' => join(' ', $classes),
             'data-categoryid' => $coursecat->id,
diff --git a/grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php b/grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php
new file mode 100644 (file)
index 0000000..bef83f9
--- /dev/null
@@ -0,0 +1,490 @@
+<?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/>.
+
+/**
+ * Steps definitions for rubrics.
+ *
+ * @package   gradingform_rubric
+ * @category  test
+ * @copyright 2013 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode,
+    Behat\Behat\Context\Step\Given as Given,
+    Behat\Behat\Context\Step\When as When,
+    Behat\Behat\Context\Step\Then as Then,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
+    Behat\Mink\Exception\ExpectationException as ExpectationException;
+
+/**
+ * Steps definitions to help with rubrics.
+ *
+ * @package   gradingform_rubric
+ * @category  test
+ * @copyright 2013 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_gradingform_rubric extends behat_base {
+
+    /**
+     * @var The number of levels added by default when a rubric is created.
+     */
+    const DEFAULT_RUBRIC_LEVELS = 3;
+
+    /**
+     * Defines the rubric with the provided data, following rubric's definition grid cells.
+     *
+     * This method fills the rubric of the rubric definition
+     * form; the provided TableNode should contain one row for
+     * each criterion and each cell of the row should contain:
+     * # Criterion description
+     * # Criterion level 1 name
+     * # Criterion level 1 points
+     * # Criterion level 2 name
+     * # Criterion level 2 points
+     * # Criterion level 3 .....
+     *
+     * Works with both JS and non-JS.
+     *
+     * @When /^I define the following rubric:$/
+     * @throws ExpectationException
+     * @param TableNode $rubric
+     */
+    public function i_define_the_following_rubric(TableNode $rubric) {
+
+        // Being a smart method is nothing good when we talk about step definitions, in
+        // this case we didn't have any other options as there are no labels no elements
+        // id we can point to without having to "calculate" them.
+
+        $steptableinfo = '| criterion description | level1 name  | level1 points | level2 name | level2 points | ...';
+
+        $criteria = $rubric->getRows();
+
+        $addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_rubric'));
+
+        // Cleaning the current ones.
+        $deletebuttons = $this->find_all('css', "input[title='" . get_string('criteriondelete', 'gradingform_rubric') . "']");
+        if ($deletebuttons) {
+
+            // We should reverse the deletebuttons because otherwise once we delete
+            // the first one the DOM will change and the [X] one will not exist anymore.
+            $deletebuttons = array_reverse($deletebuttons, true);
+            foreach ($deletebuttons as $button) {
+                $this->click_and_confirm($button);
+            }
+        }
+
+        // The level number (NEWID$N) is not reset after each criterion.
+        $levelnumber = 1;
+
+        // The next criterion is created with the same number of levels than the last criterion.
+        $defaultnumberoflevels = self::DEFAULT_RUBRIC_LEVELS;
+
+        if ($criteria) {
+            foreach ($criteria as $criterionit => $criterion) {
+
+                // Checking the number of cells.
+                if (count($criterion) % 2 === 0) {
+                    throw new ExpectationException(
+                        'The criterion levels should contain both definition and points, follow this format:' . $steptableinfo,
+                        $this->getSession()
+                    );
+                }
+
+                // Minimum 2 levels per criterion.
+                // description + definition1 + score1 + definition2 + score2 = 5.
+                if (count($criterion) < 5) {
+                    throw new ExpectationException(
+                        get_string('err_mintwolevels', 'gradingform_rubric'),
+                        $this->getSession()
+                    );
+
+                }
+
+                // Add new criterion.
+                $addcriterionbutton->click();
+
+                $criterionroot = 'rubric[criteria][NEWID' . ($criterionit + 1) . ']';
+
+                // Getting the criterion description, this one is visible by default.
+                $this->set_rubric_field_value($criterionroot . '[description]', array_shift($criterion), true);
+
+                // When JS is disabled each criterion's levels name numbers starts from 0.
+                if (!$this->running_javascript()) {
+                    $levelnumber = 0;
+                }
+
+                // Setting the correct number of levels.
+                $nlevels = count($criterion) / 2;
+                if ($nlevels < $defaultnumberoflevels) {
+
+                    // Removing levels if there are too much levels.
+                    // When we add a new level the NEWID$N is increased from the last criterion.
+                    $lastcriteriondefaultlevel = $defaultnumberoflevels + $levelnumber - 1;
+                    $lastcriterionlevel = $nlevels + $levelnumber - 1;
+                    for ($i = $lastcriteriondefaultlevel; $i > $lastcriterionlevel; $i--) {
+
+                        // If JS is disabled seems that new levels are not added.
+                        if ($this->running_javascript()) {
+                            $deletelevel = $this->find_button($criterionroot . '[levels][NEWID' . $i . '][delete]');
+                            $this->click_and_confirm($deletelevel);
+
+                        } else {
+                            // Only if the level exists.
+                            $buttonname = $criterionroot . '[levels][NEWID' . $i . '][delete]';
+                            if ($deletelevel = $this->getSession()->getPage()->findButton($buttonname)) {
+                                $deletelevel->click();
+                            }
+                        }
+                    }
+                } else if ($nlevels > $defaultnumberoflevels) {
+                    // Adding levels if we don't have enough.
+                    $addlevel = $this->find_button($criterionroot . '[levels][addlevel]');
+                    for ($i = ($defaultnumberoflevels + 1); $i <= $nlevels; $i++) {
+                        $addlevel->click();
+                    }
+                }
+
+                // Updating it.
+                if ($nlevels > self::DEFAULT_RUBRIC_LEVELS) {
+                    $defaultnumberoflevels = $nlevels;
+                } else {
+                    // If it is less than the default value it sets it to
+                    // the default value.
+                    $defaultnumberoflevels = self::DEFAULT_RUBRIC_LEVELS;
+                }
+
+                foreach ($criterion as $i => $value) {
+
+                    $levelroot = $criterionroot . '[levels][NEWID' . $levelnumber . ']';
+
+                    if ($i % 2 === 0) {
+                        // Pairs are the definitions.
+                        $fieldname = $levelroot . '[definition]';
+                        $this->set_rubric_field_value($fieldname, $value);
+
+                    } else {
+                        // Odds are the points.
+
+                        // Checking it now, we would need to remove it if we are testing the form validations...
+                        if (!is_numeric($value)) {
+                            throw new ExpectationException(
+                                'The points cells should contain numeric values, follow this format: ' . $steptableinfo,
+                                $this->getSession()
+                            );
+                        }
+
+                        $fieldname = $levelroot . '[score]';
+                        $this->set_rubric_field_value($fieldname, $value, true);
+
+                        // Increase the level by one every 2 cells.
+                        $levelnumber++;
+                    }
+
+                }
+            }
+        }
+    }
+
+    /**
+     * Replaces a value from the specified criterion. You can use it when editing rubrics, to set both name or points.
+     *
+     * @When /^I replace "(?P<current_value_string>(?:[^"]|\\")*)" rubric level with "(?P<value_string>(?:[^"]|\\")*)" in "(?P<criterion_string>(?:[^"]|\\")*)" criterion$/
+     * @throws ElementNotFoundException
+     * @param string $currentvalue
+     * @param string $value
+     * @param string $criterionname
+     * @return Given[]
+     */
+    public function i_replace_rubric_level_with($currentvalue, $value, $criterionname) {
+
+        $currentvalueliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($currentvalue);
+        $criterionliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($criterionname);
+
+        $criterionxpath = "//div[@id='rubric-rubric']" .
+            "/descendant::td[contains(concat(' ', normalize-space(@class), ' '), ' description ')]";
+        // It differs between JS on/off.
+        if ($this->running_javascript()) {
+            $criterionxpath .= "/descendant::span[@class='textvalue'][text()=$criterionliteral]" .
+                "/ancestor::tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]";
+        } else {
+            $criterionxpath .= "/descendant::textarea[text()=$criterionliteral]" .
+                "/ancestor::tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]";
+        }
+
+        $inputxpath = $criterionxpath .
+            "/descendant::input[@type='text'][@value=$currentvalueliteral]";
+        $textareaxpath = $criterionxpath .
+            "/descendant::textarea[text()=$currentvalueliteral]";
+
+        if ($this->running_javascript()) {
+
+            $spansufix = "/ancestor::div[@class='level-wrapper']" .
+                "/descendant::div[@class='definition']" .
+                "/descendant::span[@class='textvalue']";
+
+            // Expanding the level input boxes.
+            $spannode = $this->find('xpath', $inputxpath . $spansufix . '|' . $textareaxpath . $spansufix);
+            $spannode->click();
+
+            $inputfield = $this->find('xpath', $inputxpath . '|' . $textareaxpath);
+            $inputfield->setValue($value);
+
+        } else {
+            $fieldnode = $this->find('xpath', $inputxpath . '|' . $textareaxpath);
+            $this->set_rubric_field_value($fieldnode->getAttribute('name'), $value);
+        }
+
+    }
+
+    /**
+     * Grades filling the current page rubric. Set one line per criterion and for each criterion set "| Criterion name | Points | Remark |".
+     *
+     * @When /^I grade by filling the rubric with:$/
+     *
+     * @throws ExpectationException
+     * @param TableNode $rubric
+     * @return void
+     */
+    public function i_grade_by_filling_the_rubric_with(TableNode $rubric) {
+
+        $criteria = $rubric->getRowsHash();
+
+        $stepusage = '"I grade by filling the rubric with:" step needs you to provide a table where each row is a criterion' .
+            ' and each criterion has 3 different values: | Criterion name | Number of points | Remark text |';
+
+        // To fill with the steps to execute.
+        $steps = array();
+
+        // First element -> name, second -> points, third -> Remark.
+        foreach ($criteria as $name => $criterion) {
+
+            // We only expect the points and the remark, as the criterion name is $name.
+            if (count($criterion) !== 2) {
+                throw new ExpectationException($stepusage, $this->getSession());
+            }
+
+            // Numeric value here.
+            $points = $criterion[0];
+            if (!is_numeric($points)) {
+                throw new ExpectationException($stepusage, $this->getSession());
+            }
+
+            // Selecting a value.
+            // When JS is disabled there are radio options, with JS enabled divs.
+            $selectedlevelxpath = $this->get_level_xpath($points);
+            if ($this->running_javascript()) {
+
+                // Only clicking on the selected level if it was not already selected.
+                $levelnode = $this->find('xpath', $selectedlevelxpath);
+
+                // Using in_array() as there are only a few elements.
+                if (!in_array('checked', explode(' ', $levelnode->getAttribute('class')))) {
+                    $steps[] = new Given('I click on "' . $selectedlevelxpath . '" "xpath_element" in the "' .
+                        $this->escape($name) . '" "table_row"');
+                }
+
+            } else {
+
+                // Getting the name of the field.
+                $radioxpath = $this->get_criterion_xpath($name) .
+                    $selectedlevelxpath . "/descendant::input[@type='radio']";
+                $radionode = $this->find('xpath', $radioxpath);
+                // TODO MDL-43738: Change setValue() to use the generic set_value()
+                // which will delegate the process to the field type.
+                $radionode->setValue($radionode->getAttribute('value'));
+            }
+
+            // Setting the remark.
+
+            // First we need to get the textarea name, then we can set the value.
+            $textarea = $this->get_node_in_container('css_element', 'textarea', 'table_row', $name);
+            $steps[] = new Given('I fill in "' . $textarea->getAttribute('name') . '" with "' . $criterion[1] . '"');
+        }
+
+        return $steps;
+    }
+
+    /**
+     * Checks that the level was previously selected and the user changed to another level.
+     *
+     * @Then /^the level with "(?P<points_number>\d+)" points was previously selected for the rubric criterion "(?P<criterion_name_string>(?:[^"]|\\")*)"$/
+     * @throws ExpectationException
+     * @param string $criterionname
+     * @param int $points
+     * @return void
+     */
+    public function the_level_with_points_was_previously_selected_for_the_rubric_criterion($points, $criterionname) {
+
+        $levelxpath = $this->get_criterion_xpath($criterionname) .
+            $this->get_level_xpath($points) .
+            "[contains(concat(' ', normalize-space(@class), ' '), ' currentchecked ')]";
+
+        // Works both for JS and non-JS.
+        // - JS: Class -> checked is there when is marked as green.
+        // - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a
+        //   grade @class contains checked.
+        $levelxpath .= "[not(contains(concat(' ', normalize-space(@class), ' '), ' checked '))]" .
+            "[not(/descendant::input[@type='radio'][@checked!='checked'])]";
+
+        try {
+            $this->find('xpath', $levelxpath);
+        } catch (ElementNotFoundException $e) {
+            throw new ExpectationException('"' . $points . '" points level was not previously selected', $this->getSession());
+        }
+    }
+
+    /**
+     * Checks that the level is currently selected. Works both when grading rubrics and viewing graded rubrics.
+     *
+     * @Then /^the level with "(?P<points_number>\d+)" points is selected for the rubric criterion "(?P<criterion_name_string>(?:[^"]|\\")*)"$/
+     * @throws ExpectationException
+     * @param string $criterionname
+     * @param int $points
+     * @return void
+     */
+    public function the_level_with_points_is_selected_for_the_rubric_criterion($points, $criterionname) {
+
+        $levelxpath = $this->get_criterion_xpath($criterionname) .
+            $this->get_level_xpath($points);
+
+        // Works both for JS and non-JS.
+        // - JS: Class -> checked is there when is marked as green.
+        // - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a
+        //   grade @class contains checked.
+        $levelxpath .= "[" .
+            "contains(concat(' ', normalize-space(@class), ' '), ' checked ')" .
+            " or " .
+            "/descendant::input[@type='radio'][@checked='checked']" .
+            "]";
+
+        try {
+            $this->find('xpath', $levelxpath);
+        } catch (ElementNotFoundException $e) {
+            throw new ExpectationException('"' . $points . '" points level is not selected', $this->getSession());
+        }
+    }
+
+    /**
+     * Checks that the level is not currently selected. Works both when grading rubrics and viewing graded rubrics.
+     *
+     * @Then /^the level with "(?P<points_number>\d+)" points is not selected for the rubric criterion "(?P<criterion_name_string>(?:[^"]|\\")*)"$/
+     * @throws ExpectationException
+     * @param string $criterionname
+     * @param int $points
+     * @return void
+     */
+    public function the_level_with_points_is_not_selected_for_the_rubric_criterion($points, $criterionname) {
+
+        $levelxpath = $this->get_criterion_xpath($criterionname) .
+            $this->get_level_xpath($points);
+
+        // Works both for JS and non-JS.
+        // - JS: Class -> checked is there when is marked as green.
+        // - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a
+        //   grade @class contains checked.
+        $levelxpath .= "[not(contains(concat(' ', normalize-space(@class), ' '), ' checked '))]" .
+            "[./descendant::input[@type='radio'][@checked!='checked'] or not(./descendant::input[@type='radio'])]";
+
+        try {
+            $this->find('xpath', $levelxpath);
+        } catch (ElementNotFoundException $e) {
+            throw new ExpectationException('"' . $points . '" points level is selected', $this->getSession());
+        }
+    }
+
+
+    /**
+     * Makes a hidden rubric field visible (if necessary) and sets a value on it.
+     *
+     * @param string $name The name of the field
+     * @param string $value The value to set
+     * @param bool $visible
+     * @return void
+     */
+    protected function set_rubric_field_value($name, $value, $visible = false) {
+
+        // Fields are hidden by default.
+        if ($this->running_javascript() == true && $visible === false) {
+            $xpath = "//*[@name='$name']/following-sibling::*[contains(concat(' ', normalize-space(@class), ' '), ' plainvalue ')]";
+            $textnode = $this->find('xpath', $xpath);
+            $textnode->click();
+        }
+
+        // Set the value now.
+        $description = $this->find_field($name);
+        $description->setValue($value);
+    }
+
+    /**
+     * Performs click confirming the action.
+     *
+     * @param NodeElement $node
+     * @return void
+     */
+    protected function click_and_confirm($node) {
+
+        // Clicks to perform the action.
+        $node->click();
+
+        // Confirms the delete.
+        if ($this->running_javascript()) {
+            $confirmbutton = $this->get_node_in_container(
+                'button',
+                get_string('yes'),
+                'dialogue',
+                get_string('confirmation', 'admin')
+            );
+            $confirmbutton->click();
+        }
+    }
+
+    /**
+     * Returns the xpath representing a selected level.
+     *
+     * It is not including the path to the criterion.
+     *
+     * It is the xpath when grading a rubric or viewing a rubric,
+     * it is not the same xpath when editing a rubric.
+     *
+     * @param int $points
+     * @return string
+     */
+    protected function get_level_xpath($points) {
+        return "//td[contains(concat(' ', normalize-space(@class), ' '), ' level ')]" .
+            "[./descendant::span[@class='scorevalue'][text()='$points']]";
+    }
+
+    /**
+     * Returns the xpath representing the selected criterion.
+     *
+     * It is the xpath when grading a rubric or viewing a rubric,
+     * it is not the same xpath when editing a rubric.
+     *
+     * @param string $criterionname Literal including the criterion name.
+     * @return string
+     */
+    protected function get_criterion_xpath($criterionname) {
+        $literal = $this->getSession()->getSelectorsHandler()->xpathLiteral($criterionname);
+        return "//tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]" .
+            "[./descendant::td[@class='description'][text()=$literal]]";
+    }
+}
diff --git a/grade/grading/form/rubric/tests/behat/edit_rubric.feature b/grade/grading/form/rubric/tests/behat/edit_rubric.feature
new file mode 100644 (file)
index 0000000..2ff3a9a
--- /dev/null
@@ -0,0 +1,159 @@
+@gradingform @gradingform_rubric
+Feature: Rubrics can be created and edited
+  In order to use and refine rubrics to grade students
+  As a teacher
+  I need to edit previously used rubrics
+
+  Background:
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+      | student1 | Student | 1 | student1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment 1 name |
+      | Description | Test assignment description |
+      | Grading method | Rubric |
+    When I go to "Test assignment 1 name" advanced grading definition page
+    # Defining a rubric.
+    And I fill the moodle form with:
+      | Name | Assignment 1 rubric |
+      | Description | Rubric test description |
+    And I define the following rubric:
+      | TMP Criterion 1 | TMP Level 11 | 11 | TMP Level 12 | 12 |
+      | TMP Criterion 2 | TMP Level 21 | 21 | TMP Level 22 | 22 |
+      | TMP Criterion 3 | TMP Level 31 | 31 | TMP Level 32 | 32 |
+      | TMP Criterion 4 | TMP Level 41 | 41 | TMP Level 42 | 42 |
+    # Checking that only the last ones are saved.
+    And I define the following rubric:
+      | Criterion 1 | Level 11 | 1 | Level 12 | 20 | Level 13 | 40 | Level 14 | 50 |
+      | Criterion 2 | Level 21 | 10 | Level 22 | 20 | Level 23 | 30 |
+      | Criterion 3 | Level 31 | 5 | Level 32 | 20 |
+    And I press "Save as draft"
+    And I go to "Test assignment 1 name" advanced grading definition page
+    And I click on "Move down" "button" in the "Criterion 1" "table_row"
+    And I press "Save rubric and make it ready"
+    Then I should see "Ready for use"
+    # Grading two students.
+    And I go to "Student 1" "Test assignment 1 name" activity advanced grading page
+    And I grade by filling the rubric with:
+      | Criterion 1 | 50 | Very good |
+    And I press "Save changes"
+    # Checking that it complains if you don't select a level for each criterion.
+    And I should see "Please choose something for each criterion"
+    And I grade by filling the rubric with:
+      | Criterion 1 | 50 | Very good |
+      | Criterion 2 | 10 | Mmmm, you can do it better |
+      | Criterion 3 | 5 | Not good |
+    And I complete the advanced grading form with these values:
+      | Feedback comments | In general... work harder... |
+    # Checking that the user grade is correct.
+    And I should see "58.33" in the "Student 1" "table_row"
+    # Updating the user grade.
+    And I go to "Student 1" "Test assignment 1 name" activity advanced grading page
+    And I grade by filling the rubric with:
+      | Criterion 1 | 20 | Bad, I changed my mind |
+      | Criterion 2 | 10 | Mmmm, you can do it better |
+      | Criterion 3 | 5 | Not good |
+    #And the level with "50" points was previously selected for the rubric criterion "Criterion 1"
+    #And the level with "20" points is selected for the rubric criterion "Criterion 1"
+    And I save the advanced grading form
+    And I should see "22.62" in the "Student 1" "table_row"
+    And I log out
+    # Viewing it as a student.
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1 name"
+    And I should see "22.62" in the ".feedback" "css_element"
+    And I should see "Rubric test description" in the ".feedback" "css_element"
+    And I should see "In general... work harder..."
+    And the level with "10" points is selected for the rubric criterion "Criterion 2"
+    And the level with "20" points is selected for the rubric criterion "Criterion 1"
+    And the level with "5" points is selected for the rubric criterion "Criterion 3"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    # Editing a rubric definition without regrading students.
+    And I go to "Test assignment 1 name" advanced grading definition page
+    And "Save as draft" "button" should not exists
+    And I click on "Move up" "button" in the "Criterion 1" "table_row"
+    And I replace "Level 11" rubric level with "Level 11 edited" in "Criterion 1" criterion
+    And I press "Save"
+    And I should see "You are about to save changes to a rubric that has already been used for grading."
+    And I select "Do not mark for regrade" from "menurubricregrade"
+    And I press "Continue"
+    And I log out
+    # Check that the student still sees the grade.
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1 name"
+    And I should see "22.62" in the ".feedback" "css_element"
+    And the level with "20" points is selected for the rubric criterion "Criterion 1"
+    And I log out
+    # Editing a rubric with significant changes.
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I go to "Test assignment 1 name" advanced grading definition page
+    And I click on "Move down" "button" in the "Criterion 2" "table_row"
+    And I replace "1" rubric level with "11" in "Criterion 1" criterion
+    And I press "Save"
+    And I should see "You are about to save significant changes to a rubric that has already been used for grading. The gradebook value will be unchanged, but the rubric will be hidden from students until their item is regraded."
+    And I press "Continue"
+    And I log out
+    # Check that the student doesn't see the grade.
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1 name"
+    And I should see "22.62" in the ".feedback" "css_element"
+    And the level with "20" points is not selected for the rubric criterion "Criterion 1"
+    And I log out
+    # Regrade student.
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1 name"
+    And I go to "Student 1" "Test assignment 1 name" activity advanced grading page
+    And I should see "The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade."
+    And I save the advanced grading form
+    And I log out
+    # Check that the student sees the grade again.
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1 name"
+    And I should see "12.16" in the ".feedback" "css_element"
+    And the level with "20" points is not selected for the rubric criterion "Criterion 1"
+    # Hide all rubric info for students
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I go to "Test assignment 1 name" advanced grading definition page
+    And I uncheck "Allow users to preview rubric used in the module (otherwise rubric will only become visible after grading)"
+    And I uncheck "Display rubric description during evaluation"
+    And I uncheck "Display rubric description to those being graded"
+    And I uncheck "Display points for each level during evaluation"
+    And I uncheck "Display points for each level to those being graded"
+    And I press "Save"
+    And I select "Do not mark for regrade" from "menurubricregrade"
+    And I press "Continue"
+    And I log out
+    # Students should not see anything.
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1 name"
+    And I should not see "Criterion 1" in the ".submissionstatustable" "css_element"
+    And I should not see "Criterion 2" in the ".submissionstatustable" "css_element"
+    And I should not see "Criterion 3" in the ".submissionstatustable" "css_element"
+    And I should not see "Rubric test description" in the ".feedback" "css_element"
+
+  @javascript
+  Scenario: I can use rubrics to grade and edit them later updating students grades with Javascript enabled
+
+  Scenario: I can use rubrics to grade and edit them later updating students grades with Javascript disabled
diff --git a/grade/grading/form/rubric/tests/behat/publish_rubric_templates.feature b/grade/grading/form/rubric/tests/behat/publish_rubric_templates.feature
new file mode 100644 (file)
index 0000000..302a12f
--- /dev/null
@@ -0,0 +1,56 @@
+@gradingform @gradingform_rubric
+Feature: Publish rubrics as templates
+  In order to save time to teachers
+  As a manager
+  I need to publish rubrics and make them available to all teachers
+
+  Background:
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+      | manager1 | Manager | 1 | manager1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "activities" exists:
+      | activity | course | idnumber | name                   | intro | advancedgradingmethod_submissions |
+      | assign   | C1     | A1       | Test assignment 1 name | TA1   | rubric                            |
+      | assign   | C1     | A2       | Test assignment 2 name | TA2   | rubric                            |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And the following "system role assigns" exists:
+      | user | role | contextlevel | reference |
+      | manager1 | manager | System | |
+    And I log in as "manager1"
+    And I follow "Course 1"
+    And I go to "Test assignment 1 name" advanced grading definition page
+    And I fill the moodle form with:
+      | Name | Assignment 1 rubric |
+      | Description | Assignment 1 description |
+    And I define the following rubric:
+      | Criterion 1 | Level 11 | 11 | Level 12 | 12 |
+      | Criterion 2 | Level 21 | 21 | Level 22 | 22 |
+    And I press "Save rubric and make it ready"
+    When I publish "Test assignment 1 name" grading form definition as a public template
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I set "Test assignment 2 name" activity to use "Assignment 1 rubric" grading form
+    Then I should see "Advanced grading: Test assignment 2 name (Submissions)"
+    And I should see "Criterion 1"
+    And I should see "Assignment 1 description"
+    And I go to "Test assignment 2 name" advanced grading definition page
+    And I should see "Current rubric status"
+
+  @javascript
+  Scenario: Create a rubric template and reuse it as a teacher, with Javascript enabled
+    Then the "Description" field should match "<p>Assignment 1 description</p>" value
+    And I should see "Criterion 1"
+    And I press "Cancel"
+
+  Scenario: Create a rubric template and reuse it as a teacher, with Javascript disabled
+    Then the "Description" field should match "Assignment 1 description" value
+    # Trying to avoid pointing by id or name as the code internals may change.
+    And "//table[@class='criteria']//textarea[text()='Criterion 1']" "xpath_element" should exists
+    And I press "Cancel"
diff --git a/grade/grading/form/rubric/tests/behat/reuse_own_rubrics.feature b/grade/grading/form/rubric/tests/behat/reuse_own_rubrics.feature
new file mode 100644 (file)
index 0000000..0c3799c
--- /dev/null
@@ -0,0 +1,52 @@
+@gradingform @gradingform_rubric
+Feature: Reuse my rubrics in other activities
+  In order to save time creating duplicated grading forms
+  As a teacher
+  I need to reuse rubrics that I created previously
+
+  Background:
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment 1 name |
+      | Description | Test assignment 1 description |
+      | Grading method | Rubric |
+    And I go to "Test assignment 1 name" advanced grading definition page
+    And I fill the moodle form with:
+      | Name | Assignment 1 rubric |
+      | Description | Assignment 1 description |
+    And I define the following rubric:
+      | Criterion 1 | Level 11 | 11 | Level 12 | 12 | Level 3 | 13 |
+      | Criterion 2 | Level 21 | 21 | Level 22 | 22 | Level 3 | 23 |
+      | Criterion 3 | Level 31 | 31 | Level 32 | 32 |
+    And I press "Save rubric and make it ready"
+    And I follow "Course 1"
+    When I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment 2 name |
+      | Description | Test assignment 2 description |
+      | Grading method | Rubric |
+    And I set "Test assignment 2 name" activity to use "Assignment 1 rubric" grading form
+    Then I should see "Ready for use"
+    And I should see "Criterion 1"
+    And I should see "Criterion 2"
+    And I should see "Criterion 3"
+    And I go to "Test assignment 1 name" advanced grading definition page
+    And I should see "Criterion 1"
+    And I should see "Criterion 2"
+    And I should see "Criterion 3"
+    And I press "Cancel"
+
+  @javascript
+  Scenario: A teacher can reuse one of his/her previously created rubrics, with Javascript enabled
+
+  Scenario: A teacher can reuse one of his/her previously created rubrics, with Javascript disabled
diff --git a/grade/grading/tests/behat/behat_grading.php b/grade/grading/tests/behat/behat_grading.php
new file mode 100644 (file)
index 0000000..3c60cd6
--- /dev/null
@@ -0,0 +1,181 @@
+<?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/>.
+
+/**
+ * Grading methods steps definitions.
+ *
+ * @package   core_grading
+ * @category  test
+ * @copyright 2013 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode,
+    Behat\Behat\Context\Step\Given as Given,
+    Behat\Behat\Context\Step\When as When;
+
+/**
+ * Generic grading methods step definitions.
+ *
+ * @package   core_grading
+ * @category  test
+ * @copyright 2013 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_grading extends behat_base {
+
+    /**
+     * Goes to the selected advanced grading page. You should be in the course page when this step begins.
+     *
+     * @Given /^I go to "(?P<activity_name_string>(?:[^"]|\\")*)" advanced grading page$/
+     * @param string $activityname
+     * @return Given[]
+     */
+    public function i_go_to_advanced_grading_page($activityname) {
+        return array(
+            new Given('I follow "' . $this->escape($activityname) . '"'),
+            new Given('I follow "' . get_string('gradingmanagement', 'grading') . '"'),
+        );
+    }
+
+    /**
+     * Goes to the selected advanced grading definition page. You should be in the course page when this step begins.
+     *
+     * @Given /^I go to "(?P<activity_name_string>(?:[^"]|\\")*)" advanced grading definition page$/
+     * @param string $activityname
+     * @return Given[]
+     */
+    public function i_go_to_advanced_grading_definition_page($activityname) {
+
+        // Transforming to literals, probably not necessary, just in case.
+        $newactionliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string("manageactionnew", "grading"));
+        $editactionliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string("manageactionedit", "grading"));
+
+        // Working both when adding and editing.
+        $definitionxpath = "//a[@class='action']" .
+            "[./descendant::*[contains(., $newactionliteral) or contains(., $editactionliteral)]]";
+
+        return array(
+            new Given('I go to "' . $this->escape($activityname) . '" advanced grading page'),
+            new Given('I click on "' . $this->escape($definitionxpath) . '" "xpath_element"'),
+        );
+    }
+    /**
+     * Goes to the student's advanced grading page.
+     *
+     * @Given /^I go to "(?P<user_fullname_string>(?:[^"]|\\")*)" "(?P<activity_name_string>(?:[^"]|\\")*)" activity advanced grading page$/
+     * @param string $userfullname The user full name including firstname and lastname.
+     * @param string $activityname The activity name
+     * @return Given[]
+     */
+    public function i_go_to_activity_advanced_grading_page($userfullname, $activityname) {
+
+        // Step to access the user grade page from the grading page.
+        $usergradetext = get_string('gradeuser', 'assign', $userfullname);
+        $gradeuserstep = new Given('I follow "' . $this->escape($usergradetext) . '"');
+
+        // Shortcut in case we already are in the grading page.
+        $usergradetextliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($usergradetext);
+        if ($this->getSession()->getPage()->find('named', array('link', $usergradetextliteral))) {
+            return $gradeuserstep;
+        }
+
+        return array(
+            new Given('I follow "' . $this->escape($activityname) . '"'),
+            new Given('I follow "' . $this->escape(get_string('viewgrading', 'assign')) . '"'),
+            $gradeuserstep
+        );
+    }
+
+    /**
+     * Publishes current activity grading defined form as a public template.
+     *
+     * @Given /^I publish "(?P<activity_name_string>(?:[^"]|\\")*)" grading form definition as a public template$/
+     * @param string $activityname
+     * @return Given[]
+     */
+    public function i_publish_grading_form_definition_as_a_public_template($activityname) {
+
+        return array(
+            new Given('I go to "' . $this->escape($activityname) . '" advanced grading page'),
+            new Given('I click on "' . $this->escape(get_string("manageactionshare", "grading")) . '" "link"'),
+            new Given('I press "' . get_string('continue') . '"')
+        );
+    }
+
+    /**
+     * Sets a previously created grading form as the activity grading form.
+     *
+     * @Given /^I set "(?P<activity_name_string>(?:[^"]|\\")*)" activity to use "(?P<grading_form_template_string>(?:[^"]|\\")*)" grading form$/
+     * @param string $activityname
+     * @param string $templatename
+     * @return Given[]
+     */
+    public function i_set_activity_to_use_grading_form($activityname, $templatename) {
+
+        $templateliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($templatename);
+
+        $templatexpath = "//h2[@class='template-name'][contains(., $templateliteral)]/" .
+            "following-sibling::div[contains(concat(' ', normalize-space(@class), ' '), ' template-actions ')]";
+
+        // Should work with both templates and own forms.
+        $literaltemplate = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('templatepick', 'grading'));
+        $literalownform = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('templatepickownform', 'grading'));
+        $usetemplatexpath = "//a[./descendant::div[text()=$literaltemplate]]|" .
+            "//a[./descendant::div[text()=$literalownform]]";
+
+        return array(
+            new Given('I go to "' . $this->escape($activityname) . '" advanced grading page'),
+            new Given('I follow "' . $this->escape(get_string('manageactionclone', 'grading')) . '"'),
+            new Given('I check "' . get_string('searchownforms', 'grading') . '"'),
+            new Given('I click on "' . get_string('search') . '" "button" in the "region-main" "region"'),
+            new Given('I click on "' . $this->escape($usetemplatexpath) . '" "xpath_element" ' .
+                'in the "' . $this->escape($templatexpath) . '" "xpath_element"'),
+            new Given('I press "' . get_string('continue') . '"')
+        );
+    }
+
+    /**
+     * Saves the current page advanced grading form.
+     *
+     * @When /^I save the advanced grading form$/
+     * @return When[]
+     */
+    public function i_save_the_advanced_grading_form() {
+        return array(
+            new When('I press "' . get_string('savechanges') . '"'),
+            new When('I press "' . get_string('continue') . '"')
+        );
+    }
+
+    /**
+     * Grades an activity using advanced grading. Note the grade is set by other steps, depending on the grading method.
+     *
+     * @Given /^I complete the advanced grading form with these values:$/
+     * @param TableNode $data
+     * @return Given[]
+     */
+    public function i_complete_the_advanced_grading_form_with_these_values(TableNode $data) {
+        return array(
+            new Given('I fill the moodle form with:', $data),
+            new Given('I save the advanced grading form')
+        );
+    }
+}
index 07d6e2e..990764c 100644 (file)
@@ -235,7 +235,7 @@ if ($formdata = $mform2->get_data()) {
                     $studentid = $value;
                 break;
                 case 'useridnumber':
-                    if (!$user = $DB->get_record('user', array('idnumber' => $value))) {
+                    if (empty($value) || !$user = $DB->get_record('user', array('idnumber' => $value))) {
                          // user not found, abort whole import
                         import_cleanup($importcode);
                         echo $OUTPUT->notification("user mapping error, could not find user with idnumber \"$value\"");
index 97c8815..16a05fb 100644 (file)
@@ -192,6 +192,7 @@ $string['configenabledevicedetection'] = 'Enables detection of mobiles, smartpho
 $string['configdisableuserimages'] = 'Disable the ability for users to change user profile images.';
 $string['configdisplayloginfailures'] = 'This will display information to selected users about previous failed logins.';
 $string['configdndallowtextandlinks'] = 'Enable or disable the dragging and dropping of text and links onto a course page, alongside the dragging and dropping of files. Note that the dragging of text into Firefox or between different browsers is unreliable and may result in no data being uploaded, or corrupted text being uploaded.';
+$string['configdoclang'] = 'This language will be used in links for the documentation pages.';
 $string['configdocroot'] = 'Defines the path to the Moodle Docs for providing context-specific documentation via \'Moodle Docs for this page\' links in the footer of each page. If the field is left blank, links will not be displayed.';
 $string['configdoctonewwindow'] = 'If you enable this, then links to Moodle Docs will be shown in a new window.';
 $string['configeditordictionary'] = 'This value will be used if aspell doesn\'t have dictionary for users own language.';
@@ -428,6 +429,7 @@ $string['disableuserimages'] = 'Disable user profile images';
 $string['displayerrorswarning'] = 'Enabling the PHP setting <em>display_errors</em> is not recommended on production sites because some error messages may reveal sensitive information about your server.';
 $string['displayloginfailures'] = 'Display login failures to';
 $string['dndallowtextandlinks'] = 'Drag and drop upload of text/links';
+$string['doclang'] = 'Language for docs';
 $string['docroot'] = 'Moodle Docs document root';
 $string['doctonewwindow'] = 'Open in new window';
 $string['download'] = 'Download';
index b0153d9..48688b0 100644 (file)
@@ -83,10 +83,10 @@ class behat_selectors {
      */
     protected static $moodleselectors = array(
         'dialogue' => <<<XPATH
-.//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ')]/descendant::h1[normalize-space(.) = %locator%]/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ')]
+//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ')]/descendant::h1[normalize-space(.) = %locator%]/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ')] | //div[contains(concat(' ', normalize-space(@class), ' '), ' yui-dialog ')]/descendant::div[@class='hd'][normalize-space(.) = %locator%]/parent::div
 XPATH
         , 'block' => <<<XPATH
-.//div[contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' '))] | .//div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]/descendant::h2[normalize-space(.) = %locator%]/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]
+//div[contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' '))] | //div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]/descendant::h2[normalize-space(.) = %locator%]/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]
 XPATH
         , 'region' => <<<XPATH
 .//*[self::div | self::section | self::aside][./@id = %locator%]
index 799e53d..25f456b 100644 (file)
@@ -629,10 +629,16 @@ function get_docs_url($path = null) {
         // that will ensure people end up at the latest version of the docs.
         $branch = '.';
     }
-    if (!empty($CFG->docroot)) {
-        return $CFG->docroot . '/' . $branch . '/' . current_language() . '/' . $path;
+    if (empty($CFG->doclang)) {
+        $lang = current_language();
     } else {
-        return 'http://docs.moodle.org/'. $branch . '/' . current_language() . '/' . $path;
+        $lang = $CFG->doclang;
+    }
+    $end = '/' . $branch . '/' . $lang . '/' . $path;
+    if (empty($CFG->docroot)) {
+        return 'http://docs.moodle.org'. $end;
+    } else {
+        return $CFG->docroot . $end ;
     }
 }
 
index 9a609a5..af66f37 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js differ
index 54e334f..991fbbb 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js differ
index 9a609a5..af66f37 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js differ
index 2e4bc5d..2abf031 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js differ
index 313aa1c..340f083 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js differ
index 27a544a..6035f54 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js differ
index 03408fe..74972f7 100644 (file)
Binary files a/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-debug.js and b/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-debug.js differ
index c31276b..7697ff3 100644 (file)
Binary files a/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-min.js and b/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-min.js differ
index 03408fe..74972f7 100644 (file)
Binary files a/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip.js and b/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip.js differ
index a77fe23..00a8e9f 100644 (file)
@@ -536,6 +536,15 @@ Y.extend(DRAGDROP, Y.Base, {
         // Simulate the full sequence.
         this.drag_start(dragevent);
         this.global_drop_over(dropevent);
+
+        if (droptarget.hasClass(this.parentnodeclass) && droptarget.contains(dragcontainer)) {
+            // The global_drop_over function does not handle the case where an item was moved up, without the
+            // 'goingup' variable being set, as is the case wih keyboard drag/drop. We must detect this case and
+            // apply it after the drop_over, but before the drop_hit event in order for it to be moved to the
+            // correct location.
+            droptarget.prepend(dragcontainer);
+        }
+
         this.global_drop_hit(dropevent);
     },
 
index 2f3424a..bc684e4 100644 (file)
@@ -74,7 +74,7 @@ Y.extend(DIALOGUE, Y.Panel, {
      * @method initializer
      * @return void
      */
-    initializer : function(config) {
+    initializer : function() {
         var bb;
 
         if (this.get('render')) {
@@ -82,7 +82,7 @@ Y.extend(DIALOGUE, Y.Panel, {
         }
         this.makeResponsive();
         this.after('visibleChange', this.visibilityChanged, this);
-        if (config.center) {
+        if (this.get('center')) {
             this.centerDialogue();
         }
         this.set('COUNT', COUNT);
@@ -95,10 +95,10 @@ Y.extend(DIALOGUE, Y.Panel, {
         // and allow setting of z-index in theme.
         bb = this.get('boundingBox');
 
-        if (config.extraClasses) {
-            Y.Array.each(config.extraClasses, bb.addClass, bb);
-        }
-        if (config.visible) {
+        // Add any additional classes that were specified.
+        Y.Array.each(this.get('extraClasses'), bb.addClass, bb);
+
+        if (this.get('visible')) {
             this.applyZIndex();
         }
         // Recalculate the zIndex every time the modal is altered.
@@ -107,7 +107,7 @@ Y.extend(DIALOGUE, Y.Panel, {
         // either by centerDialogue or makeResonsive. This is because the show() will trigger
         // a focus on the dialogue, which will scroll the page. If the dialogue has not
         // been positioned it will scroll back to the top of the page.
-        if (config.visible) {
+        if (this.get('visible')) {
             this.show();
             this.keyDelegation();
         }
@@ -309,7 +309,15 @@ Y.extend(DIALOGUE, Y.Panel, {
         return result;
     },
 
-    hide: function() {
+    hide: function(e) {
+        if (e) {
+            // If the event was closed by an escape key event, then we need to check that this
+            // dialogue is currently focused to prevent closing all dialogues in the stack.
+            if (e.type === 'key' && e.keyCode === 27 && !this.get('focused')) {
+                return;
+            }
+        }
+
         // Unlock scroll if the plugin is present.
         if (this.lockScroll) {
             this.lockScroll.disableScrollLock();
@@ -536,6 +544,17 @@ Y.Base.modifyAttrs(DIALOGUE, {
     render : {
         value : true,
         writeOnce : true
+    },
+
+    /**
+     * Any additional classes to add to the boundingBox.
+     *
+     * @attributes extraClasses
+     * @type Array
+     * @default []
+     */
+    extraClasses: {
+        value: []
     }
 });
 
index 1812cb4..f89bbf5 100644 (file)
@@ -422,7 +422,7 @@ Y.extend(TOOLTIP, M.core.dialogue, {
 
     close_panel: function(e) {
         // Hide the panel first.
-        this.hide();
+        this.hide(e);
 
         // Cancel the listeners that we added in display_panel.
         this.cancel_events();
index e00100a..7b06b6f 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index 066f2e0..651bbd9 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index e00100a..7b06b6f 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index 7989a7a..5d10701 100644 (file)
@@ -288,6 +288,7 @@ EDITOR.prototype = {
                 headerContent: this.get('header'),
                 bodyContent: this.get('body'),
                 footerContent: this.get('footer'),
+                modal: true,
                 width: '840px',
                 visible: false,
                 draggable: true
diff --git a/mod/data/classes/event/course_module_instance_list_viewed.php b/mod/data/classes/event/course_module_instance_list_viewed.php
new file mode 100644 (file)
index 0000000..7e19721
--- /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/>.
+
+/**
+ * The mod_data course module instance list viewed event.
+ *
+ * @package    mod_data
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed {
+    // No code required here as the parent class handles it all.
+}
+
diff --git a/mod/data/classes/event/course_module_viewed.php b/mod/data/classes/event/course_module_viewed.php
new file mode 100644 (file)
index 0000000..14358e2
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The mod_data course module viewed event.
+ *
+ * @package    mod_data
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+class course_module_viewed extends \core\event\course_module_viewed {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'data';
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+}
diff --git a/mod/data/classes/event/field_created.php b/mod/data/classes/event/field_created.php
new file mode 100644 (file)
index 0000000..e479697
--- /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/>.
+
+/**
+ * The mod_data field created event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      @type string fieldname the name of the field.
+ *      @type int dataid the id of the data activity.
+ * }
+ *
+ * @package    mod_data
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+class field_created extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'data_fields';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventfieldcreated', 'mod_data');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'The field ' . $this->objectid . ' belonging to the data activity ' . $this->other['dataid'] . ' has been created.';
+    }
+
+    /**
+     * Get the legacy event log data.
+     *
+     * @return array
+     */
+    public function get_legacy_logdata() {
+        return array($this->courseid, 'data', 'fields add', 'field.php?d=' . $this->other['dataid'] . '&amp;mode=display&amp;fid=' .
+            $this->objectid, $this->objectid, $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['fieldname'])) {
+            throw new \coding_exception('The fieldname must be set in $other.');
+        }
+
+        if (!isset($this->other['dataid'])) {
+            throw new \coding_exception('The dataid must be set in $other.');
+        }
+    }
+}
diff --git a/mod/data/classes/event/field_deleted.php b/mod/data/classes/event/field_deleted.php
new file mode 100644 (file)
index 0000000..8004537
--- /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/>.
+
+/**
+ * The mod_data field deleted event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      @type string fieldname the name of the field.
+ *      @type int dataid the id of the data activity.
+ * }
+ *
+ * @package    mod_data
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+class field_deleted extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'data_fields';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventfielddeleted', 'mod_data');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'The field ' . $this->objectid . ' belonging to the data activity ' . $this->other['dataid'] . ' has been deleted.';
+    }
+
+    /**
+     * Get the legacy event log data.
+     *
+     * @return array
+     */
+    public function get_legacy_logdata() {
+        return array($this->courseid, 'data', 'fields delete', 'field.php?d=' . $this->other['dataid'],
+            $this->other['fieldname'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['fieldname'])) {
+            throw new \coding_exception('The fieldname must be set in $other.');
+        }
+
+        if (!isset($this->other['dataid'])) {
+            throw new \coding_exception('The dataid must be set in $other.');
+        }
+    }
+}
diff --git a/mod/data/classes/event/field_updated.php b/mod/data/classes/event/field_updated.php
new file mode 100644 (file)
index 0000000..eb80890
--- /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/>.
+
+/**
+ * The mod_data field updated event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      @type string fieldname the name of the field.
+ *      @type int dataid the id of the data activity.
+ * }
+ *
+ * @package    mod_data
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+class field_updated extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'data_fields';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventfieldupdated', 'mod_data');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'The field ' . $this->objectid . ' belonging to the data activity ' . $this->other['dataid'] . ' has been updated.';
+    }
+
+    /**
+     * Get the legacy event log data.
+     *
+     * @return array
+     */
+    public function get_legacy_logdata() {
+        return array($this->courseid, 'data', 'fields update', 'field.php?d=' . $this->other['dataid'] .
+            '&amp;mode=display&amp;fid=' . $this->objectid, $this->objectid, $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['fieldname'])) {
+            throw new \coding_exception('The fieldname must be set in $other.');
+        }
+
+        if (!isset($this->other['dataid'])) {
+            throw new \coding_exception('The dataid must be set in $other.');
+        }
+    }
+}
diff --git a/mod/data/classes/event/record_created.php b/mod/data/classes/event/record_created.php
new file mode 100644 (file)
index 0000000..5cbfe26
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The mod_data data record created event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      @type int dataid the id of the data activity.
+ * }
+ *
+ * @package    mod_data
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+class record_created extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'data_records';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventrecordcreated', 'mod_data');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'The data record ' . $this->objectid . ' belonging to the database activity ' . $this->other['dataid'] .
+            ' was created by the user ' . $this->userid;
+    }
+
+    /**
+     * Get the legacy event log data.
+     *
+     * @return array
+     */
+    public function get_legacy_logdata() {
+        return array($this->courseid, 'data', 'add', 'view.php?d=' . $this->other['dataid'] . '&amp;rid=' . $this->objectid,
+            $this->other['dataid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['dataid'])) {
+            throw new \coding_exception('The dataid must be set in $other.');
+        }
+    }
+}
diff --git a/mod/data/classes/event/record_deleted.php b/mod/data/classes/event/record_deleted.php
new file mode 100644 (file)
index 0000000..f611d9a
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The mod_data record deleted event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      @type int dataid the id of the data activity.
+ * }
+ *
+ * @package    mod_data
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+class record_deleted extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'data_records';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventrecorddeleted', 'mod_data');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'The data record ' . $this->objectid . ' belonging to the database activity ' . $this->other['dataid'] .
+            ' was deleted by the user ' . $this->userid;
+    }
+
+    /**
+     * Get the legacy event log data.
+     *
+     * @return array
+     */
+    public function get_legacy_logdata() {
+        return array($this->courseid, 'data', 'record delete', 'view.php?id=' . $this->contextinstanceid,
+            $this->other['dataid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['dataid'])) {
+            throw new \coding_exception('The dataid must be set in $other.');
+        }
+    }
+}
diff --git a/mod/data/classes/event/record_updated.php b/mod/data/classes/event/record_updated.php
new file mode 100644 (file)
index 0000000..5ba249a
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The mod_data record updated event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      @type int dataid the id of the data activity.
+ * }
+ *
+ * @package    mod_data
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+class record_updated extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'data_records';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventrecordupdated', 'mod_data');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'The data record ' . $this->objectid . ' belonging to the database activity ' . $this->other['dataid'] .
+            ' was updated by the user ' . $this->userid;
+    }
+
+    /**
+     * Get the legacy event log data.
+     *
+     * @return array
+     */
+    public function get_legacy_logdata() {
+        return array($this->courseid, 'data', 'update', 'view.php?d=' . $this->other['dataid'] . '&amp;rid=' . $this->objectid,
+            $this->other['dataid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['dataid'])) {
+            throw new \coding_exception('The dataid must be set in $other.');
+        }
+    }
+}
diff --git a/mod/data/classes/event/template_updated.php b/mod/data/classes/event/template_updated.php
new file mode 100644 (file)
index 0000000..e89424d
--- /dev/null
@@ -0,0 +1,88 @@
+<?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 mod_data template updated event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      @type int dataid the id of the data activity.
+ * }
+ *
+ * @package    mod_data
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+class template_updated extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventtemplateupdated', 'mod_data');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'The template for the database activity ' . $this->other['dataid'] . ' was updated by the user ' . $this->userid;
+    }
+
+    /**
+     * Get the legacy event log data.
+     *
+     * @return array
+     */
+    public function get_legacy_logdata() {
+        return array($this->courseid, 'data', 'templates saved', 'templates.php?id=' . $this->contextinstanceid .
+            '&amp;d=' . $this->other['dataid'], $this->other['dataid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['dataid'])) {
+            throw new \coding_exception('The dataid must be set in $other.');
+        }
+    }
+}
diff --git a/mod/data/classes/event/template_viewed.php b/mod/data/classes/event/template_viewed.php
new file mode 100644 (file)
index 0000000..bcacde9
--- /dev/null
@@ -0,0 +1,89 @@
+<?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 mod_data templates viewed event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      @type int dataid the id of the data activity.
+ * }
+ *
+ * @package    mod_data
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+class template_viewed extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventtemplateviewed', 'mod_data');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'The template for the database activity with the id ' . $this->other['dataid'] . ' was viewed by the ' .
+            'user with the id ' . $this->userid;
+    }
+
+    /**
+     * Get the legacy event log data.
+     *
+     * @return array
+     */
+    public function get_legacy_logdata() {
+        return array($this->courseid, 'data', 'templates view', 'templates.php?id=' . $this->contextinstanceid .
+            '&amp;d=' . $this->other['dataid'], $this->other['dataid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['dataid'])) {
+            throw new \coding_exception('The dataid must be set in $other.');
+        }
+    }
+}
index 13727ae..0f04530 100644 (file)
@@ -186,7 +186,17 @@ if ($datarecord = data_submitted() and confirm_sesskey()) {
             }
         }
 
-        add_to_log($course->id, 'data', 'update', "view.php?d=$data->id&amp;rid=$rid", $data->id, $cm->id);
+        // Trigger an event for updating this record.
+        $event = \mod_data\event\record_updated::create(array(
+            'objectid' => $rid,
+            'context' => $context,
+            'courseid' => $course->id,
+            'other' => array(
+                'dataid' => $data->id
+            )
+        ));
+        $event->add_record_snapshot('data', $data);
+        $event->trigger();
 
         redirect($CFG->wwwroot.'/mod/data/view.php?d='.$data->id.'&rid='.$rid);
 
@@ -236,8 +246,6 @@ if ($datarecord = data_submitted() and confirm_sesskey()) {
                 }
             }
 
-            add_to_log($course->id, 'data', 'add', "view.php?d=$data->id&amp;rid=$recordid", $data->id, $cm->id);
-
             if (!empty($datarecord->saveandview)) {
                 redirect($CFG->wwwroot.'/mod/data/view.php?d='.$data->id.'&rid='.$recordid);
             }
index a24d628..210311a 100644 (file)
@@ -121,9 +121,6 @@ switch ($mode) {
             /// Update some templates
                 data_append_new_field_to_templates($data, $fieldinput->name);
 
-                add_to_log($course->id, 'data', 'fields add',
-                           "field.php?d=$data->id&amp;mode=display&amp;fid=$fid", $fid, $cm->id);
-
                 $displaynoticegood = get_string('fieldadded','data');
             }
         }
@@ -163,9 +160,6 @@ switch ($mode) {
             /// Update the templates.
                 data_replace_field_in_templates($data, $oldfieldname, $field->field->name);
 
-                add_to_log($course->id, 'data', 'fields update',
-                           "field.php?d=$data->id&amp;mode=display&amp;fid=$fid", $fid, $cm->id);
-
                 $displaynoticegood = get_string('fieldupdated','data');
             }
         }
@@ -194,9 +188,6 @@ switch ($mode) {
                         $DB->update_record('data', $rec);
                     }
 
-                    add_to_log($course->id, 'data', 'fields delete',
-                               "field.php?d=$data->id", $field->field->name, $cm->id);
-
                     $displaynoticegood = get_string('fielddeleted', 'data');
                 }
 
index de7c74b..8620a34 100644 (file)
@@ -39,7 +39,11 @@ $PAGE->set_pagelayout('incourse');
 
 $context = context_course::instance($course->id);
 
-add_to_log($course->id, "data", "view all", "index.php?id=$course->id", "");
+$params = array(
+    'context' => context_course::instance($course->id)
+);
+$event = \mod_data\event\course_module_instance_list_viewed::create($params);
+$event->trigger();
 
 $strname = get_string('name');
 $strdata = get_string('modulename','data');
index d0d9110..919d5c0 100644 (file)
@@ -119,6 +119,14 @@ $string['editordisable'] = 'Disable editor';
 $string['editorenable'] = 'Enable editor';
 $string['emptyadd'] = 'The Add template is empty, generating a default form...';
 $string['emptyaddform'] = 'You did not fill out any fields!';
+$string['eventfieldcreated'] = 'Field created';
+$string['eventfielddeleted'] = 'Field deleted';
+$string['eventfieldupdated'] = 'Field updated';
+$string['eventrecordcreated'] = 'Record created';
+$string['eventrecorddeleted'] = 'Record deleted';
+$string['eventrecordupdated'] = 'Record updated';
+$string['eventtemplateupdated'] = 'Template updated';
+$string['eventtemplateviewed'] = 'Templates viewed';
 $string['fileencoding'] = 'Encoding';
 $string['entries'] = 'Entries';
 $string['entrieslefttoadd'] = 'You must add {$a->entriesleft} more entry/entries in order to complete this activity';
index 8123cc8..4bf3478 100644 (file)
@@ -188,6 +188,18 @@ class data_field_base {     // Base class for Database Field Types (see field/*/
         }
 
         $this->field->id = $DB->insert_record('data_fields',$this->field);
+
+        // Trigger an event for creating this field.
+        $event = \mod_data\event\field_created::create(array(
+            'objectid' => $this->field->id,
+            'context' => $this->context,
+            'other' => array(
+                'fieldname' => $this->field->name,
+                'dataid' => $this->data->id
+            )
+        ));
+        $event->trigger();
+
         return true;
     }
 
@@ -202,6 +214,18 @@ class data_field_base {     // Base class for Database Field Types (see field/*/
         global $DB;
 
         $DB->update_record('data_fields', $this->field);
+
+        // Trigger an event for updating this field.
+        $event = \mod_data\event\field_updated::create(array(
+            'objectid' => $this->field->id,
+            'context' => $this->context,
+            'other' => array(
+                'fieldname' => $this->field->name,
+                'dataid' => $this->data->id
+            )
+        ));
+        $event->trigger();
+
         return true;
     }
 
@@ -215,9 +239,25 @@ class data_field_base {     // Base class for Database Field Types (see field/*/
         global $DB;
 
         if (!empty($this->field->id)) {
+            // Get the field before we delete it.
+            $field = $DB->get_record('data_fields', array('id' => $this->field->id));
+
             $this->delete_content();
             $DB->delete_records('data_fields', array('id'=>$this->field->id));
+
+            // Trigger an event for deleting this field.
+            $event = \mod_data\event\field_deleted::create(array(
+                'objectid' => $this->field->id,
+                'context' => $this->context,
+                'other' => array(
+                    'fieldname' => $this->field->name,
+                    'dataid' => $this->data->id
+                 )
+            ));
+            $event->add_record_snapshot('data_fields', $field);
+            $event->trigger();
         }
+
         return true;
     }
 
@@ -802,7 +842,19 @@ function data_add_record($data, $groupid=0){
     } else {
         $record->approved = 0;
     }
-    return $DB->insert_record('data_records', $record);
+    $record->id = $DB->insert_record('data_records', $record);
+
+    // Trigger an event for creating this record.
+    $event = \mod_data\event\record_created::create(array(
+        'objectid' => $record->id,
+        'context' => $context,
+        'other' => array(
+            'dataid' => $data->id
+        )
+    ));
+    $event->trigger();
+
+    return $record->id;
 }
 
 /**
@@ -3684,7 +3736,18 @@ function data_delete_record($recordid, $data, $courseid, $cmid) {
                 $DB->delete_records('data_content', array('recordid'=>$deleterecord->id));
                 $DB->delete_records('data_records', array('id'=>$deleterecord->id));
 
-                add_to_log($courseid, 'data', 'record delete', "view.php?id=$cmid", $data->id, $cmid);
+                // Trigger an event for deleting this record.
+                $event = \mod_data\event\record_deleted::create(array(
+                    'objectid' => $deleterecord->id,
+                    'context' => context_module::instance($cmid),
+                    'courseid' => $courseid,
+                    'other' => array(
+                        'dataid' => $deleterecord->dataid
+                    )
+                ));
+                $event->add_record_snapshot('data_records', $deleterecord);
+                $event->trigger();
+
                 return true;
             }
         }
index 8adce53..237ec21 100644 (file)
@@ -73,8 +73,16 @@ if (!$DB->count_records('data_fields', array('dataid'=>$data->id))) {      // Br
     redirect($CFG->wwwroot.'/mod/data/field.php?d='.$data->id);  // Redirect to field entry
 }
 
-add_to_log($course->id, 'data', 'templates view', "templates.php?id=$cm->id&amp;d=$data->id", $data->id, $cm->id);
-
+// Trigger an event for viewing templates.
+$event = \mod_data\event\template_viewed::create(array(
+    'context' => $context,
+    'courseid' => $course->id,
+    'other' => array(
+        'dataid' => $data->id
+    )
+));
+$event->add_record_snapshot('data', $data);
+$event->trigger();
 
 /// Print the page header
 
@@ -136,7 +144,16 @@ if (($mytemplate = data_submitted()) && confirm_sesskey()) {
             if (empty($disableeditor) && empty($enableeditor)) {
                 $DB->update_record('data', $newtemplate);
                 echo $OUTPUT->notification(get_string('templatesaved', 'data'), 'notifysuccess');
-                add_to_log($course->id, 'data', 'templates saved', "templates.php?id=$cm->id&amp;d=$data->id", $data->id, $cm->id);
+
+                // Trigger an event for saving the templates.
+                $event = \mod_data\event\template_updated::create(array(
+                    'context' => $context,
+                    'courseid' => $course->id,
+                    'other' => array(
+                        'dataid' => $data->id,
+                    )
+                ));
+                $event->trigger();
             }
         }
     }
diff --git a/mod/data/tests/events_test.php b/mod/data/tests/events_test.php
new file mode 100644 (file)
index 0000000..2a5c375
--- /dev/null
@@ -0,0 +1,341 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Events tests.
+ *
+ * @package mod_data
+ * @category test
+ * @copyright 2014 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+class mod_data_events_testcase extends advanced_testcase {
+
+    /**
+     * Test set up.
+     *
+     * This is executed before running any test in this file.
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    /**
+     * Test the field created event.
+     */
+    public function test_field_created() {
+        $this->setAdminUser();
+
+        // Create a course we are going to add a data module to.
+        $course = $this->getDataGenerator()->create_course();
+
+        // The generator used to create a data module.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+
+        // Create a data module.
+        $data = $generator->create_instance(array('course' => $course->id));
+
+        // Now we want to create a field.
+        $field = data_get_field_new('text', $data);
+        $fielddata = new stdClass();
+        $fielddata->name = 'Test';
+        $fielddata->description = 'Test description';
+        $field->define_field($fielddata);
+
+        // Trigger and capture the event for creating a field.
+        $sink = $this->redirectEvents();
+        $field->insert_field();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\mod_data\event\field_created', $event);
+        $this->assertEquals(context_module::instance($data->cmid), $event->get_context());
+        $expected = array($course->id, 'data', 'fields add', 'field.php?d=' . $data->id . '&amp;mode=display&amp;fid=' .
+            $field->field->id, $field->field->id, $data->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+    }
+
+    /**
+     * Test the field updated event.
+     */
+    public function test_field_updated() {
+        $this->setAdminUser();
+
+        // Create a course we are going to add a data module to.
+        $course = $this->getDataGenerator()->create_course();
+
+        // The generator used to create a data module.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+
+        // Create a data module.
+        $data = $generator->create_instance(array('course' => $course->id));
+
+        // Now we want to create a field.
+        $field = data_get_field_new('text', $data);
+        $fielddata = new stdClass();
+        $fielddata->name = 'Test';
+        $fielddata->description = 'Test description';
+        $field->define_field($fielddata);
+        $field->insert_field();
+
+        // Trigger and capture the event for updating the field.
+        $sink = $this->redirectEvents();
+        $field->update_field();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\mod_data\event\field_updated', $event);
+        $this->assertEquals(context_module::instance($data->cmid), $event->get_context());
+        $expected = array($course->id, 'data', 'fields update', 'field.php?d=' . $data->id . '&amp;mode=display&amp;fid=' .
+            $field->field->id, $field->field->id, $data->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+    }
+
+    /**
+     * Test the field deleted event.
+     */
+    public function test_field_deleted() {
+        $this->setAdminUser();
+
+        // Create a course we are going to add a data module to.
+        $course = $this->getDataGenerator()->create_course();
+
+        // The generator used to create a data module.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+
+        // Create a data module.
+        $data = $generator->create_instance(array('course' => $course->id));
+
+        // Now we want to create a field.
+        $field = data_get_field_new('text', $data);
+        $fielddata = new stdClass();
+        $fielddata->name = 'Test';
+        $fielddata->description = 'Test description';
+        $field->define_field($fielddata);
+        $field->insert_field();
+
+        // Trigger and capture the event for deleting the field.
+        $sink = $this->redirectEvents();
+        $field->delete_field();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\mod_data\event\field_deleted', $event);
+        $this->assertEquals(context_module::instance($data->cmid), $event->get_context());
+        $expected = array($course->id, 'data', 'fields delete', 'field.php?d=' . $data->id, $field->field->name, $data->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+    }
+
+    /**
+     * Test the record created event.
+     */
+    public function test_record_created() {
+        // Create a course we are going to add a data module to.
+        $course = $this->getDataGenerator()->create_course();
+
+        // The generator used to create a data module.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+
+        // Create a data module.
+        $data = $generator->create_instance(array('course' => $course->id));
+
+        // Trigger and capture the event for creating the record.
+        $sink = $this->redirectEvents();
+        $recordid = data_add_record($data);
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\mod_data\event\record_created', $event);
+        $this->assertEquals(context_module::instance($data->cmid), $event->get_context());
+        $expected = array($course->id, 'data', 'add', 'view.php?d=' . $data->id . '&amp;rid=' . $recordid,
+            $data->id, $data->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+    }
+
+    /**
+     * Test the record updated event.
+     *
+     * There is no external API for updating a record, so the unit test will simply create
+     * and trigger the event and ensure the legacy log data is returned as expected.
+     */
+    public function test_record_updated() {
+        // Create a course we are going to add a data module to.
+        $course = $this->getDataGenerator()->create_course();
+
+        // The generator used to create a data module.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+
+        // Create a data module.
+        $data = $generator->create_instance(array('course' => $course->id));
+
+        // Trigger an event for updating this record.
+        $event = \mod_data\event\record_updated::create(array(
+            'objectid' => 1,
+            'context' => context_module::instance($data->cmid),
+            'courseid' => $course->id,
+            'other' => array(
+                'dataid' => $data->id
+            )
+        ));
+
+        // Trigger and capture the event for updating the data record.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\mod_data\event\record_updated', $event);
+        $this->assertEquals(context_module::instance($data->cmid), $event->get_context());
+        $expected = array($course->id, 'data', 'update', 'view.php?d=' . $data->id . '&amp;rid=1', $data->id, $data->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+    }
+
+    /**
+     * Test the record deleted event.
+     */
+    public function test_record_deleted() {
+        global $DB;
+
+        // Create a course we are going to add a data module to.
+        $course = $this->getDataGenerator()->create_course();
+
+        // The generator used to create a data module.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+
+        // Create a data module.
+        $data = $generator->create_instance(array('course' => $course->id));
+
+        // Now we want to create a field.
+        $field = data_get_field_new('text', $data);
+        $fielddata = new stdClass();
+        $fielddata->name = 'Test';
+        $fielddata->description = 'Test description';
+        $field->define_field($fielddata);
+        $field->insert_field();
+
+        // Create data record.
+        $datarecords = new stdClass();
+        $datarecords->userid = '2';
+        $datarecords->dataid = $data->id;
+        $datarecords->id = $DB->insert_record('data_records', $datarecords);
+
+        // Create data content.
+        $datacontent = new stdClass();
+        $datacontent->fieldid = $field->field->id;
+        $datacontent->recordid = $datarecords->id;
+        $datacontent->id = $DB->insert_record('data_content', $datacontent);
+
+        // Trigger and capture the event for deleting the data record.
+        $sink = $this->redirectEvents();
+        data_delete_record($datarecords->id, $data, $course->id, $data->cmid);
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\mod_data\event\record_deleted', $event);
+        $this->assertEquals(context_module::instance($data->cmid), $event->get_context());
+        $expected = array($course->id, 'data', 'record delete', 'view.php?id=' . $data->cmid, $data->id, $data->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+    }
+
+    /**
+     * Test the template viewed event.
+     *
+     * There is no external API for viewing templates, so the unit test will simply create
+     * and trigger the event and ensure the legacy log data is returned as expected.
+     */
+    public function test_template_viewed() {
+        // Create a course we are going to add a data module to.
+        $course = $this->getDataGenerator()->create_course();
+
+        // The generator used to create a data module.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+
+        // Create a data module.
+        $data = $generator->create_instance(array('course' => $course->id));
+
+        // Trigger an event for updating this record.
+        $event = \mod_data\event\template_viewed::create(array(
+            'context' => context_module::instance($data->cmid),
+            'courseid' => $course->id,
+            'other' => array(
+                'dataid' => $data->id
+            )
+        ));
+
+        // Trigger and capture the event for updating the data record.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\mod_data\event\template_viewed', $event);
+        $this->assertEquals(context_module::instance($data->cmid), $event->get_context());
+        $expected = array($course->id, 'data', 'templates view', 'templates.php?id=' . $data->cmid . '&amp;d=' .
+            $data->id, $data->id, $data->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+    }
+
+    /**
+     * Test the template updated event.
+     *
+     * There is no external API for updating a template, so the unit test will simply create
+     * and trigger the event and ensure the legacy log data is returned as expected.
+     */
+    public function test_template_updated() {
+        // Create a course we are going to add a data module to.
+        $course = $this->getDataGenerator()->create_course();
+
+        // The generator used to create a data module.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+
+        // Create a data module.
+        $data = $generator->create_instance(array('course' => $course->id));
+
+        // Trigger an event for updating this record.
+        $event = \mod_data\event\template_updated::create(array(
+            'context' => context_module::instance($data->cmid),
+            'courseid' => $course->id,
+            'other' => array(
+                'dataid' => $data->id,
+            )
+        ));
+
+        // Trigger and capture the event for updating the data record.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\mod_data\event\template_updated', $event);
+        $this->assertEquals(context_module::instance($data->cmid), $event->get_context());
+        $expected = array($course->id, 'data', 'templates saved', 'templates.php?id=' . $data->cmid . '&amp;d=' .
+            $data->id, $data->id, $data->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+    }
+}
index 557ce4e..1454f90 100644 (file)
         set_user_preference('data_perpage_'.$data->id, $perpage);
     }
 
-    add_to_log($course->id, 'data', 'view', "view.php?id=$cm->id", $data->id, $cm->id);
-
+    $params = array(
+        'context' => $context,
+        'objectid' => $data->id
+    );
+    $event = \mod_data\event\course_module_viewed::create($params);
+    $event->add_record_snapshot('data', $data);
+    $event->trigger();
 
     $urlparams = array('d' => $data->id);
     if ($record) {
diff --git a/mod/forum/classes/event/course_module_instance_list_viewed.php b/mod/forum/classes/event/course_module_instance_list_viewed.php
new file mode 100644 (file)
index 0000000..d0bfc30
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The mod_forum instance list viewed event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum instance list viewed event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed {
+    // No need for any code here as everything is handled by the parent class.
+}
diff --git a/mod/forum/classes/event/course_searched.php b/mod/forum/classes/event/course_searched.php
new file mode 100644 (file)
index 0000000..1861c8d
--- /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/>.
+
+/**
+ * The mod_forum course searched event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum course searched event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     -string searchterm: The searchterm used on forum search
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_searched extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        $searchterm = s($this->other['searchterm']);
+        return "The user {$this->userid} has searched the course {$this->courseid} for ".
+            "forum posts containing '{$searchterm}''";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcoursesearched', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/search.php',
+            array('id' => $this->courseid, 'search' => $this->other['searchterm']));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        // The legacy log table expects a relative path to /mod/forum/.
+        $logurl = substr($this->get_url()->out_as_local_url(), strlen('/mod/forum/'));
+
+        return array($this->courseid, 'forum', 'search', $logurl, $this->other['searchterm']);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['searchterm'])) {
+            throw new \coding_exception('searchterm must be set in $other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_COURSE) {
+            throw new \coding_exception('Context passed must be course.');
+        }
+    }
+
+}
+
diff --git a/mod/forum/classes/event/discussion_created.php b/mod/forum/classes/event/discussion_created.php
new file mode 100644 (file)
index 0000000..a52ca51
--- /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/>.
+
+/**
+ * The mod_forum discussion created event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum discussion created event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     -int forumid: The id of the forum the discussion is in
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class discussion_created extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+        $this->data['objecttable'] = 'forum_discussions';
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has created a discussion in the forum {$this->other['forumid']}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventdiscussioncreated', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->objectid));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+
+        // The legacy log table expects a relative path to /mod/forum/.
+        $logurl = substr($this->get_url()->out_as_local_url(), strlen('/mod/forum/'));
+
+        return array($this->courseid, 'forum', 'add discussion', $logurl, $this->objectid, $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in $other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('objectid must be set to the discussionid.');
+        }
+    }
+}
diff --git a/mod/forum/classes/event/discussion_deleted.php b/mod/forum/classes/event/discussion_deleted.php
new file mode 100644 (file)
index 0000000..425d017
--- /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/>.
+
+/**
+ * The mod_forum discussion deleted event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum discussion deleted event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     -int forumid: The id of the forum the discussion is in
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class discussion_deleted extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'forum_discussions';
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has deleted the forum discussion {$this->objectid} ".
+            "in forum {$this->other['forumid']}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventdiscussiondeleted', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/view.php', array('id' => $this->contextinstanceid));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'forum', 'delete discussion', 'view.php?id=' . $this->contextinstanceid,
+            $this->other['forumid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in $other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('objectid must be set to the discussionid.');
+        }
+    }
+}
+
diff --git a/mod/forum/classes/event/discussion_moved.php b/mod/forum/classes/event/discussion_moved.php
new file mode 100644 (file)
index 0000000..66f82a3
--- /dev/null
@@ -0,0 +1,114 @@
+<?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 mod_forum discussion moved event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum discussion moved event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     - int fromforumid: The id of the forum the discussion is being moved from
+ *     - int toforumid: The id of the forum the discussion is being moved to
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class discussion_moved extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'forum_discussions';
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has moved the forum discussion {$this->objectid} ".
+            "from forum {$this->other['fromforumid']} to forum {$this->other['toforumid']}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventdiscussionmoved', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->objectid));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'forum', 'move discussion', 'discuss.php?d=' . $this->objectid,
+            $this->objectid, $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['fromforumid'])) {
+            throw new \coding_exception('fromforumid must be set in $other.');
+        }
+
+        if (!isset($this->other['toforumid'])) {
+            throw new \coding_exception('toforumid must be set in $other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('objectid must be set to the discussionid.');
+        }
+    }
+}
diff --git a/mod/forum/classes/event/discussion_updated.php b/mod/forum/classes/event/discussion_updated.php
new file mode 100644 (file)
index 0000000..6b84628
--- /dev/null
@@ -0,0 +1,99 @@
+<?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 mod_forum discussion updated event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum discussion updated event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     -int forumid: The id of the forum the discussion is in
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class discussion_updated extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'forum_discussions';
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has updated the discussion {$this->objectid}.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventdiscussionupdated', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->objectid));
+    }
+
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in $other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('objectid must be set to the discussionid.');
+        }
+    }
+}
diff --git a/mod/forum/classes/event/discussion_viewed.php b/mod/forum/classes/event/discussion_viewed.php
new file mode 100644 (file)
index 0000000..c1f911d
--- /dev/null
@@ -0,0 +1,105 @@
+<?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 mod_forum discussion viewed event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum discussion viewed event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class discussion_viewed extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+        $this->data['objecttable'] = 'forum_discussions';
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has viewed the forum discussion {$this->objectid}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventdiscussionviewed', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->objectid));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'forum', 'view discussion', 'discuss.php?d=' . $this->objectid,
+            $this->objectid, $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('objectid must be set to the discussionid.');
+        }
+    }
+
+}
+
diff --git a/mod/forum/classes/event/forum_viewed.php b/mod/forum/classes/event/forum_viewed.php
new file mode 100644 (file)
index 0000000..2df0826
--- /dev/null
@@ -0,0 +1,106 @@
+<?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 mod_forum forum viewed event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum forum viewed event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class forum_viewed extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+        $this->data['objecttable'] = 'forum';
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has viewed the forum {$this->objectid}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventforumviewed', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/view.php', array('d' => $this->objectid));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'forum', 'view forum', 'view.php?f=' . $this->objectid,
+            $this->objectid, $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('objectid must be set to the forumid.');
+        }
+
+    }
+
+}
+
diff --git a/mod/forum/classes/event/post_created.php b/mod/forum/classes/event/post_created.php
new file mode 100644 (file)
index 0000000..79aa997
--- /dev/null
@@ -0,0 +1,131 @@
+<?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 mod_forum post created event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum post created event.
+ * @property-read array $other Extra information about the event.
+ *     - int discussionid: The discussion id the post is part of.
+ *     - int forumid: The forum id the post is part of.
+ *     - string forumtype: The type of forum the post is part of.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class post_created extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+        $this->data['objecttable'] = 'forum_posts';
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has created a post in the discussion {$this->other['discussionid']} ".
+            " in forum {$this->other['forumid']}.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventpostcreated', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->other['forumtype'] == 'single') {
+            // Single discussion forums are an exception. We show
+            // the forum itself since it only has one discussion
+            // thread.
+            $url = new \moodle_url('/mod/forum/view.php', array('id' => $this->other['forumid']));
+        } else {
+            $url = new \moodle_url('/mod/forum/discuss.php', array('d' => $this->other['discussionid']));
+        }
+        $url->set_anchor('p'.$this->objectid);
+        return $url;
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+
+        // The legacy log table expects a relative path to /mod/forum/.
+        $logurl = substr($this->get_url()->out_as_local_url(), strlen('/mod/forum/'));
+
+        return array($this->courseid, 'forum', 'add post', $logurl, $this->other['forumid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('objectid must be set to the postid.');
+        }
+
+        if (!isset($this->other['discussionid'])) {
+            throw new \coding_exception('discussionid must be set in other.');
+        }
+
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in other.');
+        }
+
+        if (!isset($this->other['forumtype'])) {
+            throw new \coding_exception('forumtype must be set in other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+    }
+}
diff --git a/mod/forum/classes/event/post_deleted.php b/mod/forum/classes/event/post_deleted.php
new file mode 100644 (file)
index 0000000..a73fcc7
--- /dev/null
@@ -0,0 +1,130 @@
+<?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 mod_forum post deleted event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum post deleted event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     - int discussionid: The discussion id the post is part of.
+ *     - int forumid: The forum id the post is part of.
+ *     - string forumtype: The type of forum the post is part of.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class post_deleted extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'forum_posts';
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has deleted the post {$this->objectid} ".
+            " in discussion {$this->other['discussionid']}.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventpostdeleted', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->other['forumtype'] == 'single') {
+            // Single discussion forums are an exception. We show
+            // the forum itself since it only has one discussion
+            // thread.
+            $url = new \moodle_url('/mod/forum/view.php', array('id' => $this->other['forumid']));
+        } else {
+            $url = new \moodle_url('/mod/forum/discuss.php', array('d' => $this->other['discussionid']));
+        }
+        return $url;
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        // The legacy log table expects a relative path to /mod/forum/.
+        $logurl = substr($this->get_url()->out_as_local_url(), strlen('/mod/forum/'));
+
+        return array($this->courseid, 'forum', 'delete post', $logurl, $this->objectid, $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('objectid must be set to the postid.');
+        }
+
+        if (!isset($this->other['discussionid'])) {
+            throw new \coding_exception('discussionid must be set in other.');
+        }
+
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in other.');
+        }
+
+        if (!isset($this->other['forumtype'])) {
+            throw new \coding_exception('forumtype must be set in other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+    }
+}
diff --git a/mod/forum/classes/event/post_updated.php b/mod/forum/classes/event/post_updated.php
new file mode 100644 (file)
index 0000000..5bb1f02
--- /dev/null
@@ -0,0 +1,131 @@
+<?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 mod_forum post updated event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum post updated event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     - int discussionid: The discussion id the post is part of.
+ *     - int forumid: The forum id the post is part of.
+ *     - string forumtype: The type of forum the post is part of.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class post_updated extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+        $this->data['objecttable'] = 'forum_posts';
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has created updated the post {$this->objectid} ".
+            " in discussion {$this->other['discussionid']}.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventpostupdated', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->other['forumtype'] == 'single') {
+            // Single discussion forums are an exception. We show
+            // the forum itself since it only has one discussion
+            // thread.
+            $url = new \moodle_url('/mod/forum/view.php', array('id' => $this->other['forumid']));
+        } else {
+            $url = new \moodle_url('/mod/forum/discuss.php', array('d' => $this->other['discussionid']));
+        }
+        $url->set_anchor('p'.$this->objectid);
+        return $url;
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        // The legacy log table expects a relative path to /mod/forum/.
+        $logurl = substr($this->get_url()->out_as_local_url(), strlen('/mod/forum/'));
+
+        return array($this->courseid, 'forum', 'update post', $logurl, $this->objectid, $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('objectid must be set to the postid.');
+        }
+
+        if (!isset($this->other['discussionid'])) {
+            throw new \coding_exception('discussionid must be set in other.');
+        }
+
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in other.');
+        }
+
+        if (!isset($this->other['forumtype'])) {
+            throw new \coding_exception('forumtype must be set in other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+    }
+}
diff --git a/mod/forum/classes/event/readtracking_disabled.php b/mod/forum/classes/event/readtracking_disabled.php
new file mode 100644 (file)
index 0000000..f44f1f1
--- /dev/null
@@ -0,0 +1,108 @@
+<?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 mod_forum subscription created event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum subscription created event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     - int forumid: The id of the forum which readtracking has been disabled on.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class readtracking_disabled extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "Read tracking has been disabled for user {$this->relateduserid} in forum {$this->other['forumid']}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventreadtrackingdisabled', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/view.php', array('id' => $this->other['forumid']));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'forum', 'stop tracking', 'view.php?f=' . $this->other['forumid'],
+            $this->other['forumid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('relateduserid must be set.');
+        }
+
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+    }
+}
diff --git a/mod/forum/classes/event/readtracking_enabled.php b/mod/forum/classes/event/readtracking_enabled.php
new file mode 100644 (file)
index 0000000..1d1f314
--- /dev/null
@@ -0,0 +1,108 @@
+<?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 mod_forum read tracking enabled event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum read tracking enabled event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     - int forumid: The id of the forum which readtracking has been enabled on.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class readtracking_enabled extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "Read tracking has been enabled for user {$this->relateduserid} in forum {$this->other['forumid']}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventreadtrackingenabled', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/view.php', array('id' => $this->other['forumid']));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'forum', 'start tracking', 'view.php?f=' . $this->other['forumid'],
+            $this->other['forumid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('relateduserid must be set.');
+        }
+
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+    }
+}
diff --git a/mod/forum/classes/event/subscribers_viewed.php b/mod/forum/classes/event/subscribers_viewed.php
new file mode 100644 (file)
index 0000000..c3e8a3a
--- /dev/null
@@ -0,0 +1,107 @@
+<?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 mod_forum subscribers list viewed event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum subscribers list viewed event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     - int forumid: The id of the forum which the subscriberslist has been viewed.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class subscribers_viewed extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has viewed the subscribers list for forum {$this->other['forumid']}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventsubscribersviewed', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/subscribers.php', array('id' => $this->other['forumid']));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'forum', 'view subscribers', 'subscribers.php?id=' . $this->other['forumid'],
+            $this->other['forumid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+    }
+
+}
+
diff --git a/mod/forum/classes/event/subscription_created.php b/mod/forum/classes/event/subscription_created.php
new file mode 100644 (file)
index 0000000..94c1c61
--- /dev/null
@@ -0,0 +1,106 @@
+<?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 mod_forum subscription created event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum subscription created event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     - int forumid: The id of the forum which has been subscribed to.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class subscription_created extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->relateduserid} was subscribed to the forum {$this->other['forumid']}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventsubscriptioncreated', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/view.php', array('id' => $this->other['forumid']));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'forum', 'subscribe', 'view.php?f=' . $this->other['forumid'],
+            $this->other['forumid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('relateduserid must be set.');
+        }
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+    }
+}
diff --git a/mod/forum/classes/event/subscription_deleted.php b/mod/forum/classes/event/subscription_deleted.php
new file mode 100644 (file)
index 0000000..73832ce
--- /dev/null
@@ -0,0 +1,108 @@
+<?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 mod_forum subscription created event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum subscription created event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     - int forumid: The id of the forum which has been unsusbcribed from.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class subscription_deleted extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->relateduserid} was unsubscribed the forum {$this->other['forumid']}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventsubscriptiondeleted', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/forum/view.php', array('id' => $this->other['forumid']));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'forum', 'unsubscribe', 'view.php?f=' . $this->other['forumid'],
+            $this->other['forumid'], $this->contextinstanceid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('relateduserid must be set.');
+        }
+
+        if (!isset($this->other['forumid'])) {
+            throw new \coding_exception('forumid must be set in other.');
+        }
+
+        if ($this->contextlevel != CONTEXT_MODULE) {
+            throw new \coding_exception('Context passed must be module context.');
+        }
+    }
+}
diff --git a/mod/forum/classes/event/userreport_viewed.php b/mod/forum/classes/event/userreport_viewed.php
new file mode 100644 (file)
index 0000000..91b3cc9
--- /dev/null
@@ -0,0 +1,127 @@
+<?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 mod_forum userreport viewed event.
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_forum\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_forum userreport viewed event.
+ *
+ * @property-read array $other Extra information about the event.
+ *     - string reportmode: The mode the report has been viewed in (posts or discussions).
+ *
+ * @package    mod_forum
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class userreport_viewed extends \core\event\base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user {$this->userid} has viewed the userreport for user {$this->relateduserid} in ".
+            "course {$this->courseid} with viewing mode {$this->other['reportmode']}";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserreportviewed', 'mod_forum');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+
+        $url = new \moodle_url('/mod/forum/user.php', array('id' => $this->relateduserid,
+            'mode' => $this->other['reportmode']));
+
+        if ($this->courseid != SITEID) {
+            $url->param('course', $this->courseid);
+        }
+
+        return $url;
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        // The legacy log table expects a relative path to /mod/forum/.
+        $logurl = substr($this->get_url()->out_as_local_url(), strlen('/mod/forum/'));
+
+        return array($this->courseid, 'forum', 'user report', $logurl, $this->relateduserid);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('relateduserid must be set.');
+        }
+        if (!isset($this->other['reportmode'])) {
+            throw new \coding_exception('reportmode must be set in other.');
+        }
+
+        switch ($this->contextlevel)
+        {
+            case CONTEXT_COURSE:
+            case CONTEXT_SYSTEM:
+                // OK, expected context level.
+                break;
+            default:
+                // Unexpected contextlevel.
+                throw new \coding_exception('Context passed must be system or course.');
+        }
+    }
+
+}
+
index fc821f5..d7e522e 100644 (file)
             print_error('cannotmovenotvisible', 'forum', $return);
         }
 
-        require_capability('mod/forum:startdiscussion', context_module::instance($cmto->id));
+        $destinationctx = context_module::instance($cmto->id);
+        require_capability('mod/forum:startdiscussion', $destinationctx);
 
         if (!forum_move_attachments($discussion, $forum->id, $forumto->id)) {
             echo $OUTPUT->notification("Errors occurred while moving attachment directories - check your file permissions");
         }
         $DB->set_field('forum_discussions', 'forum', $forumto->id, array('id' => $discussion->id));
         $DB->set_field('forum_read', 'forumid', $forumto->id, array('discussionid' => $discussion->id));
-        add_to_log($course->id, 'forum', 'move discussion', "discuss.php?d=$discussion->id", $discussion->id, $cmto->id);
+
+        $params = array(
+            'context' => $destinationctx,
+            'objectid' => $discussion->id,
+            'other' => array(
+                'fromforumid' => $forum->id,
+                'toforumid' => $forumto->id,
+            )
+        );
+        $event = \mod_forum\event\discussion_moved::create($params);
+        $event->add_record_snapshot('forum_discussions', $discussion);
+        $event->add_record_snapshot('forum', $forum);
+        $event->add_record_snapshot('forum', $forumto);
+        $event->trigger();
 
         // Delete the RSS files for the 2 forums to force regeneration of the feeds
         require_once($CFG->dirroot.'/mod/forum/rsslib.php');
         redirect($return.'&moved=-1&sesskey='.sesskey());
     }
 
-    add_to_log($course->id, 'forum', 'view discussion', "discuss.php?d=$discussion->id", $discussion->id, $cm->id);
+    $params = array(
+        'context' => $modcontext,
+        'objectid' => $discussion->id,
+    );
+    $event = \mod_forum\event\discussion_viewed::create($params);
+    $event->add_record_snapshot('forum_discussions', $discussion);
+    $event->add_record_snapshot('forum', $forum);
+    $event->trigger();
 
     unset($SESSION->fromdiscussion);
 
index f86c646..e282ddb 100644 (file)
@@ -51,7 +51,11 @@ $coursecontext = context_course::instance($course->id);
 
 unset($SESSION->fromdiscussion);
 
-add_to_log($course->id, 'forum', 'view forums', "index.php?id=$course->id");
+$params = array(
+    'context' => context_course::instance($course->id)
+);
+$event = \mod_forum\event\course_module_instance_list_viewed::create($params);
+$event->trigger();
 
 $strforums       = get_string('forums', 'forum');
 $strforum        = get_string('forum', 'forum');
@@ -197,10 +201,8 @@ if (!is_null($subscribe)) {
     $returnto = forum_go_back_to("index.php?id=$course->id");
     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
     if ($subscribe) {
-        add_to_log($course->id, 'forum', 'subscribeall', "index.php?id=$course->id", $course->id);
         redirect($returnto, get_string('nowallsubscribed', 'forum', $shortname), 1);
     } else {
-        add_to_log($course->id, 'forum', 'unsubscribeall', "index.php?id=$course->id", $course->id);
         redirect($returnto, get_string('nowallunsubscribed', 'forum', $shortname), 1);
     }
 }
index 7b6c321..8267591 100644 (file)
@@ -144,6 +144,22 @@ $string['edit'] = 'Edit';
 $string['editedby'] = 'Edited by {$a->name} - original submission {$a->date}';
 $string['editedpostupdated'] = '{$a}\'s post was updated';
 $string['editing'] = 'Editing';
+$string['eventcoursesearched'] = 'Course searched';
+$string['eventdiscussioncreated'] = 'Discussion created';
+$string['eventdiscussionupdated'] = 'Discussion updated';
+$string['eventdiscussiondeleted'] = 'Discussion deleted';
+$string['eventdiscussionmoved'] = 'Discussion moved';
+$string['eventdiscussionviewed'] = 'Discussion viewed';
+$string['eventforumviewed'] = 'Forum viewed';
+$string['eventuserreportviewed'] = 'User report viewed';
+$string['eventpostcreated'] = 'Post created';
+$string['eventpostdeleted'] = 'Post deleted';
+$string['eventpostupdated'] = 'Post updated';
+$string['eventreadtrackingdisabled'] = 'Read tracking disabled';
+$string['eventreadtrackingenabled'] = 'Read tracking enabled';
+$string['eventsubscribersviewed'] = 'Subscribers viewed';
+$string['eventsubscriptioncreated'] = 'Subscription created';
+$string['eventsubscriptiondeleted'] = 'Subscription deleted';
 $string['emaildigestcompleteshort'] = 'Complete posts';
 $string['emaildigestdefault'] = 'Default ({$a})';
 $string['emaildigestoffshort'] = 'No digest';
index 2bad212..a66bcf2 100644 (file)
@@ -773,8 +773,6 @@ function forum_cron() {
                 if (!$mailresult){
                     mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id $post->id to user $userto->id".
                          " ($userto->email) .. not trying again.");
-                    add_to_log($course->id, 'forum', 'mail error', "discuss.php?d=$discussion->id#p$post->id",
-                               substr(format_string($post->subject,true),0,30), $cm->id, $userto->id);
                     $errorcount[$post->id]++;
                 } else {
                     $mailcount[$post->id]++;
@@ -1073,9 +1071,8 @@ function forum_cron() {
                 $mailresult = email_to_user($userto, $site->shortname, $postsubject, $posttext, $posthtml, $attachment, $attachname);
 
                 if (!$mailresult) {
-                    mtrace("ERROR!");
-                    echo "Error: mod/forum/cron.php: Could not send out digest mail to user $userto->id ($userto->email)... not trying again.\n";
-                    add_to_log($course->id, 'forum', 'mail digest error', '', '', $cm->id, $userto->id);
+                    mtrace("ERROR: mod/forum/cron.php: Could not send out digest mail to user $userto->id ".
+                        "($userto->email)... not trying again.");
                 } else {
                     mtrace("success.");
                     $usermailcount++;
@@ -4818,7 +4815,19 @@ function forum_subscribe($userid, $forumid) {
     $sub->userid  = $userid;
     $sub->forum = $forumid;
 
-    return $DB->insert_record("forum_subscriptions", $sub);
+    $result = $DB->insert_record("forum_subscriptions", $sub);
+
+    $cm = get_coursemodule_from_instance('forum', $forumid);
+    $params = array(
+        'context' => context_module::instance($cm->id),
+        'relateduserid' => $userid,
+        'other' => array('forumid' => $forumid),
+
+    );
+    $event  = \mod_forum\event\subscription_created::create($params);
+    $event->trigger();
+
+    return $result;
 }
 
 /**
@@ -4830,8 +4839,21 @@ function forum_subscribe($userid, $forumid) {
  */
 function forum_unsubscribe($userid, $forumid) {
     global $DB;
-    return ($DB->delete_records('forum_digests', array('userid' => $userid, 'forum' => $forumid))
-            && $DB->delete_records('forum_subscriptions', array('userid' => $userid, 'forum' => $forumid)));
+
+    $DB->delete_records('forum_digests', array('userid' => $userid, 'forum' => $forumid));
+    $DB->delete_records('forum_subscriptions', array('userid' => $userid, 'forum' => $forumid));
+
+    $cm = get_coursemodule_from_instance('forum', $forumid);
+    $params = array(
+        'context' => context_module::instance($cm->id),
+        'relateduserid' => $userid,
+        'other' => array('forumid' => $forumid),
+
+    );
+    $event = \mod_forum\event\subscription_deleted::create($params);
+    $event->trigger();
+
+    return true;
 }
 
 /**
index a47f049..b26278a 100644 (file)
@@ -81,9 +81,7 @@ if ($mark == 'read') {
             print_error('invaliddiscussionid', 'forum');
         }
 
-        if (forum_tp_mark_discussion_read($user, $d)) {
-            add_to_log($course->id, "discussion", "mark read", "view.php?f=$forum->id", $d, $cm->id);
-        }
+        forum_tp_mark_discussion_read($user, $d);
     } else {
         // Mark all messages read in current group
         $currentgroup = groups_get_activity_group($cm);
@@ -92,15 +90,12 @@ if ($mark == 'read') {
             // may return 0
             $currentgroup=false;
         }
-        if (forum_tp_mark_forum_read($user, $forum->id,$currentgroup)) {
-            add_to_log($course->id, "forum", "mark read", "view.php?f=$forum->id", $forum->id, $cm->id);
-        }
+        forum_tp_mark_forum_read($user, $forum->id, $currentgroup);
     }
 
 /// FUTURE - Add ability to mark them as unread.
 //    } else { // subscribe
 //        if (forum_tp_start_tracking($forum->id, $user->id)) {
-//            add_to_log($course->id, "forum", "mark unread", "view.php?f=$forum->id", $forum->id, $cm->id);
 //            redirect($returnto, get_string("nowtracking", "forum", $info), 1);
 //        } else {
 //            print_error("Could not start tracking that forum", $_SERVER["HTTP_REFERER"]);
index f1e9988..ad1deab 100644 (file)
@@ -335,8 +335,17 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
                 }
                 forum_delete_discussion($discussion, false, $course, $cm, $forum);
 
-                add_to_log($discussion->course, "forum", "delete discussion",
-                           "view.php?id=$cm->id", "$forum->id", $cm->id);
+                $params = array(
+                    'objectid' => $discussion->id,
+                    'context' => $modcontext,
+                    'other' => array(
+                        'forumid' => $forum->id,
+                    )
+                );
+
+                $event = \mod_forum\event\discussion_deleted::create($params);
+                $event->add_record_snapshot('forum_discussions', $discussion);
+                $event->trigger();
 
                 redirect("view.php?f=$discussion->forum");
 
@@ -352,7 +361,23 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
                     $discussionurl = "discuss.php?d=$post->discussion";
                 }
 
-                add_to_log($discussion->course, "forum", "delete post", $discussionurl, "$post->id", $cm->id);
+                $params = array(
+                    'context' => $modcontext,
+                    'objectid' => $post->id,
+                    'other' => array(
+                        'discussionid' => $discussion->id,
+                        'forumid' => $forum->id,
+                        'forumtype' => $forum->type,
+                    )
+                );
+
+                if ($post->userid !== $USER->id) {
+                    $params['relateduserid'] = $post->userid;
+                }
+                $event = \mod_forum\event\post_deleted::create($params);
+                $event->add_record_snapshot('forum_posts', $post);
+                $event->add_record_snapshot('forum_discussions', $discussion);
+                $event->trigger();
 
                 redirect(forum_go_back_to($discussionurl));
             } else {
@@ -455,8 +480,39 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
         forum_discussion_update_last_post($discussion->id);
         forum_discussion_update_last_post($newid);
 
-        add_to_log($discussion->course, "forum", "prune post",
-                       "discuss.php?d=$newid", "$post->id", $cm->id);
+        // Fire events to reflect the split..
+        $params = array(
+            'context' => $modcontext,
+            'objectid' => $discussion->id,
+            'other' => array(
+                'forumid' => $forum->id,
+            )
+        );
+        $event = \mod_forum\event\discussion_updated::create($params);
+        $event->trigger();
+
+        $params = array(
+            'context' => $modcontext,
+            'objectid' => $newid,
+            'other' => array(
+                'forumid' => $forum->id,
+            )
+        );
+        $event = \mod_forum\event\discussion_created::create($params);
+        $event->trigger();
+
+        $params = array(
+            'context' => $modcontext,
+            'objectid' => $post->id,
+            'other' => array(
+                'discussionid' => $newid,
+                'forumid' => $forum->id,
+                'forumtype' => $forum->type,
+            )
+        );
+        $event = \mod_forum\event\post_updated::create($params);
+        $event->add_record_snapshot('forum_discussions', $discussion);
+        $event->trigger();
 
         redirect(forum_go_back_to("discuss.php?d=$newid"));
 
@@ -683,8 +739,25 @@ if ($fromform = $mform_post->get_data()) {
         } else {
             $discussionurl = "discuss.php?d=$discussion->id#p$fromform->id";
         }
-        add_to_log($course->id, "forum", "update post",
-                "$discussionurl&amp;parent=$fromform->id", "$fromform->id", $cm->id);
+
+        $params = array(
+            'context' => $modcontext,
+            'objectid' => $fromform->id,
+            'other' => array(
+                'discussionid' => $discussion->id,
+                'forumid' => $forum->id,
+                'forumtype' => $forum->type,
+            )
+        );
+
+        if ($realpost->userid !== $USER->id) {
+            $params['relateduserid'] = $realpost->userid;
+        }
+
+        $event = \mod_forum\event\post_updated::create($params);
+        $event->add_record_snapshot('forum_posts', $fromform);
+        $event->add_record_snapshot('forum_discussions', $discussion);
+        $event->trigger();
 
         redirect(forum_go_back_to("$discussionurl"), $message.$subscribemessage, $timemessage);
 
@@ -726,8 +799,20 @@ if ($fromform = $mform_post->get_data()) {
             } else {
                 $discussionurl = "discuss.php?d=$discussion->id";
             }
-            add_to_log($course->id, "forum", "add post",
-                      "$discussionurl&amp;parent=$fromform->id", "$fromform->id", $cm->id);
+
+            $params = array(
+                'context' => $modcontext,
+                'objectid' => $fromform->id,
+                'other' => array(
+                    'discussionid' => $discussion->id,
+                    'forumid' => $forum->id,
+                    'forumtype' => $forum->type,
+                )
+            );
+            $event = \mod_forum\event\post_created::create($params);
+            $event->add_record_snapshot('forum_posts', $fromform);
+            $event->add_record_snapshot('forum_discussions', $discussion);
+            $event->trigger();
 
             // Update completion state
             $completion=new completion_info($course);
@@ -773,8 +858,16 @@ if ($fromform = $mform_post->get_data()) {
         $message = '';
         if ($discussion->id = forum_add_discussion($discussion, $mform_post, $message)) {
 
-            add_to_log($course->id, "forum", "add discussion",
-                    "discuss.php?d=$discussion->id", "$discussion->id", $cm->id);
+            $params = array(
+                'context' => $modcontext,
+                'objectid' => $discussion->id,
+                'other' => array(
+                    'forumid' => $forum->id,
+                )
+            );
+            $event = \mod_forum\event\discussion_created::create($params);
+            $event->add_record_snapshot('forum_discussions', $discussion);
+            $event->trigger();
 
             $timemessage = 2;
             if (!empty($message)) { // if we're printing stuff about the file upload
index c7cc504..4f257b1 100644 (file)
@@ -112,7 +112,13 @@ if (!$course = $DB->get_record('course', array('id'=>$id))) {
 
 require_course_login($course);
 
-add_to_log($course->id, "forum", "search", "search.php?id=$course->id&amp;search=".urlencode($search), $search);
+$params = array(
+    'context' => $PAGE->context,
+    'other' => array('searchterm' => $search)
+);
+
+$event = \mod_forum\event\course_searched::create($params);
+$event->trigger();
 
 $strforums = get_string("modulenameplural", "forum");
 $strsearch = get_string("search", "forum");
index 9a39722..f8b8587 100644 (file)
@@ -58,9 +58,17 @@ if (!forum_tp_can_track_forums($forum)) {
 $info = new stdClass();
 $info->name  = fullname($USER);
 $info->forum = format_string($forum->name);
+
+$eventparams = array(
+    'context' => context_module::instance($cm->id),
+    'relateduserid' => $USER->id,
+    'other' => array('forumid' => $forum->id),
+);
+
 if (forum_tp_is_tracked($forum) ) {
     if (forum_tp_stop_tracking($forum->id)) {
-        add_to_log($course->id, "forum", "stop tracking", "view.php?f=$forum->id", $forum->id, $cm->id);
+        $event = \mod_forum\event\readtracking_disabled::create($eventparams);
+        $event->trigger();
         redirect($returnto, get_string("nownottracking", "forum", $info), 1);
     } else {
         print_error('cannottrack', '', $_SERVER["HTTP_REFERER"]);
@@ -68,7 +76,8 @@ if (forum_tp_is_tracked($forum) ) {
 
 } else { // subscribe
     if (forum_tp_start_tracking($forum->id)) {
-        add_to_log($course->id, "forum", "start tracking", "view.php?f=$forum->id", $forum->id, $cm->id);
+        $event = \mod_forum\event\readtracking_enabled::create($eventparams);
+        $event->trigger();
         redirect($returnto, get_string("nowtracking", "forum", $info), 1);
     } else {
         print_error('cannottrack', '', $_SERVER["HTTP_REFERER"]);
index 1bd5277..37aea54 100644 (file)
@@ -149,7 +149,6 @@ if (forum_is_subscribed($user->id, $forum->id)) {
     }
     require_sesskey();
     if (forum_unsubscribe($user->id, $forum->id)) {
-        add_to_log($course->id, "forum", "unsubscribe", "view.php?f=$forum->id", $forum->id, $cm->id);
         redirect($returnto, get_string("nownotsubscribed", "forum", $info), 1);
     } else {
         print_error('cannotunsubscribe', 'forum', $_SERVER["HTTP_REFERER"]);
@@ -174,6 +173,5 @@ if (forum_is_subscribed($user->id, $forum->id)) {
     }
     require_sesskey();
     forum_subscribe($user->id, $forum->id);
-    add_to_log($course->id, "forum", "subscribe", "view.php?f=$forum->id", $forum->id, $cm->id);
     redirect($returnto, get_string("nowsubscribed", "forum", $info), 1);
 }
index 87c3b54..eda984f 100644 (file)
@@ -54,7 +54,12 @@ if (!has_capability('mod/forum:viewsubscribers', $context)) {
 
 unset($SESSION->fromdiscussion);
 
-add_to_log($course->id, "forum", "view subscribers", "subscribers.php?id=$forum->id", $forum->id, $cm->id);
+$params = array(
+    'context' => $context,
+    'other' => array('forumid' => $forum->id),
+);
+$event = \mod_forum\event\subscribers_viewed::create($params);
+$event->trigger();
 
 $forumoutput = $PAGE->get_renderer('mod_forum');
 $currentgroup = groups_get_activity_group($cm);
diff --git a/mod/forum/tests/events_test.php b/mod/forum/tests/events_test.php
new file mode 100644 (file)
index 0000000..105b42a
--- /dev/null
@@ -0,0 +1,1750 @@
+<?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/>.
+
+/**
+ * Tests for forum events.
+ *
+ * @package    mod_forum
+ * @category   test
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests for forum events.
+ *
+ * @package    mod_forum
+ * @category   test
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_forum_events_testcase extends advanced_testcase {
+
+    /**
+     * Tests set up.
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    /**
+     * Ensure course_searched event validates that searchterm is set.
+     */
+    public function test_course_searched_searchterm_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $coursectx = context_course::instance($course->id);
+        $params = array(
+            'context' => $coursectx,
+        );
+
+        $this->setExpectedException('coding_exception', 'searchterm must be set in $other.');
+        \mod_forum\event\course_searched::create($params);
+    }
+
+    /**
+     * Ensure course_searched event validates that context is the correct level.
+     */
+    public function test_course_searched_context_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+        $params = array(
+            'context' => $context,
+            'other' => array('searchterm' => 'testing'),
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be course.');
+        \mod_forum\event\course_searched::create($params);
+    }
+
+    /**
+     * Test course_searched event.
+     */
+    public function test_course_searched() {
+
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $coursectx = context_course::instance($course->id);
+        $searchterm = 'testing123';
+
+        $params = array(
+            'context' => $coursectx,
+            'other' => array('searchterm' => $searchterm),
+        );
+
+        // Create event.
+        $event = \mod_forum\event\course_searched::create($params);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+         // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\course_searched', $event);
+        $this->assertEquals($coursectx, $event->get_context());
+        $expected = array($course->id, 'forum', 'search', "search.php?id={$course->id}&amp;search={$searchterm}", $searchterm);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     * Ensure discussion_created event validates that forumid is set.
+     */
+    public function test_discussion_created_forumid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in $other.');
+        \mod_forum\event\discussion_created::create($params);
+    }
+
+    /**
+     * Ensure discussion_created event validates that discussionid is set.
+     */
+    public function test_discussion_created_objectid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('forumid' => $forum->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'objectid must be set to the discussionid.');
+        \mod_forum\event\discussion_created::create($params);
+    }
+
+    /**
+     * Ensure discussion_created event validates that the context is the correct level.
+     */
+    public function test_discussion_created_context_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_system::instance(),
+            'other' => array('forumid' => $forum->id),
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\discussion_created::create($params);
+    }
+
+    /**
+     * Test discussion_created event.
+     */
+    public function test_discussion_created() {
+
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'objectid' => $discussion->id,
+            'other' => array('forumid' => $forum->id),
+        );
+
+        // Create the event.
+        $event = \mod_forum\event\discussion_created::create($params);
+
+        // Trigger and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Check that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\discussion_created', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'add discussion', "discuss.php?d={$discussion->id}", $discussion->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     * Ensure discussion_updated event validates that forumid is set.
+     */
+    public function test_discussion_updated_forumid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in $other.');
+        \mod_forum\event\discussion_updated::create($params);
+    }
+
+    /**
+     * Ensure discussion_updated event validates that discussionid is set.
+     */
+    public function test_discussion_updated_objectid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('forumid' => $forum->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'objectid must be set to the discussionid.');
+        \mod_forum\event\discussion_updated::create($params);
+    }
+
+    /**
+     * Ensure discussion_created event validates that the context is the correct level.
+     */
+    public function test_discussion_updated_context_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_system::instance(),
+            'other' => array('forumid' => $forum->id),
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\discussion_updated::create($params);
+    }
+
+    /**
+     * Test discussion_created event.
+     */
+    public function test_discussion_updated() {
+
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'objectid' => $discussion->id,
+            'other' => array('forumid' => $forum->id),
+        );
+
+        // Create the event.
+        $event = \mod_forum\event\discussion_updated::create($params);
+
+        // Trigger and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Check that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\discussion_updated', $event);
+        $this->assertEquals($context, $event->get_context());
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     * Ensure discussion_deleted event validates that forumid is set.
+     */
+    public function test_discussion_deleted_forumid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in $other.');
+        \mod_forum\event\discussion_deleted::create($params);
+    }
+
+    /**
+     * Ensure discussion_deleted event validates that discussionid is set.
+     */
+    public function test_discussion_deleted_objectid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('forumid' => $forum->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'objectid must be set to the discussionid.');
+        \mod_forum\event\discussion_deleted::create($params);
+    }
+
+    /**
+     * Ensure discussion_deleted event validates that context is of the correct level.
+     */
+    public function test_discussion_deleted_context_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_system::instance(),
+            'other' => array('forumid' => $forum->id),
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\discussion_deleted::create($params);
+    }
+
+    /**
+     * Test discussion_deleted event.
+     */
+    public function test_discussion_deleted() {
+
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'objectid' => $discussion->id,
+            'other' => array('forumid' => $forum->id),
+        );
+
+        $event = \mod_forum\event\discussion_deleted::create($params);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\discussion_deleted', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'delete discussion', "view.php?id={$forum->cmid}", $forum->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     * Ensure discussion_moved event validates that fromforumid is set.
+     */
+    public function test_discussion_moved_fromforumid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $toforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $context = context_module::instance($toforum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('toforumid' => $toforum->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'fromforumid must be set in $other.');
+        \mod_forum\event\discussion_moved::create($params);
+    }
+
+    /**
+     * Ensure discussion_moved event validates that toforumid is set.
+     */
+    public function test_discussion_moved_toforumid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $fromforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $toforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($toforum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('fromforumid' => $fromforum->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'toforumid must be set in $other.');
+        \mod_forum\event\discussion_moved::create($params);
+    }
+
+    /**
+     * Ensure discussion_moved event validates that the discussionid is set.
+     */
+    public function test_discussion_moved_objectid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $fromforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $toforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($toforum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('fromforumid' => $fromforum->id, 'toforumid' => $toforum->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'objectid must be set to the discussionid.');
+        \mod_forum\event\discussion_moved::create($params);
+    }
+
+    /**
+     * Ensure discussion_moved event validates that the context level is correct.
+     */
+    public function test_discussion_moved_context_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $fromforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $toforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $fromforum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        $params = array(
+            'context' => context_system::instance(),
+            'objectid' => $discussion->id,
+            'other' => array('fromforumid' => $fromforum->id, 'toforumid' => $toforum->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\discussion_moved::create($params);
+    }
+
+    /**
+     * Test discussion_moved event.
+     */
+    public function test_discussion_moved() {
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $fromforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $toforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $fromforum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        $context = context_module::instance($toforum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'objectid' => $discussion->id,
+            'other' => array('fromforumid' => $fromforum->id, 'toforumid' => $toforum->id)
+        );
+
+        $event = \mod_forum\event\discussion_moved::create($params);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\discussion_moved', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'move discussion', "discuss.php?d={$discussion->id}",
+            $discussion->id, $toforum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     * Ensure discussion_viewed event validates that the discussionid is set
+     */
+    public function test_discussion_viewed_objectid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+        );
+
+        $this->setExpectedException('coding_exception', 'objectid must be set to the discussionid.');
+        \mod_forum\event\discussion_viewed::create($params);
+    }
+
+    /**
+     * Ensure discussion_viewed event validates that the contextlevel is correct.
+     */
+    public function test_discussion_viewed_context_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        $params = array(
+            'context' => context_system::instance(),
+            'objectid' => $discussion->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\discussion_viewed::create($params);
+    }
+
+    /**
+     * Test discussion_viewed event.
+     */
+    public function test_discussion_viewed() {
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'objectid' => $discussion->id,
+        );
+
+        $event = \mod_forum\event\discussion_viewed::create($params);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\discussion_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'view discussion', "discuss.php?d={$discussion->id}",
+            $discussion->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     * Ensure forum_viewed event validates that the forumid is set.
+     */
+    public function test_forum_viewed_objectid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+        );
+
+        $this->setExpectedException('coding_exception', 'objectid must be set to the forumid.');
+        \mod_forum\event\forum_viewed::create($params);
+    }
+
+    /**
+     * Ensure forum_viewed event validates that the contextlevel is correct.
+     */
+    public function test_forum_viewed_context_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_system::instance(),
+            'objectid' => $forum->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\forum_viewed::create($params);
+    }
+
+    /**
+     * Test the forum_viewed event.
+     */
+    public function test_forum_viewed() {
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'objectid' => $forum->id,
+        );
+
+        $event = \mod_forum\event\forum_viewed::create($params);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\forum_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'view forum', "view.php?f={$forum->id}", $forum->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     * Ensure subscription_created event validates that the forumid is set.
+     */
+    public function test_subscription_created_forumid_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in other.');
+        \mod_forum\event\subscription_created::create($params);
+    }
+
+    /**
+     * Ensure subscription_created event validates that the relateduserid is set.
+     */
+    public function test_subscription_created_relateduserid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $forum->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'relateduserid must be set.');
+        \mod_forum\event\subscription_created::create($params);
+    }
+
+    /**
+     * Ensure subscription_created event validates that the contextlevel is correct.
+     */
+    public function test_subscription_created_contextlevel_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_system::instance(),
+            'other' => array('forumid' => $forum->id),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\subscription_created::create($params);
+    }
+
+    /**
+     * Test the subscription_created event.
+     */
+    public function test_subscription_created() {
+        // Setup test data.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('forumid' => $forum->id),
+            'relateduserid' => $user->id,
+        );
+
+        $event = \mod_forum\event\subscription_created::create($params);
+
+        // Trigger and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\subscription_created', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'subscribe', "view.php?f={$forum->id}", $forum->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     * Ensure subscription_deleted event validates that the forumid is set.
+     */
+    public function test_subscription_deleted_forumid_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in other.');
+        \mod_forum\event\subscription_deleted::create($params);
+    }
+
+    /**
+     * Ensure subscription_deleted event validates that the relateduserid is set.
+     */
+    public function test_subscription_deleted_relateduserid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $forum->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'relateduserid must be set.');
+        \mod_forum\event\subscription_deleted::create($params);
+    }
+
+    /**
+     * Ensure subscription_deleted event validates that the contextlevel is correct.
+     */
+    public function test_subscription_deleted_contextlevel_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_system::instance(),
+            'other' => array('forumid' => $forum->id),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\subscription_deleted::create($params);
+    }
+
+    /**
+     * Test the subscription_deleted event.
+     */
+    public function test_subscription_deleted() {
+        // Setup test data.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('forumid' => $forum->id),
+            'relateduserid' => $user->id,
+        );
+
+        $event = \mod_forum\event\subscription_deleted::create($params);
+
+        // Trigger and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\subscription_deleted', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'unsubscribe', "view.php?f={$forum->id}", $forum->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     * Ensure readtracking_enabled event validates that the forumid is set.
+     */
+    public function test_readtracking_enabled_forumid_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in other.');
+        \mod_forum\event\readtracking_enabled::create($params);
+    }
+
+    /**
+     * Ensure readtracking_enabled event validates that the relateduserid is set.
+     */
+    public function test_readtracking_enabled_relateduserid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $forum->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'relateduserid must be set.');
+        \mod_forum\event\readtracking_enabled::create($params);
+    }
+
+    /**
+     * Ensure readtracking_enabled event validates that the contextlevel is correct.
+     */
+    public function test_readtracking_enabled_contextlevel_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_system::instance(),
+            'other' => array('forumid' => $forum->id),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\readtracking_enabled::create($params);
+    }
+
+    /**
+     * Test the readtracking_enabled event.
+     */
+    public function test_readtracking_enabled() {
+        // Setup test data.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('forumid' => $forum->id),
+            'relateduserid' => $user->id,
+        );
+
+        $event = \mod_forum\event\readtracking_enabled::create($params);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\readtracking_enabled', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'start tracking', "view.php?f={$forum->id}", $forum->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     *  Ensure readtracking_disabled event validates that the forumid is set.
+     */
+    public function test_readtracking_disabled_forumid_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in other.');
+        \mod_forum\event\readtracking_disabled::create($params);
+    }
+
+    /**
+     *  Ensure readtracking_disabled event validates that the relateduserid is set.
+     */
+    public function test_readtracking_disabled_relateduserid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $forum->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'relateduserid must be set.');
+        \mod_forum\event\readtracking_disabled::create($params);
+    }
+
+    /**
+     *  Ensure readtracking_disabled event validates that the contextlevel is correct
+     */
+    public function test_readtracking_disabled_contextlevel_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_system::instance(),
+            'other' => array('forumid' => $forum->id),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\readtracking_disabled::create($params);
+    }
+
+    /**
+     *  Test the readtracking_disabled event.
+     */
+    public function test_readtracking_disabled() {
+        // Setup test data.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('forumid' => $forum->id),
+            'relateduserid' => $user->id,
+        );
+
+        $event = \mod_forum\event\readtracking_disabled::create($params);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\readtracking_disabled', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'stop tracking', "view.php?f={$forum->id}", $forum->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     *  Ensure subscribers_viewed event validates that the forumid is set.
+     */
+    public function test_subscribers_viewed_forumid_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in other.');
+        \mod_forum\event\subscribers_viewed::create($params);
+    }
+
+    /**
+     *  Ensure subscribers_viewed event validates that the contextlevel is correct.
+     */
+    public function test_subscribers_viewed_contextlevel_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_system::instance(),
+            'other' => array('forumid' => $forum->id),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context.');
+        \mod_forum\event\subscribers_viewed::create($params);
+    }
+
+    /**
+     *  Test the subscribers_viewed event.
+     */
+    public function test_subscribers_viewed() {
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'other' => array('forumid' => $forum->id),
+        );
+
+        $event = \mod_forum\event\subscribers_viewed::create($params);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\subscribers_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'view subscribers', "subscribers.php?id={$forum->id}", $forum->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     *  Ensure userreport_viewed event validates that the reportmode is set.
+     */
+    public function test_userreport_viewed_reportmode_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+
+        $params = array(
+            'context' => context_course::instance($course->id),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'reportmode must be set in other.');
+        \mod_forum\event\userreport_viewed::create($params);
+    }
+
+    /**
+     *  Ensure userreport_viewed event validates that the contextlevel is correct.
+     */
+    public function test_userreport_viewed_contextlevel_validation() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        $params = array(
+            'context' => context_module::instance($forum->id),
+            'other' => array('reportmode' => 'posts'),
+            'relateduserid' => $user->id,
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be system or course.');
+        \mod_forum\event\userreport_viewed::create($params);
+    }
+
+    /**
+     *  Ensure userreport_viewed event validates that the relateduserid is set.
+     */
+    public function test_userreport_viewed_relateduserid_validation() {
+
+        $params = array(
+            'context' => context_system::instance(),
+            'other' => array('reportmode' => 'posts'),
+        );
+
+        $this->setExpectedException('coding_exception', 'relateduserid must be set.');
+        \mod_forum\event\userreport_viewed::create($params);
+    }
+
+    /**
+     * Test the userreport_viewed event.
+     */
+    public function test_userreport_viewed() {
+        // Setup test data.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+
+        $params = array(
+            'context' => $context,
+            'relateduserid' => $user->id,
+            'other' => array('reportmode' => 'discussions'),
+        );
+
+        $event = \mod_forum\event\userreport_viewed::create($params);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\userreport_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'user report',
+            "user.php?id={$user->id}&amp;mode=discussions&amp;course={$course->id}", $user->id);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     *  Ensure post_created event validates that the postid is set.
+     */
+    public function test_post_created_postid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type, 'discussionid' => $discussion->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'objectid must be set to the postid.');
+        \mod_forum\event\post_created::create($params);
+    }
+
+    /**
+     *  Ensure post_created event validates that the discussionid is set.
+     */
+    public function test_post_created_discussionid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $post->id,
+            'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type)
+        );
+
+        $this->setExpectedException('coding_exception', 'discussionid must be set in other.');
+        \mod_forum\event\post_created::create($params);
+    }
+
+    /**
+     *  Ensure post_created event validates that the forumid is set.
+     */
+    public function test_post_created_forumid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumtype' => $forum->type)
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in other.');
+        \mod_forum\event\post_created::create($params);
+    }
+
+    /**
+     *  Ensure post_created event validates that the forumtype is set.
+     */
+    public function test_post_created_forumtype_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'forumtype must be set in other.');
+        \mod_forum\event\post_created::create($params);
+    }
+
+    /**
+     *  Ensure post_created event validates that the contextlevel is correct.
+     */
+    public function test_post_created_context_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_system::instance(),
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type)
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context');
+        \mod_forum\event\post_created::create($params);
+    }
+
+    /**
+     * Test the post_created event.
+     */
+    public function test_post_created() {
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type)
+        );
+
+        $event = \mod_forum\event\post_created::create($params);
+
+        // Trigger and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\post_created', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'add post', "discuss.php?d={$discussion->id}#p{$post->id}",
+            $forum->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     *  Ensure post_deleted event validates that the postid is set.
+     */
+    public function test_post_deleted_postid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type, 'discussionid' => $discussion->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'objectid must be set to the postid.');
+        \mod_forum\event\post_deleted::create($params);
+    }
+
+    /**
+     *  Ensure post_deleted event validates that the discussionid is set.
+     */
+    public function test_post_deleted_discussionid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $post->id,
+            'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type)
+        );
+
+        $this->setExpectedException('coding_exception', 'discussionid must be set in other.');
+        \mod_forum\event\post_deleted::create($params);
+    }
+
+    /**
+     *  Ensure post_deleted event validates that the forumid is set.
+     */
+    public function test_post_deleted_forumid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumtype' => $forum->type)
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in other.');
+        \mod_forum\event\post_deleted::create($params);
+    }
+
+    /**
+     *  Ensure post_deleted event validates that the forumtype is set.
+     */
+    public function test_post_deleted_forumtype_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'forumtype must be set in other.');
+        \mod_forum\event\post_deleted::create($params);
+    }
+
+    /**
+     *  Ensure post_deleted event validates that the contextlevel is correct.
+     */
+    public function test_post_deleted_context_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_system::instance(),
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type)
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context');
+        \mod_forum\event\post_deleted::create($params);
+    }
+
+    /**
+     * Test post_deleted event.
+     */
+    public function test_post_deleted() {
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type)
+        );
+
+        $event = \mod_forum\event\post_deleted::create($params);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\post_deleted', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'delete post', "discuss.php?d={$discussion->id}", $post->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     *  Ensure post_updated event validates that the postid is set.
+     */
+    public function test_post_updated_postid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type, 'discussionid' => $discussion->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'objectid must be set to the postid.');
+        \mod_forum\event\post_updated::create($params);
+    }
+
+    /**
+     *  Ensure post_updated event validates that the discussionid is set.
+     */
+    public function test_post_updated_discussionid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $post->id,
+            'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type)
+        );
+
+        $this->setExpectedException('coding_exception', 'discussionid must be set in other.');
+        \mod_forum\event\post_updated::create($params);
+    }
+
+    /**
+     *  Ensure post_updated event validates that the forumid is set.
+     */
+    public function test_post_updated_forumid_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumtype' => $forum->type)
+        );
+
+        $this->setExpectedException('coding_exception', 'forumid must be set in other.');
+        \mod_forum\event\post_updated::create($params);
+    }
+
+    /**
+     *  Ensure post_updated event validates that the forumtype is set.
+     */
+    public function test_post_updated_forumtype_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_module::instance($forum->cmid),
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id)
+        );
+
+        $this->setExpectedException('coding_exception', 'forumtype must be set in other.');
+        \mod_forum\event\post_updated::create($params);
+    }
+
+    /**
+     *  Ensure post_updated event validates that the contextlevel is correct.
+     */
+    public function test_post_updated_context_validation() {
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $params = array(
+            'context' => context_system::instance(),
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type)
+        );
+
+        $this->setExpectedException('coding_exception', 'Context passed must be module context');
+        \mod_forum\event\post_updated::create($params);
+    }
+
+    /**
+     * Test post_updated event.
+     */
+    public function test_post_updated() {
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a discussion.
+        $record = array();
+        $record['course'] = $course->id;
+        $record['forum'] = $forum->id;
+        $record['userid'] = $user->id;
+        $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Add a post.
+        $record = array();
+        $record['discussion'] = $discussion->id;
+        $record['userid'] = $user->id;
+        $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+        $context = context_module::instance($forum->cmid);
+
+        $params = array(
+            'context' => $context,
+            'objectid' => $post->id,
+            'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type)
+        );
+
+        $event = \mod_forum\event\post_updated::create($params);
+
+        // Trigger and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_forum\event\post_updated', $event);
+        $this->assertEquals($context, $event->get_context());
+        $expected = array($course->id, 'forum', 'update post', "discuss.php?d={$discussion->id}#p{$post->id}",
+            $post->id, $forum->cmid);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+
+        $this->assertNotEmpty($event->get_name());
+    }
+}
index 0059ae8..6c5c3c5 100644 (file)
@@ -62,8 +62,6 @@ if ($perpage != 5) {
     $url->param('perpage', $perpage);
 }
 
-add_to_log(($isspecificcourse)?$courseid:SITEID, "forum", "user report", 'user.php?'.$url->get_query_string(), $userid);
-
 $user = $DB->get_record("user", array("id" => $userid), '*', MUST_EXIST);
 $usercontext = context_user::instance($user->id, MUST_EXIST);
 // Check if the requested user is the guest user
@@ -117,6 +115,15 @@ if ($isspecificcourse) {
     $courses = forum_get_courses_user_posted_in($user, $discussionsonly);
 }
 
+
+$params = array(
+    'context' => $PAGE->context,
+    'relateduserid' => $user->id,
+    'other' => array('reportmode' => $mode),
+);
+$event = \mod_forum\event\userreport_viewed::create($params);
+$event->trigger();
+
 // Get the posts by the requested user that the current user can access.
 $result = forum_get_posts_by_user($user, $courses, $isspecificcourse, $discussionsonly, ($page * $perpage), $perpage);
 
index 8f874d5..1a966f2 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013110500;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2014021700;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2013110500;       // Requires this Moodle version
 $plugin->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 60;
index ec69112..8081f64 100644 (file)
 /// find out current groups mode
     groups_print_activity_menu($cm, $CFG->wwwroot . '/mod/forum/view.php?id=' . $cm->id);
 
-/// Okay, we can show the discussions. Log the forum view.
-    if ($cm->id) {
-        add_to_log($course->id, "forum", "view forum", "view.php?id=$cm->id", "$forum->id", $cm->id);
-    } else {
-        add_to_log($course->id, "forum", "view forum", "view.php?f=$forum->id", "$forum->id");
-    }
+    $params = array(
+        'context' => $context,
+        'objectid' => $forum->id
+    );
+    $event = \mod_forum\event\forum_viewed::create($params);
+    $event->add_record_snapshot('forum', $forum);
+    $event->trigger();
 
     $SESSION->fromdiscussion = qualified_me();   // Return here if we post or set subscription etc
 
index 9b88976..ac66183 100644 (file)
@@ -364,7 +364,7 @@ function glossary_get_recent_mod_activity(&$activities, &$index, $timestart, $co
     $params['timestart'] = $timestart;
     $params['glossaryid'] = $cm->instance;
 
-    $ufields = user_picture::fields('u', array('lastaccess', 'firstname', 'lastname', 'email', 'picture', 'imagealt'));
+    $ufields = user_picture::fields('u');
     $entries = $DB->get_records_sql("
               SELECT ge.id AS entryid, ge.*, $ufields
                 FROM {glossary_entries} ge
@@ -399,6 +399,9 @@ function glossary_get_recent_mod_activity(&$activities, &$index, $timestart, $co
         }
 
         $tmpactivity                       = new stdClass();
+        $tmpactivity->user = username_load_fields_from_object(new stdClass(), $entry, null,
+                explode(',', user_picture::fields()));
+        $tmpactivity->user->fullname       = fullname($tmpactivity->user, $viewfullnames);
         $tmpactivity->type                 = 'glossary';
         $tmpactivity->cmid                 = $cm->id;
         $tmpactivity->glossaryid           = $entry->glossaryid;
@@ -409,14 +412,6 @@ function glossary_get_recent_mod_activity(&$activities, &$index, $timestart, $co
         $tmpactivity->content->entryid     = $entry->entryid;
         $tmpactivity->content->concept     = $entry->concept;
         $tmpactivity->content->definition  = $entry->definition;
-        $tmpactivity->user                 = new stdClass();
-        $tmpactivity->user->id             = $entry->userid;
-        $tmpactivity->user->firstname      = $entry->firstname;
-        $tmpactivity->user->lastname       = $entry->lastname;
-        $tmpactivity->user->fullname       = fullname($entry, $viewfullnames);
-        $tmpactivity->user->picture        = $entry->picture;
-        $tmpactivity->user->imagealt       = $entry->imagealt;
-        $tmpactivity->user->email          = $entry->email;
 
         $activities[$index++] = $tmpactivity;
     }
index 6dac738..b29ae77 100644 (file)
@@ -847,9 +847,10 @@ function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
     $params['timestart'] = $timestart;
     $params['quizid'] = $quiz->id;
 
+    $ufields = user_picture::fields('u');
     if (!$attempts = $DB->get_records_sql("
               SELECT qa.*,
-                     u.firstname, u.lastname, u.email, u.picture, u.imagealt
+                     {$ufields}
                 FROM {quiz_attempts} qa
                      JOIN {user} u ON u.id = qa.userid
                      $groupjoin
@@ -919,14 +920,9 @@ function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
             $tmpactivity->content->maxgrade  = null;
         }
 
-        $tmpactivity->user = new stdClass();
-        $tmpactivity->user->id        = $attempt->userid;
-        $tmpactivity->user->firstname = $attempt->firstname;
-        $tmpactivity->user->lastname  = $attempt->lastname;
-        $tmpactivity->user->fullname  = fullname($attempt, $viewfullnames);
-        $tmpactivity->user->picture   = $attempt->picture;
-        $tmpactivity->user->imagealt  = $attempt->imagealt;
-        $tmpactivity->user->email     = $attempt->email;
+        $tmpactivity->user = username_load_fields_from_object(new stdClass(), $attempt, null,
+                explode(',', user_picture::fields()));
+        $tmpactivity->user->fullname  = fullname($tmpactivity->user, $viewfullnames);
 
         $activities[$index++] = $tmpactivity;
     }
index 14f2483..93280a7 100644 (file)
@@ -387,7 +387,7 @@ class qformat_default {
             $question->modifiedby = $USER->id;
             $question->timemodified = time();
             $fileoptions = array(
-                    'subdirs' => false,
+                    'subdirs' => true,
                     'maxfiles' => -1,
                     'maxbytes' => 0,
                 );
index df9e609..304d379 100644 (file)
@@ -169,11 +169,13 @@ class qformat_xml extends qformat_default {
         }
         $fs = get_file_storage();
         $itemid = file_get_unused_draft_itemid();
-        $filenames = array();
+        $filepaths = array();
         foreach ($xml as $file) {
-            $filename = $file['@']['name'];
-            if (in_array($filename, $filenames)) {
-                debugging('Duplicate file in XML: ' . $filename, DEBUG_DEVELOPER);
+            $filename = $this->getpath($file, array('@', 'name'), '', true);
+            $filepath = $this->getpath($file, array('@', 'path'), '/', true);
+            $fullpath = $filepath . $filename;
+            if (in_array($fullpath, $filepaths)) {
+                debugging('Duplicate file in XML: ' . $fullpath, DEBUG_DEVELOPER);
                 continue;
             }
             $filerecord = array(
@@ -181,11 +183,11 @@ class qformat_xml extends qformat_default {
                 'component' => 'user',
                 'filearea'  => 'draft',
                 'itemid'    => $itemid,
-                'filepath'  => '/',
+                'filepath'  => $filepath,
                 'filename'  => $filename,
             );
             $fs->create_file_from_string($filerecord, base64_decode($file['#']));
-            $filenames[] = $filename;
+            $filepaths[] = $fullpath;
         }
         return $itemid;
     }
@@ -1092,9 +1094,9 @@ class qformat_xml extends qformat_default {
             if ($file->is_directory()) {
                 continue;
             }
-            $string .= '<file name="' . $file->get_filename() . '" encoding="base64">';
+            $string .= '<file name="' . $file->get_filename() . '" path="' . $file->get_filepath() . '" encoding="base64">';
             $string .= base64_encode($file->get_content());
-            $string .= '</file>';
+            $string .= "</file>\n";
         }
         return $string;
     }
index 8396838..429f787 100644 (file)
@@ -1473,4 +1473,51 @@ END;
         $this->assertEquals('/',          $file->filepath);
         $this->assertEquals(6,            $file->size);
     }
+
+    public function test_import_truefalse_wih_files() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $xml = '<question type="truefalse">
+    <name>
+      <text>truefalse</text>
+    </name>
+    <questiontext format="html">
+      <text><![CDATA[<p><a href="@@PLUGINFILE@@/myfolder/moodle.txt">This text file</a> contains the word Moodle.</p>]]></text>
+<file name="moodle.txt" path="/myfolder/" encoding="base64">TW9vZGxl</file>
+    </questiontext>
+    <generalfeedback format="html">
+      <text><![CDATA[<p>For further information, see the documentation about Moodle.</p>]]></text>
+</generalfeedback>
+    <defaultgrade>1.0000000</defaultgrade>
+    <penalty>1.0000000</penalty>
+    <hidden>0</hidden>
+    <answer fraction="100" format="moodle_auto_format">
+      <text>true</text>
+      <feedback format="html">
+        <text></text>
+      </feedback>
+    </answer>
+    <answer fraction="0" format="moodle_auto_format">
+      <text>false</text>
+      <feedback format="html">
+        <text></text>
+      </feedback>
+    </answer>
+  </question>';
+        $xmldata = xmlize($xml);
+
+        $importer = new qformat_xml();
+        $q = $importer->import_truefalse($xmldata['question']);
+
+        $draftitemid = $q->questiontextitemid;
+        $files = file_get_drafarea_files($draftitemid, '/myfolder/');
+
+        $this->assertEquals(1, count($files->list));
+
+        $file = $files->list[0];
+        $this->assertEquals('moodle.txt', $file->filename);
+        $this->assertEquals('/myfolder/', $file->filepath);
+        $this->assertEquals(6,            $file->size);
+    }
 }
index 5bc2f82..09c2c7a 100644 (file)
@@ -58,9 +58,7 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
             $result .= html_writer::start_tag('tr', array('class' => 'r' . $parity));
             $fieldname = 'sub' . $key;
 
-            $result .= html_writer::tag('td', $question->format_text(
-                    $question->stems[$stemid], $question->stemformat[$stemid],
-                    $qa, 'qtype_match', 'subquestion', $stemid),
+            $result .= html_writer::tag('td', $this->format_stem_text($qa, $stemid),
                     array('class' => 'text'));
 
             $classes = 'control';
@@ -109,6 +107,20 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
         return $this->combined_feedback($qa);
     }
 
+    /**
+     * Format each question stem. Overwritten by randomsamatch renderer.
+     *
+     * @param question_attempt $qa
+     * @param integer $stemid stem index
+     * @return string
+     */
+    public function format_stem_text($qa, $stemid) {
+        $question = $qa->get_question();
+        return $question->format_text(
+                    $question->stems[$stemid], $question->stemformat[$stemid],
+                    $qa, 'qtype_match', 'subquestion', $stemid);
+    }
+
     protected function format_choices($question) {
         $choices = array();
         foreach ($question->get_choice_order() as $key => $choiceid) {
@@ -125,9 +137,7 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
         $choices = $this->format_choices($question);
         $right = array();
         foreach ($stemorder as $key => $stemid) {
-            $right[] = $question->format_text($question->stems[$stemid],
-                    $question->stemformat[$stemid], $qa,
-                    'qtype_match', 'subquestion', $stemid) . ' – ' .
+            $right[] = $this->format_stem_text($qa, $stemid) . ' – ' .
                     $choices[$question->get_right_choice_for($stemid)];
         }
 
diff --git a/question/type/randomsamatch/backup/moodle1/lib.php b/question/type/randomsamatch/backup/moodle1/lib.php
new file mode 100644 (file)
index 0000000..b6fba4d
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Serve question type files
+ *
+ * @package    qtype_randomsamatch
+ * @copyright  2013 Jean-Michel Vedrine
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Random shortanswer matching question type conversion handler.
+ *
+ * @copyright  2013 Jean-Michel Vedrine
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+class moodle1_qtype_randomsamatch_handler extends moodle1_qtype_handler {
+
+    /**
+     * Returns the list of paths within one <QUESTION> that this qtype needs to have included
+     * in the grouped question structure
+     *
+     * @return array of strings
+     */
+    public function get_question_subpaths() {
+        return array(
+            'RANDOMSAMATCH',
+        );
+    }
+
+    /**
+     * Appends the randomsamatch specific information to the question.
+     *
+     * @param array $data grouped question data
+     * @param array $raw grouped raw QUESTION data
+     */
+    public function process_question(array $data, array $raw) {
+
+        // Convert match options.
+        if (isset($data['randomsamatch'])) {
+            $randomsamatch = $data['randomsamatch'][0];
+        } else {
+            $randomsamatch = array('choose' => 4);
+        }
+        $randomsamatch['id'] = $this->converter->get_nextid();
+        $randomsamatch['subcats'] = 1;
+        $randomsamatch['correctfeedback'] = '';
+        $randomsamatch['correctfeedbackformat'] = FORMAT_HTML;
+        $randomsamatch['partiallycorrectfeedback'] = '';
+        $randomsamatch['partiallycorrectfeedbackformat'] = FORMAT_HTML;
+        $randomsamatch['incorrectfeedback'] = '';
+        $randomsamatch['incorrectfeedbackformat'] = FORMAT_HTML;
+        $this->write_xml('randomsamatch', $randomsamatch, array('/randomsamatch/id'));
+    }
+}
index aaa0acf..2ac9191 100644 (file)
@@ -49,14 +49,16 @@ class backup_qtype_randomsamatch_plugin extends backup_qtype_plugin {
 
         // Now create the qtype own structures.
         $randomsamatch = new backup_nested_element('randomsamatch', array('id'), array(
-            'choose'));
+            'choose', 'subcats', 'correctfeedback', 'correctfeedbackformat',
+            'partiallycorrectfeedback', 'partiallycorrectfeedbackformat',
+            'incorrectfeedback', 'incorrectfeedbackformat', 'shownumcorrect'));
 
         // Now the own qtype tree.
         $pluginwrapper->add_child($randomsamatch);
 
         // Set source to populate the data.
-        $randomsamatch->set_source_table('question_randomsamatch',
-                array('question' => backup::VAR_PARENTID));
+        $randomsamatch->set_source_table('qtype_randomsamatch_options',
+                array('questionid' => backup::VAR_PARENTID));
 
         return $plugin;
     }
index 4e3961c..a86e0e3 100644 (file)
@@ -41,7 +41,7 @@ class restore_qtype_randomsamatch_plugin extends restore_qtype_plugin {
 
         $paths = array();
 
-        // Add own qtype stuff
+        // Add own qtype stuff.
         $elename = 'randomsamatch';
         $elepath = $this->get_pathfor('/randomsamatch');
         $paths[] = new restore_path_element($elename, $elepath);
@@ -64,14 +64,34 @@ class restore_qtype_randomsamatch_plugin extends restore_qtype_plugin {
         $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
 
         // If the question has been created by restore, we need to create its
-        // question_randomsamatch too.
+        // qtype_randomsamatch_options too.
         if ($questioncreated) {
+            // Fill in some field that were added in 2.1, and so which may be missing
+            // from backups made in older versions of Moodle.
+            if (!isset($data->subcats)) {
+                $data->subcats = 1;
+            }
+            if (!isset($data->correctfeedback)) {
+                $data->correctfeedback = '';
+                $data->correctfeedbackformat = FORMAT_HTML;
+            }
+            if (!isset($data->partiallycorrectfeedback)) {
+                $data->partiallycorrectfeedback = '';
+                $data->partiallycorrectfeedbackformat = FORMAT_HTML;
+            }
+            if (!isset($data->incorrectfeedback)) {
+                $data->incorrectfeedback = '';
+                $data->incorrectfeedbackformat = FORMAT_HTML;
+            }
+            if (!isset($data->shownumcorrect)) {
+                $data->shownumcorrect = 0;
+            }
             // Adjust some columns.
-            $data->question = $newquestionid;
+            $data->questionid = $newquestionid;
             // Insert record.
-            $newitemid = $DB->insert_record('question_randomsamatch', $data);
+            $newitemid = $DB->insert_record('qtype_randomsamatch_options', $data);
             // Create mapping.
-            $this->set_mapping('question_randomsamatch', $oldid, $newitemid);
+            $this->set_mapping('qtype_randomsamatch_options', $oldid, $newitemid);
         }
     }
 
@@ -82,7 +102,7 @@ class restore_qtype_randomsamatch_plugin extends restore_qtype_plugin {
      * answer is one comma separated list of hypen separated pairs
      * containing question->id and question_answers->id
      */
-    public function recode_state_answer($state) {
+    public function recode_legacy_state_answer($state) {
         $answer = $state->answer;
         $resultarr = array();
         foreach (explode(',', $answer) as $pair) {
@@ -95,4 +115,17 @@ class restore_qtype_randomsamatch_plugin extends restore_qtype_plugin {
         }
         return implode(',', $resultarr);
     }
+
+    /**
+     * Return the contents of this qtype to be processed by the links decoder.
+     */
+    public static function define_decode_contents() {
+
+        $contents = array();
+
+        $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
+        $contents[] = new restore_decode_content('qtype_randomsamatch_options', $fields, 'qtype_randomsamatch_options');
+
+        return $contents;
+    }
 }
index 682de29..b3f917c 100644 (file)
@@ -4,15 +4,23 @@
     xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
 >
   <TABLES>
-    <TABLE NAME="question_randomsamatch" COMMENT="Info about a random short-answer matching question">
+    <TABLE NAME="qtype_randomsamatch_options" COMMENT="Info about a random short-answer matching question">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
-        <FIELD NAME="question" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references question.id."/>
+        <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references question.id."/>
         <FIELD NAME="choose" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="4" SEQUENCE="false" COMMENT="Number of subquestions to randomly generate."/>
+        <FIELD NAME="subcats" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Whether to include or not the subcategories."/>
+        <FIELD NAME="correctfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any correct response."/>
+        <FIELD NAME="correctfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="partiallycorrectfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any partially correct response."/>
+        <FIELD NAME="partiallycorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="incorrectfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any incorrect response."/>
+        <FIELD NAME="incorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="shownumcorrect" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If true, then when the user gets the question partially correct, tell them how many choices they got correct alongside the feedback."/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
-        <KEY NAME="question" TYPE="foreign" FIELDS="question" REFTABLE="question" REFFIELDS="id"/>
+        <KEY NAME="questionid" TYPE="foreign-unique" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
       </KEYS>
     </TABLE>
   </TABLES>
diff --git a/question/type/randomsamatch/db/upgrade.php b/question/type/randomsamatch/db/upgrade.php
new file mode 100644 (file)
index 0000000..fc0e1b3
--- /dev/null
@@ -0,0 +1,190 @@
+<?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/>.
+
+/**
+ * Matching question type upgrade code.
+ *
+ * @package   qtype_randomsamatch
+ * @copyright 2013 Jean-Michel Vedrine
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Upgrade code for the random short answer matching question type.
+ * @param int $oldversion the version we are upgrading from.
+ */
+function xmldb_qtype_randomsamatch_upgrade($oldversion) {
+    global $CFG, $DB;
+
+    $dbman = $DB->get_manager();
+
+    // Moodle v2.2.0 release upgrade line.
+    // Put any upgrade step following this.
+
+    // Moodle v2.3.0 release upgrade line.
+    // Put any upgrade step following this.
+
+    // Moodle v2.4.0 release upgrade line.
+    // Put any upgrade step following this.
+
+    if ($oldversion < 2013110501) {
+
+        // Define table question_randomsamatch to be renamed to qtype_randomsamatch_options.
+        $table = new xmldb_table('question_randomsamatch');
+
+        // Launch rename table for qtype_randomsamatch_options.
+        if ($dbman->table_exists($table)) {
+            $dbman->rename_table($table, 'qtype_randomsamatch_options');
+        }
+
+        // Record that qtype_randomsamatch savepoint was reached.
+        upgrade_plugin_savepoint(true, 2013110501, 'qtype', 'randomsamatch');
+    }
+
+    if ($oldversion < 2013110502) {
+
+        // Define key question (foreign) to be dropped form qtype_randomsamatch_options.
+        $table = new xmldb_table('qtype_randomsamatch_options');
+        $field = new xmldb_field('question', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'id');
+
+        if ($dbman->field_exists($table, $field)) {
+            // Launch drop key question.
+            $key = new xmldb_key('question', XMLDB_KEY_FOREIGN, array('question'), 'question', array('id'));
+            $dbman->drop_key($table, $key);
+
+            // Launch rename field question.
+            $dbman->rename_field($table, $field, 'questionid');
+
+            $key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN_UNIQUE, array('questionid'), 'question', array('id'));
+            // Launch add key questionid.
+            $dbman->add_key($table, $key);
+        }
+
+        // Record that qtype_randomsamatch savepoint was reached.
+        upgrade_plugin_savepoint(true, 2013110502, 'qtype', 'randomsamatch');
+    }
+
+    if ($oldversion < 2013110503) {
+
+        // Add subcats field.
+        $table = new xmldb_table('qtype_randomsamatch_options');
+
+        // Define field subcats to be added to qtype_randomsamatch_options.
+        $field = new xmldb_field('subcats', XMLDB_TYPE_INTEGER, 2, null,
+                XMLDB_NOTNULL, null, '1', 'choose');
+
+        // Conditionally launch add field subcats.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Add combined feedback fields.
+        // Define field correctfeedback to be added to qtype_randomsamatch_options.
+        $field = new xmldb_field('correctfeedback', XMLDB_TYPE_TEXT, 'small', null,
+                null, null, null, 'subcats');
+
+        // Conditionally launch add field correctfeedback.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+
+            // Now fill it with ''.
+            $DB->set_field('qtype_randomsamatch_options', 'correctfeedback', '');
+
+            // Now add the not null constraint.
+            $field = new xmldb_field('correctfeedback', XMLDB_TYPE_TEXT, 'small', null,
+                    XMLDB_NOTNULL, null, null, 'subcats');
+            $dbman->change_field_notnull($table, $field);
+        }
+
+        // Define field correctfeedbackformat to be added to qtype_randomsamatch_options.
+        $field = new xmldb_field('correctfeedbackformat', XMLDB_TYPE_INTEGER, '2', null,
+                XMLDB_NOTNULL, null, '0', 'correctfeedback');
+
+        // Conditionally launch add field correctfeedbackformat.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Define field partiallycorrectfeedback to be added to qtype_randomsamatch_options.
+        $field = new xmldb_field('partiallycorrectfeedback', XMLDB_TYPE_TEXT, 'small', null,
+                null, null, null, 'correctfeedbackformat');
+
+        // Conditionally launch add field partiallycorrectfeedback.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+
+