MDL-48680 mod_scorm: Added new events: {scoreraw,status}_submitted.
authorMatteo Scaramuccia <moodle@matteoscaramuccia.com>
Sun, 28 Feb 2016 11:42:11 +0000 (12:42 +0100)
committerMatteo Scaramuccia <moodle@matteoscaramuccia.com>
Sun, 13 Mar 2016 08:58:51 +0000 (09:58 +0100)
Thanks to Ben Johnson and Josh Willcock for their contribution to this
patch.

mod/scorm/classes/event/cmielement_submitted.php [new file with mode: 0644]
mod/scorm/classes/event/scoreraw_submitted.php [new file with mode: 0644]
mod/scorm/classes/event/status_submitted.php [new file with mode: 0644]
mod/scorm/lang/en/scorm.php
mod/scorm/locallib.php
mod/scorm/tests/events_test.php
mod/scorm/version.php

diff --git a/mod/scorm/classes/event/cmielement_submitted.php b/mod/scorm/classes/event/cmielement_submitted.php
new file mode 100644 (file)
index 0000000..31281f3
--- /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_scorm generic CMI element submitted event.
+ *
+ * @package    mod_scorm
+ * @copyright  2016 onwards Matteo Scaramuccia
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_scorm\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_scorm generic CMI element submitted event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event properties.
+ *
+ *      - int attemptid: Attempt id.
+ *      - string cmielement: CMI element.
+ *      - string cmivalue: CMI value.
+ * }
+ *
+ * @package    mod_scorm
+ * @since      Moodle 3.1
+ * @copyright  2016 onwards Matteo Scaramuccia
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cmielement_submitted extends \core\event\base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+        $this->data['objecttable'] = 'scorm_scoes_track';
+    }
+
+    /**
+     * Returns non-localised description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with the id '$this->userid' submitted the element '{$this->other['cmielement']}' " .
+                "with the value of '{$this->other['cmivalue']}' " .
+                "for the attempt with the id '{$this->other['attemptid']}' " .
+                "for a scorm activity with the course module id '$this->contextinstanceid'.";
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/scorm/report/userreport.php',
+                array('id' => $this->contextinstanceid, 'user' => $this->userid, 'attempt' => $this->other['attemptid']));
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (empty($this->other['attemptid'])) {
+            throw new \coding_exception("The 'attemptid' must be set in other.");
+        }
+
+        if (empty($this->other['cmielement'])) {
+            throw new \coding_exception("The 'cmielement' must be set in other.");
+        }
+        // Trust that 'cmielement' represents a valid CMI datamodel element:
+        // just check that the given value starts with 'cmi.'.
+        if (strpos($this->other['cmielement'], 'cmi.', 0) !== 0) {
+            throw new \coding_exception(
+                "A valid 'cmielement' must start with 'cmi.' ({$this->other['cmielement']}).");
+        }
+
+        // Warning: 'cmivalue' could be also "0" e.g. when 'cmielement' represents a score.
+        if (!isset($this->other['cmivalue'])) {
+            throw new \coding_exception("The 'cmivalue' must be set in other.");
+        }
+    }
+}
diff --git a/mod/scorm/classes/event/scoreraw_submitted.php b/mod/scorm/classes/event/scoreraw_submitted.php
new file mode 100644 (file)
index 0000000..a9a6f84
--- /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/>.
+
+/**
+ * The mod_scorm raw score submitted event.
+ *
+ * @package    mod_scorm
+ * @copyright  2016 onwards Matteo Scaramuccia
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_scorm\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_scorm raw score submitted event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event properties.
+ *
+ *      - int attemptid: Attempt id.
+ *      - string cmielement: CMI element representing a raw score.
+ *      - string cmivalue: CMI value.
+ * }
+ *
+ * @package    mod_scorm
+ * @since      Moodle 3.1
+ * @copyright  2016 onwards Matteo Scaramuccia
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class scoreraw_submitted extends cmielement_submitted {
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventscorerawsubmitted', 'mod_scorm');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!strstr($this->other['cmielement'], '.score.raw')) {
+            throw new \coding_exception(
+                "The 'cmielement' must represents a valid CMI raw score ({$this->other['cmielement']}).");
+        }
+
+        // Note: we trust that 'cmivalue' represents a valid SCORM CMI score value.
+    }
+}
diff --git a/mod/scorm/classes/event/status_submitted.php b/mod/scorm/classes/event/status_submitted.php
new file mode 100644 (file)
index 0000000..05bbac5
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The mod_scorm status submitted event.
+ *
+ * @package    mod_scorm
+ * @copyright  2016 onwards Matteo Scaramuccia
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_scorm\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_scorm status submitted event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event properties.
+ *
+ *      - int attemptid: Attempt id.
+ *      - string cmielement: CMI element representing a status.
+ *      - string cmivalue: CMI value.
+ * }
+ *
+ * @package    mod_scorm
+ * @since      Moodle 3.1
+ * @copyright  2016 onwards Matteo Scaramuccia
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class status_submitted extends cmielement_submitted {
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventstatussubmitted', 'mod_scorm');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!in_array($this->other['cmielement'],
+                array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))) {
+            throw new \coding_exception(
+                "The 'cmielement' must represents a valid CMI status element ({$this->other['cmielement']}).");
+        }
+
+        if (!in_array($this->other['cmivalue'],
+                array('passed', 'completed', 'failed', 'incomplete', 'browsed', 'not attempted', 'unknown'))) {
+            throw new \coding_exception(
+                "The 'cmivalue' must represents a valid CMI status value ({$this->other['cmivalue']}).");
+        }
+    }
+}
index 60ad31c..caea8dd 100644 (file)
@@ -122,6 +122,8 @@ $string['eventattemptdeleted'] = 'Attempt deleted';
 $string['eventinteractionsviewed'] = 'Interactions viewed';
 $string['eventreportviewed'] = 'Report viewed';
 $string['eventscolaunched'] = 'Sco launched';
+$string['eventscorerawsubmitted'] = 'Submitted SCORM raw score';
+$string['eventstatussubmitted'] = 'Submitted SCORM status';
 $string['eventtracksviewed'] = 'Tracks viewed';
 $string['eventuserreportviewed'] = 'User report viewed';
 $string['everyday'] = 'Every day';
index b948906..2640ecc 100644 (file)
@@ -533,8 +533,11 @@ function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $valu
         $track->value = $value;
         $track->timemodified = time();
         $id = $DB->insert_record('scorm_scoes_track', $track);
+        $track->id = $id;
     }
 
+    // Trigger updating grades based on a given set of SCORM CMI elements.
+    $scorm = false;
     if (strstr($element, '.score.raw') ||
         (in_array($element, array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))
          && in_array($track->value, array('completed', 'passed')))) {
@@ -543,6 +546,34 @@ function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $valu
         scorm_update_grades($scorm, $userid);
     }
 
+    // Trigger CMI element events.
+    if (strstr($element, '.score.raw') ||
+        (in_array($element, array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))
+        && in_array($track->value, array('completed', 'failed', 'passed')))) {
+        if (!$scorm) {
+            $scorm = $DB->get_record('scorm', array('id' => $scormid));
+        }
+        $cm = get_coursemodule_from_instance('scorm', $scormid);
+        $data = array(
+            'other' => array('attemptid' => $attempt, 'cmielement' => $element, 'cmivalue' => $track->value),
+            'objectid' => $scorm->id,
+            'context' => context_module::instance($cm->id),
+            'relateduserid' => $userid
+        );
+        if (strstr($element, '.score.raw')) {
+            // Create score submitted event.
+            $event = \mod_scorm\event\scoreraw_submitted::create($data);
+        } else {
+            // Create status submitted event.
+            $event = \mod_scorm\event\status_submitted::create($data);
+        }
+        // Trigger submitted event.
+        $event->add_record_snapshot('scorm_scoes_track', $track);
+        $event->add_record_snapshot('course_modules', $cm);
+        $event->add_record_snapshot('scorm', $scorm);
+        $event->trigger();
+    }
+
     return $id;
 }
 
index 48a80a9..a52cb05 100644 (file)
@@ -383,5 +383,251 @@ class mod_scorm_event_testcase extends advanced_testcase {
             $this->assertInstanceOf('coding_exception', $e);
         }
     }
-}
 
+    /**
+     * dataProvider for test_scoreraw_submitted_event().
+     */
+    public function get_scoreraw_submitted_event_provider() {
+        return array(
+            // SCORM 1.2.
+            // - cmi.core.score.raw.
+            'cmi.core.score.raw => 100' => array('cmi.core.score.raw', '100'),
+            'cmi.core.score.raw => 90' => array('cmi.core.score.raw', '90'),
+            'cmi.core.score.raw => 50' => array('cmi.core.score.raw', '50'),
+            'cmi.core.score.raw => 10' => array('cmi.core.score.raw', '10'),
+            // Check an edge case (PHP empty() vs isset()): score value equals to '0'.
+            'cmi.core.score.raw => 0' => array('cmi.core.score.raw', '0'),
+            // SCORM 1.3 AKA 2004.
+            // - cmi.score.raw.
+            'cmi.score.raw => 100' => array('cmi.score.raw', '100'),
+            'cmi.score.raw => 90' => array('cmi.score.raw', '90'),
+            'cmi.score.raw => 50' => array('cmi.score.raw', '50'),
+            'cmi.score.raw => 10' => array('cmi.score.raw', '10'),
+            // Check an edge case (PHP empty() vs isset()): score value equals to '0'.
+            'cmi.score.raw => 0' => array('cmi.score.raw', '0'),
+        );
+    }
+
+    /**
+     * Tests for score submitted event.
+     *
+     * There is no api involved so the best we can do is test data by triggering event manually.
+     *
+     * @dataProvider get_scoreraw_submitted_event_provider
+     *
+     * @param string $cmielement a valid CMI raw score element
+     * @param string $cmivalue a valid CMI raw score value
+     */
+    public function test_scoreraw_submitted_event($cmielement, $cmivalue) {
+        $this->resetAfterTest();
+        $event = \mod_scorm\event\scoreraw_submitted::create(array(
+            'other' => array('attemptid' => '2', 'cmielement' => $cmielement, 'cmivalue' => $cmivalue),
+            'objectid' => $this->eventscorm->id,
+            'context' => context_module::instance($this->eventcm->id),
+            'relateduserid' => $this->eventuser->id
+        ));
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $sink->close();
+        $event = reset($events);
+        $this->assertEquals(2, $event->other['attemptid']);
+        $this->assertEquals($cmielement, $event->other['cmielement']);
+        $this->assertEquals($cmivalue, $event->other['cmivalue']);
+
+        // Check that no legacy log data is provided.
+        $this->assertEventLegacyLogData(null, $event);
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * dataProvider for test_scoreraw_submitted_event_validations().
+     */
+    public function get_scoreraw_submitted_event_validations() {
+        return array(
+            'scoreraw_submitted => missing cmielement' => array(
+                null, '50',
+                "Event validation should not allow \\mod_scorm\\event\\scoreraw_submitted " .
+                    "to be triggered without other['cmielement']",
+                'Coding error detected, it must be fixed by a programmer: ' .
+                    "The 'cmielement' must be set in other."
+            ),
+            'scoreraw_submitted => missing cmivalue' => array(
+                'cmi.core.score.raw', null,
+                "Event validation should not allow \\mod_scorm\\event\\scoreraw_submitted " .
+                    "to be triggered without other['cmivalue']",
+                'Coding error detected, it must be fixed by a programmer: ' .
+                    "The 'cmivalue' must be set in other."
+            ),
+            'scoreraw_submitted => wrong CMI element' => array(
+                'cmi.core.lesson_status', '50',
+                "Event validation should not allow \\mod_scorm\\event\\scoreraw_submitted " .
+                    'to be triggered with a CMI element not representing a raw score',
+                'Coding error detected, it must be fixed by a programmer: ' .
+                    "The 'cmielement' must represents a valid CMI raw score (cmi.core.lesson_status)."
+            ),
+        );
+    }
+
+    /**
+     * Tests for score submitted event validations.
+     *
+     * @dataProvider get_scoreraw_submitted_event_validations
+     *
+     * @param string $cmielement a valid CMI raw score element
+     * @param string $cmivalue a valid CMI raw score value
+     * @param string $failmessage the message used to fail the test in case of missing to violate a validation rule
+     * @param string $excmessage the exception message when violating the validations rules
+     */
+    public function test_scoreraw_submitted_event_validations($cmielement, $cmivalue, $failmessage, $excmessage) {
+        $this->resetAfterTest();
+        try {
+            $data = array(
+                'context' => context_module::instance($this->eventcm->id),
+                'courseid' => $this->eventcourse->id,
+                'other' => array('attemptid' => 2)
+            );
+            if ($cmielement != null) {
+                $data['other']['cmielement'] = $cmielement;
+            }
+            if ($cmivalue != null) {
+                $data['other']['cmivalue'] = $cmivalue;
+            }
+            \mod_scorm\event\scoreraw_submitted::create($data);
+            $this->fail($failmessage);
+        } catch (Exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+            $this->assertEquals($excmessage, $e->getMessage());
+        }
+    }
+
+    /**
+     * dataProvider for test_status_submitted_event().
+     */
+    public function get_status_submitted_event_provider() {
+        return array(
+            // SCORM 1.2.
+            // 1. Status: cmi.core.lesson_status.
+            'cmi.core.lesson_status => passed' => array('cmi.core.lesson_status', 'passed'),
+            'cmi.core.lesson_status => completed' => array('cmi.core.lesson_status', 'completed'),
+            'cmi.core.lesson_status => failed' => array('cmi.core.lesson_status', 'failed'),
+            'cmi.core.lesson_status => incomplete' => array('cmi.core.lesson_status', 'incomplete'),
+            'cmi.core.lesson_status => browsed' => array('cmi.core.lesson_status', 'browsed'),
+            'cmi.core.lesson_status => not attempted' => array('cmi.core.lesson_status', 'not attempted'),
+            // SCORM 1.3 AKA 2004.
+            // 1. Completion status: cmi.completion_status.
+            'cmi.completion_status => completed' => array('cmi.completion_status', 'completed'),
+            'cmi.completion_status => incomplete' => array('cmi.completion_status', 'incomplete'),
+            'cmi.completion_status => not attempted' => array('cmi.completion_status', 'not attempted'),
+            'cmi.completion_status => unknown' => array('cmi.completion_status', 'unknown'),
+            // 2. Success status: cmi.success_status.
+            'cmi.success_status => passed' => array('cmi.success_status', 'passed'),
+            'cmi.success_status => failed' => array('cmi.success_status', 'failed'),
+            'cmi.success_status => unknown' => array('cmi.success_status', 'unknown')
+        );
+    }
+
+    /**
+     * Tests for status submitted event.
+     *
+     * There is no api involved so the best we can do is test data by triggering event manually.
+     *
+     * @dataProvider get_status_submitted_event_provider
+     *
+     * @param string $cmielement a valid CMI status element
+     * @param string $cmivalue a valid CMI status value
+     */
+    public function test_status_submitted_event($cmielement, $cmivalue) {
+        $this->resetAfterTest();
+        $event = \mod_scorm\event\status_submitted::create(array(
+            'other' => array('attemptid' => '2', 'cmielement' => $cmielement, 'cmivalue' => $cmivalue),
+            'objectid' => $this->eventscorm->id,
+            'context' => context_module::instance($this->eventcm->id),
+            'relateduserid' => $this->eventuser->id
+        ));
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $sink->close();
+        $event = reset($events);
+        $this->assertEquals(2, $event->other['attemptid']);
+        $this->assertEquals($cmielement, $event->other['cmielement']);
+        $this->assertEquals($cmivalue, $event->other['cmivalue']);
+
+        // Check that no legacy log data is provided.
+        $this->assertEventLegacyLogData(null, $event);
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * dataProvider for test_status_submitted_event_validations().
+     */
+    public function get_status_submitted_event_validations() {
+        return array(
+            'status_submitted => missing cmielement' => array(
+                null, 'passed',
+                "Event validation should not allow \\mod_scorm\\event\\status_submitted " .
+                    "to be triggered without other['cmielement']",
+                'Coding error detected, it must be fixed by a programmer: ' .
+                    "The 'cmielement' must be set in other."
+            ),
+            'status_submitted => missing cmivalue' => array(
+                'cmi.core.lesson_status', null,
+                "Event validation should not allow \\mod_scorm\\event\\status_submitted " .
+                    "to be triggered without other['cmivalue']",
+                'Coding error detected, it must be fixed by a programmer: ' .
+                    "The 'cmivalue' must be set in other."
+            ),
+            'status_submitted => wrong CMI element' => array(
+                'cmi.core.score.raw', 'passed',
+                "Event validation should not allow \\mod_scorm\\event\\status_submitted " .
+                    'to be triggered with a CMI element not representing a valid CMI status element',
+                'Coding error detected, it must be fixed by a programmer: ' .
+                    "The 'cmielement' must represents a valid CMI status element (cmi.core.score.raw)."
+            ),
+            'status_submitted => wrong CMI value' => array(
+                'cmi.core.lesson_status', 'blahblahblah',
+                "Event validation should not allow \\mod_scorm\\event\\status_submitted " .
+                    'to be triggered with a CMI element not representing a valid CMI status',
+                'Coding error detected, it must be fixed by a programmer: ' .
+                    "The 'cmivalue' must represents a valid CMI status value (blahblahblah)."
+            ),
+        );
+    }
+
+    /**
+     * Tests for status submitted event validations.
+     *
+     * @dataProvider get_status_submitted_event_validations
+     *
+     * @param string $cmielement a valid CMI status element
+     * @param string $cmivalue a valid CMI status value
+     * @param string $failmessage the message used to fail the test in case of missing to violate a validation rule
+     * @param string $excmessage the exception message when violating the validations rules
+     */
+    public function test_status_submitted_event_validations($cmielement, $cmivalue, $failmessage, $excmessage) {
+        $this->resetAfterTest();
+        try {
+            $data = array(
+                'context' => context_module::instance($this->eventcm->id),
+                'courseid' => $this->eventcourse->id,
+                'other' => array('attemptid' => 2)
+            );
+            if ($cmielement != null) {
+                $data['other']['cmielement'] = $cmielement;
+            }
+            if ($cmivalue != null) {
+                $data['other']['cmivalue'] = $cmivalue;
+            }
+            \mod_scorm\event\status_submitted::create($data);
+            $this->fail($failmessage);
+        } catch (Exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+            $this->assertEquals($excmessage, $e->getMessage());
+        }
+    }
+}
index 67d6043..02924bb 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016021001;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2016031300;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2015111000;    // Requires this Moodle version.
 $plugin->component = 'mod_scorm';   // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 300;