diff --git a/mod/scorm/lang/en/scorm.php b/mod/scorm/lang/en/scorm.php index 06c2e1c558c..c6ad22ff722 100644 --- a/mod/scorm/lang/en/scorm.php +++ b/mod/scorm/lang/en/scorm.php @@ -297,6 +297,7 @@ $string['notattempted'] = 'Not attempted'; $string['not_corr_type'] = 'Type mismatch for tag {$a->tag}'; $string['notopenyet'] = 'Sorry, this activity is not available until {$a}'; $string['objectives'] = 'Objectives'; +$string['openafterclose'] = 'You have specified an open date after the close date'; $string['optallstudents'] = 'all users'; $string['optattemptsonly'] = 'users with attempts only'; $string['optnoattemptsonly'] = 'users with no attempts only'; diff --git a/mod/scorm/lib.php b/mod/scorm/lib.php index 2416be80fdf..622b994517c 100644 --- a/mod/scorm/lib.php +++ b/mod/scorm/lib.php @@ -50,6 +50,9 @@ define('SCORM_DISPLAY_ATTEMPTSTATUS_ALL', 1); define('SCORM_DISPLAY_ATTEMPTSTATUS_MY', 2); define('SCORM_DISPLAY_ATTEMPTSTATUS_ENTRY', 3); +define('SCORM_EVENT_TYPE_OPEN', 'open'); +define('SCORM_EVENT_TYPE_CLOSE', 'close'); + /** * Return an array of status options * @@ -1798,3 +1801,117 @@ function mod_scorm_get_completion_active_rule_descriptions($cm) { } return $descriptions; } + +/** + * This function will update the scorm module according to the + * event that has been modified. + * + * It will set the timeopen or timeclose value of the scorm instance + * according to the type of event provided. + * + * @throws \moodle_exception + * @param \calendar_event $event + * @param stdClass $scorm The module instance to get the range from + */ +function mod_scorm_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $scorm) { + global $DB; + + if (empty($event->instance) || $event->modulename != 'scorm') { + return; + } + + if ($event->instance != $scorm->id) { + return; + } + + if (!in_array($event->eventtype, [SCORM_EVENT_TYPE_OPEN, SCORM_EVENT_TYPE_CLOSE])) { + return; + } + + $courseid = $event->courseid; + $modulename = $event->modulename; + $instanceid = $event->instance; + $modified = false; + + $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; + $context = context_module::instance($coursemodule->id); + + // The user does not have the capability to modify this activity. + if (!has_capability('moodle/course:manageactivities', $context)) { + return; + } + + if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) { + // If the event is for the scorm activity opening then we should + // set the start time of the scorm activity to be the new start + // time of the event. + if ($scorm->timeopen != $event->timestart) { + $scorm->timeopen = $event->timestart; + $scorm->timemodified = time(); + $modified = true; + } + } else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) { + // If the event is for the scorm activity closing then we should + // set the end time of the scorm activity to be the new start + // time of the event. + if ($scorm->timeclose != $event->timestart) { + $scorm->timeclose = $event->timestart; + $modified = true; + } + } + + if ($modified) { + $scorm->timemodified = time(); + $DB->update_record('scorm', $scorm); + $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); + $event->trigger(); + } +} + +/** + * This function calculates the minimum and maximum cutoff values for the timestart of + * the given event. + * + * It will return an array with two values, the first being the minimum cutoff value and + * the second being the maximum cutoff value. Either or both values can be null, which + * indicates there is no minimum or maximum, respectively. + * + * If a cutoff is required then the function must return an array containing the cutoff + * timestamp and error string to display to the user if the cutoff value is violated. + * + * A minimum and maximum cutoff return value will look like: + * [ + * [1505704373, 'The date must be after this date'], + * [1506741172, 'The date must be before this date'] + * ] + * + * @param \calendar_event $event The calendar event to get the time range for + * @param \stdClass $instance The module instance to get the range from + * @return array Returns an array with min and max date. + */ +function mod_scorm_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) { + $mindate = null; + $maxdate = null; + + if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) { + // The start time of the open event can't be equal to or after the + // close time of the scorm activity. + if (!empty($instance->timeclose)) { + $maxdate = [ + $instance->timeclose, + get_string('openafterclose', 'scorm') + ]; + } + } else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) { + // The start time of the close event can't be equal to or earlier than the + // open time of the scorm activity. + if (!empty($instance->timeopen)) { + $mindate = [ + $instance->timeopen, + get_string('closebeforeopen', 'scorm') + ]; + } + } + + return [$mindate, $maxdate]; +} \ No newline at end of file diff --git a/mod/scorm/locallib.php b/mod/scorm/locallib.php index 869de923df9..58fbc8aa548 100644 --- a/mod/scorm/locallib.php +++ b/mod/scorm/locallib.php @@ -51,9 +51,6 @@ define('LASTATTEMPT', '3'); define('TOCJSLINK', 1); define('TOCFULLURL', 2); -define('SCORM_EVENT_TYPE_OPEN', 'open'); -define('SCORM_EVENT_TYPE_CLOSE', 'close'); - // Local Library of functions for module scorm. /** diff --git a/mod/scorm/tests/lib_test.php b/mod/scorm/tests/lib_test.php index 9440e24d63d..0e98e3b5a1d 100644 --- a/mod/scorm/tests/lib_test.php +++ b/mod/scorm/tests/lib_test.php @@ -360,9 +360,10 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase { * @param int $courseid * @param int $instanceid The data id. * @param string $eventtype The event type. eg. DATA_EVENT_TYPE_OPEN. + * @param int|null $timestart The start timestamp for the event * @return bool|calendar_event */ - private function create_action_event($courseid, $instanceid, $eventtype) { + private function create_action_event($courseid, $instanceid, $eventtype, $timestart = null) { $event = new stdClass(); $event->name = 'Calendar event'; $event->modulename = 'scorm'; @@ -370,7 +371,13 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase { $event->instance = $instanceid; $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = $eventtype; - $event->timestart = time(); + $event->eventtype = $eventtype; + + if ($timestart) { + $event->timestart = $timestart; + } else { + $event->timestart = time(); + } return calendar_event::create($event); } @@ -433,4 +440,300 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase { $this->assertEquals(mod_scorm_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions); $this->assertEquals(mod_scorm_get_completion_active_rule_descriptions(new stdClass()), []); } + + /** + * An unkown event type should not change the scorm instance. + */ + public function test_mod_scorm_core_calendar_event_timestart_updated_unknown_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $scormgenerator = $generator->get_plugin_generator('mod_scorm'); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $scorm = $scormgenerator->create_instance(['course' => $course->id]); + $scorm->timeopen = $timeopen; + $scorm->timeclose = $timeclose; + $DB->update_record('scorm', $scorm); + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'scorm', + 'instance' => $scorm->id, + 'eventtype' => SCORM_EVENT_TYPE_OPEN . "SOMETHING ELSE", + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + mod_scorm_core_calendar_event_timestart_updated($event, $scorm); + + $scorm = $DB->get_record('scorm', ['id' => $scorm->id]); + $this->assertEquals($timeopen, $scorm->timeopen); + $this->assertEquals($timeclose, $scorm->timeclose); + } + + /** + * A SCORM_EVENT_TYPE_OPEN event should update the timeopen property of + * the scorm activity. + */ + public function test_mod_scorm_core_calendar_event_timestart_updated_open_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $scormgenerator = $generator->get_plugin_generator('mod_scorm'); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $timemodified = 1; + $newtimeopen = $timeopen - DAYSECS; + $scorm = $scormgenerator->create_instance(['course' => $course->id]); + $scorm->timeopen = $timeopen; + $scorm->timeclose = $timeclose; + $scorm->timemodified = $timemodified; + $DB->update_record('scorm', $scorm); + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'scorm', + 'instance' => $scorm->id, + 'eventtype' => SCORM_EVENT_TYPE_OPEN, + 'timestart' => $newtimeopen, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + // Trigger and capture the event when adding a contact. + $sink = $this->redirectEvents(); + + mod_scorm_core_calendar_event_timestart_updated($event, $scorm); + + $triggeredevents = $sink->get_events(); + $moduleupdatedevents = array_filter($triggeredevents, function($e) { + return is_a($e, 'core\event\course_module_updated'); + }); + + $scorm = $DB->get_record('scorm', ['id' => $scorm->id]); + // Ensure the timeopen property matches the event timestart. + $this->assertEquals($newtimeopen, $scorm->timeopen); + // Ensure the timeclose isn't changed. + $this->assertEquals($timeclose, $scorm->timeclose); + // Ensure the timemodified property has been changed. + $this->assertNotEquals($timemodified, $scorm->timemodified); + // Confirm that a module updated event is fired when the module + // is changed. + $this->assertNotEmpty($moduleupdatedevents); + } + + /** + * A SCORM_EVENT_TYPE_CLOSE event should update the timeclose property of + * the scorm activity. + */ + public function test_mod_scorm_core_calendar_event_timestart_updated_close_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $scormgenerator = $generator->get_plugin_generator('mod_scorm'); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $timemodified = 1; + $newtimeclose = $timeclose + DAYSECS; + $scorm = $scormgenerator->create_instance(['course' => $course->id]); + $scorm->timeopen = $timeopen; + $scorm->timeclose = $timeclose; + $scorm->timemodified = $timemodified; + $DB->update_record('scorm', $scorm); + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'scorm', + 'instance' => $scorm->id, + 'eventtype' => SCORM_EVENT_TYPE_CLOSE, + 'timestart' => $newtimeclose, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + // Trigger and capture the event when adding a contact. + $sink = $this->redirectEvents(); + + mod_scorm_core_calendar_event_timestart_updated($event, $scorm); + + $triggeredevents = $sink->get_events(); + $moduleupdatedevents = array_filter($triggeredevents, function($e) { + return is_a($e, 'core\event\course_module_updated'); + }); + + $scorm = $DB->get_record('scorm', ['id' => $scorm->id]); + // Ensure the timeclose property matches the event timestart. + $this->assertEquals($newtimeclose, $scorm->timeclose); + // Ensure the timeopen isn't changed. + $this->assertEquals($timeopen, $scorm->timeopen); + // Ensure the timemodified property has been changed. + $this->assertNotEquals($timemodified, $scorm->timemodified); + // Confirm that a module updated event is fired when the module + // is changed. + $this->assertNotEmpty($moduleupdatedevents); + } + + /** + * An unkown event type should not have any limits + */ + public function test_mod_scorm_core_calendar_get_valid_event_timestart_range_unknown_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $scorm = new \stdClass(); + $scorm->timeopen = $timeopen; + $scorm->timeclose = $timeclose; + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'scorm', + 'instance' => 1, + 'eventtype' => SCORM_EVENT_TYPE_OPEN . "SOMETHING ELSE", + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm); + $this->assertNull($min); + $this->assertNull($max); + } + + /** + * The open event should be limited by the scorm's timeclose property, if it's set. + */ + public function test_mod_scorm_core_calendar_get_valid_event_timestart_range_open_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $scorm = new \stdClass(); + $scorm->timeopen = $timeopen; + $scorm->timeclose = $timeclose; + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'scorm', + 'instance' => 1, + 'eventtype' => SCORM_EVENT_TYPE_OPEN, + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + // The max limit should be bounded by the timeclose value. + list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm); + + $this->assertNull($min); + $this->assertEquals($timeclose, $max[0]); + + // No timeclose value should result in no upper limit. + $scorm->timeclose = 0; + list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm); + + $this->assertNull($min); + $this->assertNull($max); + } + + /** + * The close event should be limited by the scorm's timeopen property, if it's set. + */ + public function test_mod_scorm_core_calendar_get_valid_event_timestart_range_close_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $scorm = new \stdClass(); + $scorm->timeopen = $timeopen; + $scorm->timeclose = $timeclose; + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'scorm', + 'instance' => 1, + 'eventtype' => SCORM_EVENT_TYPE_CLOSE, + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + // The max limit should be bounded by the timeclose value. + list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm); + + $this->assertEquals($timeopen, $min[0]); + $this->assertNull($max); + + // No timeclose value should result in no upper limit. + $scorm->timeopen = 0; + list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm); + + $this->assertNull($min); + $this->assertNull($max); + } }