MDL-48680 mod_scorm: Added new events: {scoreraw,status}_submitted.

Thanks to Ben Johnson and Josh Willcock for their contribution to this
patch.
This commit is contained in:
Matteo Scaramuccia
2016-03-13 09:58:51 +01:00
parent e8952c5951
commit f5bc19c877
7 changed files with 533 additions and 2 deletions
@@ -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.");
}
}
}
@@ -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.
}
}
@@ -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']}).");
}
}
}
+2
View 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';
+31
View 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;
}
+247 -1
View 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());
}
}
}
+1 -1
View 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;