diff --git a/mod/scorm/aicc.php b/mod/scorm/aicc.php index 9598e3f17a6..38aae294459 100644 --- a/mod/scorm/aicc.php +++ b/mod/scorm/aicc.php @@ -77,7 +77,7 @@ if (!empty($command)) { } else { $attempt = 1; } - + $attemptobject = scorm_get_attempt($aiccuser->id, $scormsession->scormid, $attempt); if ($sco = scorm_get_sco($scoid, SCO_ONLY)) { if (!$scorm = $DB->get_record('scorm', array('id' => $sco->scorm))) { throw new \moodle_exception('cannotcallscript'); @@ -226,7 +226,7 @@ if (!empty($command)) { switch ($element) { case 'cmi.core.lesson_location': $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, - $attempt, $element, $value); + $attemptobject, $element, $value); break; case 'cmi.core.lesson_status': $statuses = array( @@ -263,14 +263,14 @@ if (!empty($command)) { if (empty($value) || isset($exites[$value])) { $subelement = 'cmi.core.exit'; $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, - $attempt, $subelement, $value); + $attemptobject, $subelement, $value); } $value = trim(strtolower($values[0])); $value = $value[0]; if (isset($statuses[$value]) && ($mode == 'normal')) { $value = $statuses[$value]; $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, - $attempt, $element, $value); + $attemptobject, $element, $value); } $lessonstatus = $value; break; @@ -280,12 +280,12 @@ if (!empty($command)) { $subelement = 'cmi.core.score.max'; $value = trim($values[1]); $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, - $attempt, $subelement, $value); + $attemptobject, $subelement, $value); if ((count($values) == 3) && ($values[2] <= $values[0]) && is_numeric($values[2])) { $subelement = 'cmi.core.score.min'; $value = trim($values[2]); $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, - $attempt, $subelement, $value); + $attemptobject, $subelement, $value); } } @@ -293,7 +293,7 @@ if (!empty($command)) { if (is_numeric($values[0])) { $value = trim($values[0]); $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, - $attempt, $element, $value); + $attemptobject, $element, $value); } $score = $value; break; @@ -311,14 +311,14 @@ if (!empty($command)) { next($datarows); } $value = rawurlencode($value); - $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, $attempt, $element, $value); + $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, $attemptobject, $element, $value); } } } if (($mode == 'browse') && ($initlessonstatus == 'not attempted')) { $lessonstatus = 'browsed'; $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, - $attempt, 'cmi.core.lesson_status', 'browsed'); + $attemptobject, 'cmi.core.lesson_status', 'browsed'); } if ($mode == 'normal') { if ($sco = scorm_get_sco($scoid)) { @@ -332,7 +332,7 @@ if (!empty($command)) { } } $id = scorm_insert_track($aiccuser->id, $scorm->id, $sco->id, - $attempt, 'cmi.core.lesson_status', $lessonstatus); + $attemptobject, 'cmi.core.lesson_status', $lessonstatus); } } } @@ -391,26 +391,24 @@ if (!empty($command)) { case 'exitau': if ($status == 'Running') { if (isset($scormsession->sessiontime) && ($scormsession->sessiontime != '')) { - if ($track = $DB->get_record('scorm_scoes_track', array("userid" => $aiccuser->id, - "scormid" => $scorm->id, - "scoid" => $sco->id, - "attempt" => $attempt, - "element" => 'cmi.core.total_time'))) { + $track = scorm_get_sco_value($sco->id, $aiccuser->id, 'cmi.core.total_time', $attempt); + if (!empty($track)) { // Add session_time to total_time. $value = scorm_add_time($track->value, $scormsession->sessiontime); - $track->value = $value; - $track->timemodified = time(); - $DB->update_record('scorm_scoes_track', $track); + $v = new stdClass(); + $v->id = $track->valueid; + $v->value = $value; + $v->timemodified = time(); + $DB->update_record('scorm_scoes_value', $v); } else { $track = new stdClass(); - $track->userid = $aiccuser->id; - $track->scormid = $scorm->id; $track->scoid = $sco->id; - $track->element = 'cmi.core.total_time'; + $track->element = scorm_get_elementid('cmi.core.total_time'); $track->value = $scormsession->sessiontime; - $track->attempt = $attempt; + $atobject = scorm_get_attempt($userid, $scormsession->scormid, $attempt); + $track->attempt = $atobject->id; $track->timemodified = time(); - $id = $DB->insert_record('scorm_scoes_track', $track); + $id = $DB->insert_record('scorm_scoes_value', $track); } scorm_update_grades($scorm, $aiccuser->id); } diff --git a/mod/scorm/backup/moodle2/backup_scorm_stepslib.php b/mod/scorm/backup/moodle2/backup_scorm_stepslib.php index 43582201655..a32c7373b7e 100644 --- a/mod/scorm/backup/moodle2/backup_scorm_stepslib.php +++ b/mod/scorm/backup/moodle2/backup_scorm_stepslib.php @@ -141,7 +141,12 @@ class backup_scorm_activity_structure_step extends backup_activity_structure_ste // All the rest of elements only happen if we are including user info if ($userinfo) { - $scotrack->set_source_table('scorm_scoes_track', array('scoid' => backup::VAR_PARENTID), 'id ASC'); + $sql = 'SELECT v.id, a.userid, a.attempt, e.element, v.value, v.timemodified + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v ON v.attemptid = a.id + JOIN {scorm_element} e ON e.id = v.elementid + WHERE v.scoid = :scoid'; + $scotrack->set_source_sql($sql, ['scoid' => backup::VAR_PARENTID], 'id ASC'); } // Define id annotations diff --git a/mod/scorm/backup/moodle2/restore_scorm_stepslib.php b/mod/scorm/backup/moodle2/restore_scorm_stepslib.php index 1577d12b8f3..77c55cd6d34 100644 --- a/mod/scorm/backup/moodle2/restore_scorm_stepslib.php +++ b/mod/scorm/backup/moodle2/restore_scorm_stepslib.php @@ -173,17 +173,19 @@ class restore_scorm_activity_structure_step extends restore_activity_structure_s } protected function process_scorm_sco_track($data) { - global $DB; - + global $DB, $CFG; + require_once($CFG->dirroot.'/mod/scorm/locallib.php'); $data = (object)$data; - $oldid = $data->id; - $data->scormid = $this->get_new_parentid('scorm'); + $attemptobject = scorm_get_attempt($this->get_mappingid('user', $data->userid), + $this->get_new_parentid('scorm'), + $data->attempt); $data->scoid = $this->get_new_parentid('scorm_sco'); $data->userid = $this->get_mappingid('user', $data->userid); + $data->attemptid = $attemptobject->id; + $data->elementid = scorm_get_elementid($data->element); - $newitemid = $DB->insert_record('scorm_scoes_track', $data); - // No need to save this mapping as far as nothing depend on it - // (child paths, file areas nor links decoder) + $DB->insert_record('scorm_scoes_value', $data); + // No need to save this mapping as far as nothing depend on it. } protected function after_execute() { diff --git a/mod/scorm/classes/cache/elements.php b/mod/scorm/classes/cache/elements.php new file mode 100644 index 00000000000..fb5c7261bb8 --- /dev/null +++ b/mod/scorm/classes/cache/elements.php @@ -0,0 +1,77 @@ +. + +declare(strict_types=1); +namespace mod_scorm\cache; +use cache_definition; + +/** + * Cache data source for the scorm elements. + * + * @package mod_scorm + * @copyright 2023 Catalyst IT Ltd + * @author Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class elements implements \cache_data_source { + + /** @var elements the singleton instance of this class. */ + protected static $instance = null; + + /** + * Returns an instance of the data source class that the cache can use for loading data using the other methods + * specified by this interface. + * + * @param cache_definition $definition + * @return object + */ + public static function get_instance_for_cache(cache_definition $definition): elements { + if (is_null(self::$instance)) { + self::$instance = new elements(); + } + return self::$instance; + } + + /** + * Loads the data for the key provided ready formatted for caching. + * + * @param string|int $key The key to load. + * @return string What ever data should be returned, or null if it can't be loaded. + * @throws \coding_exception + */ + public function load_for_cache($key): ?string { + global $DB; + + $element = $DB->get_field('scorm_element', 'id', ['element' => $key]); + // Return null instead of false, because false will not be cached. + return $element ?: null; + } + + /** + * Loads several keys for the cache. + * + * @param array $keys An array of keys each of which will be string|int. + * @return array An array of matching data items. + */ + public function load_many_for_cache(array $keys): array { + global $DB; + list ($elementsql, $params) = $DB->get_in_or_equal($keys); + $sql = "SELECT element, id + FROM {scorm_element} + WHERE element ".$elementsql; + return $DB->get_records_sql_menu($sql, $params); + } +} diff --git a/mod/scorm/classes/completion/custom_completion.php b/mod/scorm/classes/completion/custom_completion.php index c5b11058531..92e41feb310 100644 --- a/mod/scorm/classes/completion/custom_completion.php +++ b/mod/scorm/classes/completion/custom_completion.php @@ -48,16 +48,18 @@ class custom_completion extends activity_custom_completion { $this->validate_rule($rule); // Base query used when fetching user's tracks data. - $basequery = "SELECT id, scoid, element, value - FROM {scorm_scoes_track} - WHERE scormid = ? - AND userid = ?"; + $basequery = "SELECT v.id, v.scoid, e.element, v.value + FROM {scorm_scoes_value} v + JOIN {scorm_attempt} a ON a.id = v.attemptid + JOIN {scorm_element} e ON e.id = v.elementid + WHERE a.scormid = ? + AND a.userid = ?"; switch ($rule) { case 'completionstatusrequired': $status = COMPLETION_INCOMPLETE; $query = $basequery . - " AND element IN ( + " AND e.element IN ( 'cmi.core.lesson_status', 'cmi.completion_status', 'cmi.success_status' @@ -85,7 +87,7 @@ class custom_completion extends activity_custom_completion { case 'completionscorerequired': $status = COMPLETION_INCOMPLETE; $query = $basequery . - " AND element IN ( + " AND e.element IN ( 'cmi.core.score.raw', 'cmi.score.raw' )"; @@ -110,7 +112,7 @@ class custom_completion extends activity_custom_completion { // Assume complete unless we find a sco that is not complete. $status = COMPLETION_COMPLETE; $query = $basequery . - " AND element IN ( + " AND e.element IN ( 'cmi.core.lesson_status', 'cmi.completion_status', 'cmi.success_status' diff --git a/mod/scorm/classes/event/cmielement_submitted.php b/mod/scorm/classes/event/cmielement_submitted.php index a62827419e9..8b997db99f6 100644 --- a/mod/scorm/classes/event/cmielement_submitted.php +++ b/mod/scorm/classes/event/cmielement_submitted.php @@ -49,7 +49,7 @@ abstract class cmielement_submitted extends \core\event\base { protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; - $this->data['objecttable'] = 'scorm_scoes_track'; + $this->data['objecttable'] = 'scorm_scoes_value'; } /** diff --git a/mod/scorm/classes/external.php b/mod/scorm/classes/external.php index cfc139a5348..c185c0314e6 100644 --- a/mod/scorm/classes/external.php +++ b/mod/scorm/classes/external.php @@ -481,11 +481,12 @@ class mod_scorm_external extends external_api { // Check settings / permissions to view the SCORM. scorm_require_available($scorm); + $attemptobject = scorm_get_attempt($USER->id, $scorm->id, $params['attempt']); foreach ($params['tracks'] as $track) { $element = $track['element']; $value = $track['value']; - $trackid = scorm_insert_track($USER->id, $scorm->id, $sco->id, $params['attempt'], $element, $value, + $trackid = scorm_insert_track($USER->id, $scorm->id, $sco->id, $attemptobject, $element, $value, $scorm->forcecompleted); if ($trackid) { diff --git a/mod/scorm/classes/privacy/provider.php b/mod/scorm/classes/privacy/provider.php index bbb40b578ac..14cb1103c82 100644 --- a/mod/scorm/classes/privacy/provider.php +++ b/mod/scorm/classes/privacy/provider.php @@ -53,13 +53,10 @@ class provider implements * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { - $collection->add_database_table('scorm_scoes_track', [ + $collection->add_database_table('scorm_attempt', [ 'userid' => 'privacy:metadata:userid', 'attempt' => 'privacy:metadata:attempt', - 'element' => 'privacy:metadata:scoes_track:element', - 'value' => 'privacy:metadata:scoes_track:value', - 'timemodified' => 'privacy:metadata:timemodified' - ], 'privacy:metadata:scorm_scoes_track'); + ], 'privacy:metadata:scorm_attempt'); $collection->add_database_table('scorm_aicc_session', [ 'userid' => 'privacy:metadata:userid', @@ -100,7 +97,7 @@ class provider implements $params = ['modlevel' => CONTEXT_MODULE, 'userid' => $userid]; $contextlist = new contextlist(); - $contextlist->add_from_sql(sprintf($sql, 'scorm_scoes_track'), $params); + $contextlist->add_from_sql(sprintf($sql, 'scorm_attempt'), $params); $contextlist->add_from_sql(sprintf($sql, 'scorm_aicc_session'), $params); return $contextlist; @@ -132,7 +129,7 @@ class provider implements $params = ['modlevel' => CONTEXT_MODULE, 'contextid' => $context->id]; - $userlist->add_from_sql('userid', sprintf($sql, 'scorm_scoes_track'), $params); + $userlist->add_from_sql('userid', sprintf($sql, 'scorm_attempt'), $params); $userlist->add_from_sql('userid', sprintf($sql, 'scorm_aicc_session'), $params); } @@ -168,19 +165,21 @@ class provider implements // Get scoes_track data. list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED); - $sql = "SELECT ss.id, - ss.attempt, - ss.element, - ss.value, - ss.timemodified, + $sql = "SELECT v.id, + a.attempt, + e.element, + v.value, + v.timemodified, ctx.id as contextid - FROM {scorm_scoes_track} ss + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v ON a.id = v.attemptid + JOIN {scorm_element} e on e.id = v.elementid JOIN {course_modules} cm - ON cm.instance = ss.scormid + ON cm.instance = a.scormid JOIN {context} ctx ON ctx.instanceid = cm.id WHERE ctx.id $insql - AND ss.userid = :userid"; + AND a.userid = :userid"; $params = array_merge($inparams, ['userid' => $userid]); $alldata = []; @@ -280,8 +279,9 @@ class provider implements WHERE cm.id = :cmid"; $params = ['cmid' => $context->instanceid]; - static::delete_data('scorm_scoes_track', $sql, $params); static::delete_data('scorm_aicc_session', $sql, $params); + $coursemodule = get_coursemodule_from_id('scorm', $context->instanceid); + scorm_delete_tracks($coursemodule->instance); } /** @@ -319,8 +319,13 @@ class provider implements AND ctx.id $insql"; $params = array_merge($inparams, ['userid' => $userid]); - static::delete_data('scorm_scoes_track', $sql, $params); static::delete_data('scorm_aicc_session', $sql, $params); + foreach ($contextlist->get_contexts() as $context) { + if ($context->contextlevel == CONTEXT_MODULE) { + $coursemodule = get_coursemodule_from_id('scorm', $context->instanceid); + scorm_delete_tracks($coursemodule->instance, null, $userid); + } + } } /** @@ -353,8 +358,11 @@ class provider implements AND ss.userid $insql"; $params = array_merge($inparams, ['contextid' => $context->id]); - static::delete_data('scorm_scoes_track', $sql, $params); static::delete_data('scorm_aicc_session', $sql, $params); + $coursemodule = get_coursemodule_from_id('scorm', $context->instanceid); + foreach ($userlist->get_userids() as $userid) { + scorm_delete_tracks($coursemodule->instance, null, $userid); + } } /** diff --git a/mod/scorm/datamodel.php b/mod/scorm/datamodel.php index 7dafc71779d..b2f59b1f805 100644 --- a/mod/scorm/datamodel.php +++ b/mod/scorm/datamodel.php @@ -55,14 +55,20 @@ if (confirm_sesskey() && (!empty($scoid))) { $request = null; if (has_capability('mod/scorm:savetrack', context_module::instance($cm->id))) { // Preload all current tracking data. - $trackdata = $DB->get_records('scorm_scoes_track', array('userid' => $USER->id, 'scormid' => $scorm->id, 'scoid' => $scoid, - 'attempt' => $attempt), '', 'element, id, value, timemodified'); + $sql = "SELECT e.element, v.value, v.timemodified, v.id as valueid + FROM {scorm_scoes_value} v + JOIN {scorm_attempt} a ON a.id = v.attemptid + JOIN {scorm_element} e on e.id = v.elementid + WHERE a.scormid = :scormid AND a.userid = :userid AND v.scoid = :scoid AND a.attempt = :attempt"; + $trackdata = $DB->get_records_sql($sql, ['userid' => $USER->id, 'scormid' => $scorm->id, + 'scoid' => $scoid, 'attempt' => $attempt]); + $attemptobject = scorm_get_attempt($USER->id, $scorm->id, $attempt); foreach (data_submitted() as $element => $value) { $element = str_replace('__', '.', $element); if (substr($element, 0, 3) == 'cmi') { $netelement = preg_replace('/\.N(\d+)\./', "\.\$1\.", $element); - $result = scorm_insert_track($USER->id, $scorm->id, $scoid, $attempt, $element, $value, $scorm->forcecompleted, - $trackdata) && $result; + $result = scorm_insert_track($USER->id, $scorm->id, $scoid, $attemptobject, $element, $value, + $scorm->forcecompleted, $trackdata) && $result; } if (substr($element, 0, 15) == 'adl.nav.request') { // SCORM 2004 Sequencing Request. diff --git a/mod/scorm/datamodels/aicclib.php b/mod/scorm/datamodels/aicclib.php index 35f1450835f..aecd3afabd2 100644 --- a/mod/scorm/datamodels/aicclib.php +++ b/mod/scorm/datamodels/aicclib.php @@ -374,8 +374,8 @@ function scorm_parse_aicc(&$scorm) { } if (!empty($oldscoes)) { foreach ($oldscoes as $oldsco) { - $DB->delete_records('scorm_scoes', array('id' => $oldsco->id)); - $DB->delete_records('scorm_scoes_track', array('scoid' => $oldsco->id)); + scorm_delete_tracks($scorm->id, $oldsco->id); + $DB->delete_records('scorm_scoes', ['id' => $oldsco->id]); } } @@ -461,8 +461,8 @@ function scorm_aicc_generate_simple_sco($scorm) { } // Get rid of old ones. foreach ($scos as $oldsco) { - $DB->delete_records('scorm_scoes', array('id' => $oldsco->id)); - $DB->delete_records('scorm_scoes_track', array('scoid' => $oldsco->id)); + scorm_delete_tracks($scorm->id, $oldsco->id); + $DB->delete_records('scorm_scoes', ['id' => $oldsco->id]); } $sco->identifier = 'A1'; diff --git a/mod/scorm/datamodels/scorm_13lib.php b/mod/scorm/datamodels/scorm_13lib.php index 6eb017f68a0..a68e5bc1ab5 100644 --- a/mod/scorm/datamodels/scorm_13lib.php +++ b/mod/scorm/datamodels/scorm_13lib.php @@ -81,8 +81,7 @@ function scorm_seq_navigation ($scoid, $userid, $request, $attempt=0) { case 'resumeall_': if (empty($seq->currentactivity)) { // TODO: I think it's suspend instead of suspendedactivity. - if ($track = $DB->get_record('scorm_scoes_track', - array('scoid' => $scoid, 'userid' => $userid, 'element' => 'suspendedactivity'))) { + if (scorm_get_sco_value($scoid, $userid, 'suspendedactivity')) { $seq->navigation = true; $seq->sequencing = 'resumeall'; @@ -323,11 +322,8 @@ function scorm_seq_end_attempt($sco, $userid, $seq) { if (!scorm_seq_is('suspended', $sco->id, $userid)) { if (!isset($sco->completionsetbycontent) || ($sco->completionsetbycontent == 0)) { if (!scorm_seq_is('attemptprogressstatus', $sco->id, $userid, $seq->attempt)) { - $incomplete = $DB->get_field('scorm_scoes_track', 'value', - array('scoid' => $sco->id, - 'userid' => $userid, - 'element' => 'cmi.completion_status')); - if ($incomplete != 'incomplete') { + $r = scorm_get_sco_value($sco->id, $userid, 'cmi.completion_status'); + if ($r->value != 'incomplete') { scorm_seq_set('attemptprogressstatus', $sco->id, $userid, $seq->attempt); scorm_seq_set('attemptcompletionstatus', $sco->id, $userid, $seq->attempt); } @@ -366,12 +362,9 @@ function scorm_seq_end_attempt($sco, $userid, $seq) { } function scorm_seq_is($what, $scoid, $userid, $attempt=0) { - global $DB; - // Check if passed activity $what is active. $active = false; - if ($track = $DB->get_record('scorm_scoes_track', - array('scoid' => $scoid, 'userid' => $userid, 'attempt' => $attempt, 'element' => $what))) { + if (scorm_get_sco_value($scoid, $userid, $what, $attempt)) { $active = true; } return $active; @@ -384,8 +377,11 @@ function scorm_seq_set($what, $scoid, $userid, $attempt=0, $value='true') { // Set passed activity to active or not. if ($value == false) { - $DB->delete_records('scorm_scoes_track', array('scoid' => $scoid, 'userid' => $userid, - 'attempt' => $attempt, 'element' => $what)); + $params = ['userid' => $userid, 'scormid' => $sco->scorm, 'attempt' => $attempt, 'element' => $what]; + $sql = "WHERE scoid = :scoid AND attemptid = :attemptid AND elementid = (SELECT id + FROM {scorm_element} + WHERE element = :element)"; + $DB->delete_records_select('scorm_scoes_value', $sql, $params); } else { scorm_insert_track($userid, $sco->scorm, $sco->id, $attempt, $what, $value); } @@ -425,11 +421,9 @@ function scorm_evaluate_condition ($rollupruleconds, $sco, $userid) { } switch ($condition['condition']) { case 'satisfied': - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectivesatisfiedstatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'objectivesatisfiedstatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectiveprogressstatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'objectiveprogressstatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { $res = true; } @@ -437,43 +431,37 @@ function scorm_evaluate_condition ($rollupruleconds, $sco, $userid) { break; case 'objectiveStatusKnown': - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectiveprogressstatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'objectiveprogressstatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { $res = true; } break; case 'notobjectiveStatusKnown': - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectiveprogressstatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'objectiveprogressstatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { $res = true; } break; case 'objectiveMeasureKnown': - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectivemeasurestatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'objectivemeasurestatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { $res = true; } break; case 'notobjectiveMeasureKnown': - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectivemeasurestatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'objectivemeasurestatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { $res = true; } break; case 'completed': - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'attemptcompletionstatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'attemptcompletionstatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'attemptprogressstatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'attemptprogressstatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { $res = true; } @@ -481,30 +469,29 @@ function scorm_evaluate_condition ($rollupruleconds, $sco, $userid) { break; case 'attempted': - $attempt = $DB->get_field('scorm_scoes_track', 'attempt', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'x.start.time')); - if ($checknot && $attempt > 0) { + $r = scorm_get_sco_value($sco->id, $userid, 'x.start.time'); + if ($checknot && $r->attempt > 0) { $res = true; - } else if (!$checknot && $attempt <= 0) { + } else if (!$checknot && $r->attempt <= 0) { $res = true; } break; case 'attemptLimitExceeded': - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'activityprogressstatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'activityprogressstatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, - 'element' => 'limitconditionattemptlimitcontrol')); + $r = scorm_get_sco_value($sco->id, $userid, 'limitconditionattemptlimitcontrol'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { - if ($r = $DB->get_field('scorm_scoes_track', 'attempt', array('scoid' => $sco->id, 'userid' => $userid)) && - $r2 = $DB->get_record('scorm_scoes_track', array('scoid' => $sco->id, 'userid' => $userid, - 'element' => 'limitconditionattemptlimit')) ) { - - if ($checknot && ($r->value >= $r2->value)) { + $sql = "SELECT max(attempt) as attempt + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v on v.attemptid = a.id + WHERE v.scoid = :scoid AND a.userid = :userid"; + $r2 = scorm_get_sco_value($sco->id, $userid, 'limitconditionattemptlimit'); + $attempts = $DB->get_field_sql($sql, ['scoid' => $sco->id, 'userid' => $userid]); + if (!empty($attempts) && !empty($r2)) { + if ($checknot && ($attempts >= $r2->value)) { $res = true; - } else if (!$checknot && ($r->value < $r2->value)) { + } else if (!$checknot && ($attempts < $r2->value)) { $res = true; } } @@ -513,11 +500,9 @@ function scorm_evaluate_condition ($rollupruleconds, $sco, $userid) { break; case 'activityProgressKnown': - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'activityprogressstatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'activityprogressstatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'attemptprogressstatus')); + $r = scorm_get_sco_value($sco->id, $userid, 'attemptprogressstatus'); if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) { $res = true; } @@ -558,48 +543,42 @@ function scorm_limit_cond_check ($activity, $userid) { } if (!isset($activity->limitcontrol) || ($activity->limitcontrol == 1)) { - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'activityattemptcount')); + $r = scorm_get_sco_value($activity->id, $userid, 'activityattemptcount'); if (scorm_seq_is('activityprogressstatus', $activity->id, $userid) && ($r->value >= $activity->limitattempt)) { return true; } } if (!isset($activity->limitabsdurcontrol) || ($activity->limitabsdurcontrol == 1)) { - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'activityabsoluteduration')); + $r = scorm_get_sco_value($activity->id, $userid, 'activityabsoluteduration'); if (scorm_seq_is('activityprogressstatus', $activity->id, $userid) && ($r->value >= $activity->limitabsduration)) { return true; } } if (!isset($activity->limitexpdurcontrol) || ($activity->limitexpdurcontrol == 1)) { - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'activityexperiencedduration')); + $r = scorm_get_sco_value($activity->id, $userid, 'activityexperiencedduration'); if (scorm_seq_is('activityprogressstatus', $activity->id, $userid) && ($r->value >= $activity->limitexpduration)) { return true; } } if (!isset($activity->limitattabsdurcontrol) || ($activity->limitattabsdurcontrol == 1)) { - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'attemptabsoluteduration')); + $r = scorm_get_sco_value($activity->id, $userid, 'attemptabsoluteduration'); if (scorm_seq_is('activityprogressstatus', $activity->id, $userid) && ($r->value >= $activity->limitattabsduration)) { return true; } } if (!isset($activity->limitattexpdurcontrol) || ($activity->limitattexpdurcontrol == 1)) { - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'attemptexperiencedduration')); + $r = scorm_get_sco_value($activity->id, $userid, 'attemptexperiencedduration'); if (scorm_seq_is('activityprogressstatus', $activity->id, $userid) && ($r->value >= $activity->limitattexpduration)) { return true; } } if (!isset($activity->limitbegincontrol) || ($activity->limitbegincontrol == 1)) { - $r = $DB->get_record('scorm_scoes_track', - array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'begintime')); + $r = scorm_get_sco_value($activity->id, $userid, 'begintime'); if (isset($activity->limitbegintime) && time() >= $activity->limitbegintime) { return true; } @@ -711,8 +690,7 @@ function scorm_seq_measure_rollup($sco, $userid, $attempt = 0) { $child = scorm_get_sco($child->id); $countedmeasures = $countedmeasures + ($child->measureweight); if (!scorm_seq_is('objectivemeasurestatus', $sco->id, $userid, $attempt)) { - $normalizedmeasure = $DB->get_record('scorm_scoes_track', - array('scoid' => $child->id, 'userid' => $userid, 'element' => 'objectivenormalizedmeasure')); + $normalizedmeasure = scorm_get_sco_value($child->id, $userid, 'objectivenormalizedmeasure'); $totalmeasure = $totalmeasure + (($normalizedmeasure->value) * ($child->measureweight)); $valid = true; } @@ -795,9 +773,7 @@ function scorm_seq_objective_rollup_measure($sco, $userid, $attempt = 0) { } else { $isactive = false; } - - $normalizedmeasure = $DB->get_record('scorm_scoes_track', - array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectivenormalizedmeasure')); + $normalizedmeasure = scorm_get_sco_value($sco->id, $userid, 'objectivenormalizedmeasure'); $sco = scorm_get_sco ($sco->id); @@ -891,8 +867,6 @@ function scorm_seq_rollup_rule_check ($sco, $userid, $action) { foreach ($rolluprules as $rolluprule) { foreach ($children as $child) { - /*$tracked = $DB->get_records('scorm_scoes_track', array('scoid'=>$child->id, 'userid'=>$userid)); - if ($tracked && $tracked->attemp != 0) {*/ $child = scorm_get_sco ($child); if (!isset($child->tracked) || ($child->tracked == 1)) { if (scorm_seq_check_child ($child, $action, $userid)) { diff --git a/mod/scorm/datamodels/scormlib.php b/mod/scorm/datamodels/scormlib.php index e0a2f6498f3..82e6539e3a5 100644 --- a/mod/scorm/datamodels/scormlib.php +++ b/mod/scorm/datamodels/scormlib.php @@ -709,15 +709,15 @@ function scorm_parse_scorm(&$scorm, $manifest) { } if (!empty($olditems)) { foreach ($olditems as $olditem) { - $DB->delete_records('scorm_scoes', array('id' => $olditem->id)); - $DB->delete_records('scorm_scoes_data', array('scoid' => $olditem->id)); - $DB->delete_records('scorm_scoes_track', array('scoid' => $olditem->id)); - $DB->delete_records('scorm_seq_objective', array('scoid' => $olditem->id)); - $DB->delete_records('scorm_seq_mapinfo', array('scoid' => $olditem->id)); - $DB->delete_records('scorm_seq_ruleconds', array('scoid' => $olditem->id)); - $DB->delete_records('scorm_seq_rulecond', array('scoid' => $olditem->id)); - $DB->delete_records('scorm_seq_rolluprule', array('scoid' => $olditem->id)); - $DB->delete_records('scorm_seq_rolluprulecond', array('scoid' => $olditem->id)); + $DB->delete_records('scorm_scoes', ['id' => $olditem->id]); + $DB->delete_records('scorm_scoes_data', ['scoid' => $olditem->id]); + scorm_delete_tracks($scorm->id, $olditem->id); + $DB->delete_records('scorm_seq_objective', ['scoid' => $olditem->id]); + $DB->delete_records('scorm_seq_mapinfo', ['scoid' => $olditem->id]); + $DB->delete_records('scorm_seq_ruleconds', ['scoid' => $olditem->id]); + $DB->delete_records('scorm_seq_rulecond', ['scoid' => $olditem->id]); + $DB->delete_records('scorm_seq_rolluprule', ['scoid' => $olditem->id]); + $DB->delete_records('scorm_seq_rolluprulecond', ['scoid' => $olditem->id]); } } if (empty($scoes->version)) { diff --git a/mod/scorm/datamodels/sequencinglib.php b/mod/scorm/datamodels/sequencinglib.php index 709efe0cdbe..2311b7af5e4 100644 --- a/mod/scorm/datamodels/sequencinglib.php +++ b/mod/scorm/datamodels/sequencinglib.php @@ -119,7 +119,7 @@ function scorm_seq_check_child ($sco, $action, $userid) { $included = false; $sco = scorm_get_sco($sco->id); - $r = $DB->get_record('scorm_scoes_track', array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'activityattemptcount')); + $r = scorm_get_sco_value($sco->id, $userid, 'activityattemptcount'); if ($action == 'satisfied' || $action == 'notsatisfied') { if (!$sco->rollupobjectivesatisfied) { $included = true; @@ -262,7 +262,7 @@ function scorm_seq_resume_all_sequencing($scoid, $userid, $seq) { $seq->exception = 'SB.2.6-1'; return $seq; } - $track = $DB->get_record('scorm_scoes_track', array('scoid' => $scoid, 'userid' => $userid, 'element' => 'suspendedactivity')); + $track = scorm_get_sco_value($scoid, $userid, 'suspendedactivity'); if (!$track) { $seq->delivery = null; $seq->exception = 'SB.2.6-2'; @@ -752,13 +752,12 @@ function scorm_content_delivery_environment($seq, $userid) { $seq->exception = 'DB.2-1'; return $seq; } - $track = $DB->get_record('scorm_scoes_track', array('scoid' => $act->id, - 'userid' => $userid, - 'element' => 'suspendedactivity')); + $track = scorm_get_sco_value($act->id, $userid, 'suspendedactivity'); if ($track != null) { $seq = scorm_clear_suspended_activity($seq->delivery, $seq, $userid); } + $attemptobject = scorm_get_attempt($userid, $track->scormid, 0); $seq = scorm_terminate_descendent_attempts ($seq->delivery, $userid, $seq); $ancestors = scorm_get_ancestors($seq->delivery); $arrpath = array_reverse($ancestors); @@ -767,24 +766,32 @@ function scorm_content_delivery_environment($seq, $userid) { if (!scorm_seq_is('active', $activity->id, $userid)) { if (!isset($activity->tracked) || ($activity->tracked == 1)) { if (!scorm_seq_is('suspended', $activity->id, $userid)) { - $r = $DB->get_record('scorm_scoes_track', array('scoid' => $activity->id, - 'userid' => $userid, - 'element' => 'activityattemptcount')); - $r->value = ($r->value) + 1; - $DB->update_record('scorm_scoes_track', $r); + $r = scorm_get_sco_value($activity->id, $userid, 'activityattemptcount'); + $value = new stdClass(); + $value->id = $r->valueid; + $value->value = ($r->value) + 1; + $DB->update_record('scorm_scoes_value', $value); if ($r->value == 1) { scorm_seq_set('activityprogressstatus', $activity->id, $userid, 'true'); } - scorm_insert_track($userid, $activity->scorm, $activity->id, 0, 'objectiveprogressstatus', 'false'); - scorm_insert_track($userid, $activity->scorm, $activity->id, 0, 'objectivesatisfiedstatus', 'false'); - scorm_insert_track($userid, $activity->scorm, $activity->id, 0, 'objectivemeasurestatus', 'false'); - scorm_insert_track($userid, $activity->scorm, $activity->id, 0, 'objectivenormalizedmeasure', 0.0); - - scorm_insert_track($userid, $activity->scorm, $activity->id, 0, 'attemptprogressstatus', 'false'); - scorm_insert_track($userid, $activity->scorm, $activity->id, 0, 'attemptcompletionstatus', 'false'); - scorm_insert_track($userid, $activity->scorm, $activity->id, 0, 'attemptabsoluteduration', 0.0); - scorm_insert_track($userid, $activity->scorm, $activity->id, 0, 'attemptexperiencedduration', 0.0); - scorm_insert_track($userid, $activity->scorm, $activity->id, 0, 'attemptcompletionamount', 0.0); + scorm_insert_track($userid, $activity->scorm, $activity->id, $attemptobject, + 'objectiveprogressstatus', 'false'); + scorm_insert_track($userid, $activity->scorm, $activity->id, $attemptobject, + 'objectivesatisfiedstatus', 'false'); + scorm_insert_track($userid, $activity->scorm, $activity->id, $attemptobject, + 'objectivemeasurestatus', 'false'); + scorm_insert_track($userid, $activity->scorm, $activity->id, $attemptobject, + 'objectivenormalizedmeasure', 0.0); + scorm_insert_track($userid, $activity->scorm, $activity->id, $attemptobject, + 'attemptprogressstatus', 'false'); + scorm_insert_track($userid, $activity->scorm, $activity->id, $attemptobject, + 'attemptcompletionstatus', 'false'); + scorm_insert_track($userid, $activity->scorm, $activity->id, $attemptobject, + 'attemptabsoluteduration', 0.0); + scorm_insert_track($userid, $activity->scorm, $activity->id, $attemptobject, + 'attemptexperiencedduration', 0.0); + scorm_insert_track($userid, $activity->scorm, $activity->id, $attemptobject, + 'attemptcompletionamount', 0.0); } } scorm_seq_set('active', $activity->id, $userid, 'true'); @@ -793,26 +800,13 @@ function scorm_content_delivery_environment($seq, $userid) { $seq->delivery = $seq->currentactivity; scorm_seq_set('suspendedactivity', $activity->id, $userid, 'false'); - // ONCE THE DELIVERY BEGINS (How should I check that?). - - if (isset($activity->tracked) || ($activity->tracked == 0)) { - // How should I track the info and what should I do to not record the information for the activity during delivery? - $atabsdur = $DB->get_record('scorm_scoes_track', array('scoid' => $activity->id, - 'userid' => $userid, - 'element' => 'attemptabsoluteduration')); - $atexpdur = $DB->get_record('scorm_scoes_track', array('scoid' => $activity->id, - 'userid' => $userid, - 'element' => 'attemptexperiencedduration')); - } return $seq; } function scorm_clear_suspended_activity($act, $seq, $userid) { global $DB; $currentact = $seq->currentactivity; - $track = $DB->get_record('scorm_scoes_track', array('scoid' => $currentact->id, - 'userid' => $userid, - 'element' => 'suspendedactivity')); + $track = scorm_get_sco_value($currentact->id, $userid, 'suspendedactivity'); if ($track != null) { $ancestors = scorm_get_ancestors($act); $commonpos = scorm_find_common_ancestor($ancestors, $currentact); @@ -849,9 +843,7 @@ function scorm_select_children_process($scoid, $userid) { $sco = scorm_get_sco($scoid); if (!scorm_is_leaf($sco)) { if (!scorm_seq_is('suspended', $scoid, $userid) && !scorm_seq_is('active', $scoid, $userid)) { - $r = $DB->get_record('scorm_scoes_track', array('scoid' => $scoid, - 'userid' => $userid, - 'element' => 'selectiontiming')); + $r = scorm_get_sco_value($scoid, $userid, 'selectiontiming'); switch ($r->value) { case 'oneachnewattempt': @@ -862,9 +854,7 @@ function scorm_select_children_process($scoid, $userid) { if (!scorm_seq_is('activityprogressstatus', $scoid, $userid)) { if (scorm_seq_is('selectioncountsstatus', $scoid, $userid)) { $childlist = ''; - $res = $DB->get_record('scorm_scoes_track', array('scoid' => $scoid, - 'userid' => $userid, - 'element' => 'selectioncount')); + $res = scorm_get_sco_value($scoid, $userid, 'selectioncount'); $i = ($res->value) - 1; $children = scorm_get_children($sco); @@ -892,9 +882,7 @@ function scorm_randomize_children_process($scoid, $userid) { $sco = scorm_get_sco($scoid); if (!scorm_is_leaf($sco)) { if (!scorm_seq_is('suspended', $scoid, $userid) && !scorm_seq_is('active', $scoid, $userid)) { - $r = $DB->get_record('scorm_scoes_track', array('scoid' => $scoid, - 'userid' => $userid, - 'element' => 'randomizationtiming')); + $r = scorm_get_sco_value($scoid, $userid, 'randomizationtiming'); switch ($r->value) { case 'never': diff --git a/mod/scorm/db/caches.php b/mod/scorm/db/caches.php new file mode 100644 index 00000000000..197f697f2f1 --- /dev/null +++ b/mod/scorm/db/caches.php @@ -0,0 +1,33 @@ +. + +/** + * SCORM cache definition. + * + * @package mod_scorm + * @copyright 2023 Catalyst IT Ltd + * @author Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$definitions = [ + 'elements' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'datasource' => '\mod_scorm\cache\elements', + ] +]; diff --git a/mod/scorm/db/install.xml b/mod/scorm/db/install.xml index 92a369c1560..6c0f97918f4 100644 --- a/mod/scorm/db/install.xml +++ b/mod/scorm/db/install.xml @@ -1,5 +1,5 @@ - @@ -85,27 +85,6 @@ - - - - - - - - - - - - - - - - - - - - -
@@ -222,5 +201,46 @@
+ + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
diff --git a/mod/scorm/db/upgrade.php b/mod/scorm/db/upgrade.php index 4b8ee8f3db2..24cab147a73 100644 --- a/mod/scorm/db/upgrade.php +++ b/mod/scorm/db/upgrade.php @@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die(); * @return bool */ function xmldb_scorm_upgrade($oldversion) { - global $DB; + global $DB, $OUTPUT; $dbman = $DB->get_manager(); @@ -55,5 +55,118 @@ function xmldb_scorm_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. + // New table structure for scorm_scoes_track. + if ($oldversion < 2023042401) { + // Define table scorm_attempt to be created. + $table = new xmldb_table('scorm_attempt'); + + // Adding fields to table scorm_attempt. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('scormid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('attempt', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '1'); + + // Adding keys to table scorm_attempt. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('user', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']); + $table->add_key('scorm', XMLDB_KEY_FOREIGN, ['scormid'], 'scorm', ['id']); + + // Conditionally launch create table for scorm_attempt. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define table scorm_element to be created. + $table = new xmldb_table('scorm_element'); + + // Adding fields to table scorm_element. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('element', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table scorm_element. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Adding indexes to table scorm_element. + $table->add_index('element', XMLDB_INDEX_UNIQUE, ['element']); + + // Conditionally launch create table for scorm_element. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define table scorm_scoes_value to be created. + $table = new xmldb_table('scorm_scoes_value'); + + // Adding fields to table scorm_scoes_value. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('scoid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('attemptid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('elementid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('value', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table scorm_scoes_value. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('scoe', XMLDB_KEY_FOREIGN, ['scoid'], 'scorm_scoes', ['id']); + $table->add_key('attempt', XMLDB_KEY_FOREIGN, ['attemptid'], 'scorm_attempt', ['id']); + $table->add_key('element', XMLDB_KEY_FOREIGN, ['elementid'], 'scorm_element', ['id']); + + // Conditionally launch create table for scorm_scoes_value. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + upgrade_mod_savepoint(true, 2023042401, 'scorm'); + } + + if ($oldversion < 2023042402) { + $trans = $DB->start_delegated_transaction(); + + // First grab all elements and store those. + $sql = "INSERT INTO {scorm_element} (element) + SELECT DISTINCT element FROM {scorm_scoes_track}"; + $DB->execute($sql); + + // Now store all data in the scorm_attempt table. + $sql = "INSERT INTO {scorm_attempt} (userid, scormid, attempt) + SELECT DISTINCT userid, scormid, attempt FROM {scorm_scoes_track}"; + $DB->execute($sql); + + $trans->allow_commit(); + // Scorm savepoint reached. + upgrade_mod_savepoint(true, 2023042402, 'scorm'); + + } + if ($oldversion < 2023042403) { + // Now store all translated data in the scorm_scoes_value table. + $total = $DB->count_records('scorm_scoes_track'); + if ($total > 500000) { + // This site has a large number of user track records, lets warn that this next part may take some time. + $notification = new \core\output\notification(get_string('largetrackupgrade', 'scorm', format_float($total, 0)), + \core\output\notification::NOTIFY_WARNING); + $notification->set_show_closebutton(false); + echo $OUTPUT->render($notification); + } + + // We don't need a progress bar - just run the fastest option possible. + $sql = "INSERT INTO {scorm_scoes_value} (attemptid, scoid, elementid, value, timemodified) + SELECT a.id as attemptid, t.scoid as scoid, e.id as elementid, t.value as value, t.timemodified + FROM {scorm_scoes_track} t + JOIN {scorm_element} e ON e.element = t.element + JOIN {scorm_attempt} a ON (t.userid = a.userid AND t.scormid = a.scormid AND a.attempt = t.attempt)"; + $DB->execute($sql); + + // Drop old table scorm_scoes_track. + $table = new xmldb_table('scorm_scoes_track'); + + // Conditionally launch drop table for scorm_scoes_track. + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + + // Scorm savepoint reached. + upgrade_mod_savepoint(true, 2023042403, 'scorm'); + } + return true; } diff --git a/mod/scorm/deprecatedlib.php b/mod/scorm/deprecatedlib.php index f29515abb10..114a4acb634 100644 --- a/mod/scorm/deprecatedlib.php +++ b/mod/scorm/deprecatedlib.php @@ -54,16 +54,18 @@ function scorm_get_completion_state($course, $cm, $userid, $type) { $tracks = $DB->get_records_sql( " SELECT - id, - scoid, - element, - value + v.id, + v.scoid, + e.element, + v.value FROM - {scorm_scoes_track} + {scorm_scoes_value} v + JOIN {scorm_attempt} a on a.id = v.attemptid + JOIN {scorm_element} e on e.id = v.elementid WHERE - scormid = ? - AND userid = ? - AND element IN + a.scormid = ? + AND a.userid = ? + AND e.element IN ( 'cmi.core.lesson_status', 'cmi.completion_status', diff --git a/mod/scorm/lang/en/scorm.php b/mod/scorm/lang/en/scorm.php index 4505114217f..57cd8556d27 100644 --- a/mod/scorm/lang/en/scorm.php +++ b/mod/scorm/lang/en/scorm.php @@ -71,6 +71,7 @@ $string['browse'] = 'Preview'; $string['browsed'] = 'Browsed'; $string['browsemode'] = 'Preview mode'; $string['browserepository'] = 'Browse repository'; +$string['cachedef_elements'] = 'Element cache'; $string['calculatedweight'] = 'Calculated weight'; $string['calendarend'] = '{$a} closes'; $string['calendarstart'] = '{$a} opens'; @@ -213,6 +214,7 @@ $string['indicator:socialbreadthdef_help'] = 'The participant has reached this p $string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth'; $string['interactions'] = 'Interactions'; +$string['largetrackupgrade'] = 'This next upgrade step may take some time to complete, your site has {$a} SCORM track records that need to be migrated to the new table structure, please be patient as a progress bar is not able to be displayed.'; $string['masteryoverride'] = 'Mastery score overrides status'; $string['masteryoverride_help'] = 'If enabled and a mastery score is provided, when LMSFinish is called and a raw score has been set, status will be recalculated using the raw score and mastery score and any status provided by the SCORM (including "incomplete") will be overridden.'; $string['masteryoverridedesc'] = 'This preference sets the default for the mastery score override setting'; @@ -354,7 +356,7 @@ $string['privacy:metadata:attempt'] = 'The attempt number'; $string['privacy:metadata:scoes_track:element'] = 'The name of the element to be tracked'; $string['privacy:metadata:scoes_track:value'] = 'The value of the given element'; $string['privacy:metadata:scorm_aicc_session'] = 'The session information of the AICC HACP'; -$string['privacy:metadata:scorm_scoes_track'] = 'The tracked data of the SCOes belonging to the activity'; +$string['privacy:metadata:scorm_attempt'] = 'The SCORM attempts made by a user'; $string['privacy:metadata:timemodified'] = 'The time when the tracked element was last modified'; $string['privacy:metadata:userid'] = 'The ID of the user who accessed the SCORM activity'; $string['protectpackagedownloads'] = 'Protect package downloads'; diff --git a/mod/scorm/lib.php b/mod/scorm/lib.php index ddd4864753a..6c18c96c440 100644 --- a/mod/scorm/lib.php +++ b/mod/scorm/lib.php @@ -293,9 +293,7 @@ function scorm_delete_instance($id) { $result = true; // Delete any dependent records. - if (! $DB->delete_records('scorm_scoes_track', array('scormid' => $scorm->id))) { - $result = false; - } + scorm_delete_tracks($scorm->id); if ($scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id))) { foreach ($scoes as $sco) { if (! $DB->delete_records('scorm_scoes_data', array('scoid' => $sco->id))) { @@ -594,31 +592,33 @@ function scorm_get_user_grades($scorm, $userid=0) { $grades = array(); if (empty($userid)) { - $scousers = $DB->get_records_select('scorm_scoes_track', "scormid=? GROUP BY userid", - array($scorm->id), "", "userid,null"); - if ($scousers) { - foreach ($scousers as $scouser) { - $grades[$scouser->userid] = new stdClass(); - $grades[$scouser->userid]->id = $scouser->userid; - $grades[$scouser->userid]->userid = $scouser->userid; - $grades[$scouser->userid]->rawgrade = scorm_grade_user($scorm, $scouser->userid); - } - } else { - return false; - } + $sql = "SELECT DISTINCT userid + FROM {scorm_attempt} + WHERE scormid = ?"; + $scousers = $DB->get_recordset_sql($sql, [$scorm->id]); + foreach ($scousers as $scouser) { + $grades[$scouser->userid] = new stdClass(); + $grades[$scouser->userid]->id = $scouser->userid; + $grades[$scouser->userid]->userid = $scouser->userid; + $grades[$scouser->userid]->rawgrade = scorm_grade_user($scorm, $scouser->userid); + } + $scousers->close(); } else { - $preattempt = $DB->get_records_select('scorm_scoes_track', "scormid=? AND userid=? GROUP BY userid", - array($scorm->id, $userid), "", "userid,null"); + $preattempt = $DB->record_exists('scorm_attempt', ['scormid' => $scorm->id, 'userid' => $userid]); if (!$preattempt) { return false; // No attempt yet. } $grades[$userid] = new stdClass(); - $grades[$userid]->id = $userid; - $grades[$userid]->userid = $userid; + $grades[$userid]->id = $userid; + $grades[$userid]->userid = $userid; $grades[$userid]->rawgrade = scorm_grade_user($scorm, $userid); } + if (empty($grades)) { + return false; + } + return $grades; } @@ -819,30 +819,31 @@ function scorm_reset_gradebook($courseid, $type='') { * @return array status array */ function scorm_reset_userdata($data) { - global $CFG, $DB; + global $DB; $componentstr = get_string('modulenameplural', 'scorm'); - $status = array(); + $status = []; if (!empty($data->reset_scorm)) { - $scormssql = "SELECT s.id - FROM {scorm} s - WHERE s.course=?"; - $DB->delete_records_select('scorm_scoes_track', "scormid IN ($scormssql)", array($data->courseid)); + $scorms = $DB->get_recordset('scorm', ['course' => $data->courseid]); + foreach ($scorms as $scorm) { + scorm_delete_tracks($scorm->id); + } + $scorms->close(); // Remove all grades from gradebook. if (empty($data->reset_gradebook_grades)) { scorm_reset_gradebook($data->courseid); } - $status[] = array('component' => $componentstr, 'item' => get_string('deleteallattempts', 'scorm'), 'error' => false); + $status[] = ['component' => $componentstr, 'item' => get_string('deleteallattempts', 'scorm'), 'error' => false]; } // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. shift_course_mod_dates('scorm', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid); - $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false); + $status[] = ['component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false]; return $status; } @@ -1296,7 +1297,7 @@ function scorm_check_mode($scorm, &$newattempt, &$attempt, $userid, &$mode) { $mode = 'normal'; if ($attempt == 1) { // Check if the user has any existing data or if this is really the first attempt. - $exists = $DB->record_exists('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id)); + $exists = $DB->record_exists('scorm_attempt', ['userid' => $userid, 'scormid' => $scorm->id]); if (!$exists) { // No records yet - Attempt should == 1. return; @@ -1326,12 +1327,17 @@ function scorm_check_mode($scorm, &$newattempt, &$attempt, $userid, &$mode) { } $completionelement = $completionelements[$scormversion]; - $sql = "SELECT sc.id, t.value + $sql = "SELECT sc.id, sub.value FROM {scorm_scoes} sc - LEFT JOIN {scorm_scoes_track} t ON sc.scorm = t.scormid AND sc.id = t.scoid - AND t.element = ? AND t.userid = ? AND t.attempt = ? - WHERE sc.scormtype = 'sco' AND sc.scorm = ?"; - $tracks = $DB->get_recordset_sql($sql, array($completionelement, $userid, $attempt, $scorm->id)); + LEFT JOIN (SELECT v.scoid, v.value + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v ON a.id = v.attemptid + JOIN {scorm_element} e on e.id = v.elementid AND e.element = :element + WHERE a.userid = :userid AND a.attempt = :attempt AND a.scormid = :scormid) sub ON sub.scoid = sc.id + WHERE sc.scormtype = 'sco' AND sc.scorm = :scormid2"; + $tracks = $DB->get_recordset_sql($sql, ['userid' => $userid, 'attempt' => $attempt, + 'element' => $completionelement, 'scormid' => $scorm->id, + 'scormid2' => $scorm->id]); foreach ($tracks as $track) { if (($track->value == 'completed') || ($track->value == 'passed') || ($track->value == 'failed')) { @@ -1410,9 +1416,12 @@ function scorm_check_updates_since(cm_info $cm, $from, $filter = array()) { $updates = course_check_module_updates_since($cm, $from, array('package'), $filter); $updates->tracks = (object) array('updated' => false); - $select = 'scormid = ? AND userid = ? AND timemodified > ?'; - $params = array($scorm->id, $USER->id, $from); - $tracks = $DB->get_records_select('scorm_scoes_track', $select, $params, '', 'id'); + $sql = "SELECT v.id + FROM {scorm_scoes_value} v + JOIN {scorm_attempt} a ON a.id = v.attemptid + WHERE a.scormid = :scormid AND v.timemodified > :timemodified"; + $params = ['scormid' => $scorm->id, 'timemodified' => $from, 'userid' => $USER->id]; + $tracks = $DB->get_records_sql($sql ." AND a.userid = :userid", $params); if (!empty($tracks)) { $updates->tracks->updated = true; $updates->tracks->itemids = array_keys($tracks); @@ -1420,21 +1429,21 @@ function scorm_check_updates_since(cm_info $cm, $from, $filter = array()) { // Now, teachers should see other students updates. if (has_capability('mod/scorm:viewreport', $cm->context)) { - $select = 'scormid = ? AND timemodified > ?'; - $params = array($scorm->id, $from); + $params = ['scormid' => $scorm->id, 'timemodified' => $from]; if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) { $groupusers = array_keys(groups_get_activity_shared_group_members($cm)); if (empty($groupusers)) { return $updates; } - list($insql, $inparams) = $DB->get_in_or_equal($groupusers); - $select .= ' AND userid ' . $insql; + list($insql, $inparams) = $DB->get_in_or_equal($groupusers, SQL_PARAMS_NAMED); + $sql .= ' AND userid ' . $insql; $params = array_merge($params, $inparams); } $updates->usertracks = (object) array('updated' => false); - $tracks = $DB->get_records_select('scorm_scoes_track', $select, $params, '', 'id'); + + $tracks = $DB->get_records_sql($sql, $params); if (!empty($tracks)) { $updates->usertracks->updated = true; $updates->usertracks->itemids = array_keys($tracks); diff --git a/mod/scorm/locallib.php b/mod/scorm/locallib.php index 5ebdde57dad..af82cf20aa6 100644 --- a/mod/scorm/locallib.php +++ b/mod/scorm/locallib.php @@ -436,29 +436,46 @@ function scorm_get_scoes($id, $organisation=false) { } } -function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $value, $forcecompleted=false, $trackdata = null) { +/** + * Insert SCORM track into db. + * + * @param int $userid The userid + * @param int $scormid The id from scorm table + * @param int $scoid The scoid + * @param int|stdClass $attemptornumber - number of attempt or attempt record from scorm_attempt table. + * @param string $element The element being saved + * @param string $value The value of the element + * @param boolean $forcecompleted Force this sco as completed + * @param stdclass $trackdata - existing tracking data + * @return int - the id of the record being saved. + */ +function scorm_insert_track($userid, $scormid, $scoid, $attemptornumber, $element, $value, $forcecompleted=false, $trackdata = null) { global $DB, $CFG; + if (is_object($attemptornumber)) { + $attempt = $attemptornumber; + } else { + $attempt = scorm_get_attempt($userid, $scormid, $attemptornumber); + } + $id = null; if ($forcecompleted) { // TODO - this could be broadened to encompass SCORM 2004 in future. if (($element == 'cmi.core.lesson_status') && ($value == 'incomplete')) { - if ($track = $DB->get_record_select('scorm_scoes_track', - 'userid=? AND scormid=? AND scoid=? AND attempt=? '. - 'AND element=\'cmi.core.score.raw\'', - array($userid, $scormid, $scoid, $attempt))) { + $track = scorm_get_sco_value($scoid, $userid, 'cmi.core.score.raw', $attempt->attempt); + if (!empty($track)) { $value = 'completed'; } } if ($element == 'cmi.core.score.raw') { - if ($tracktest = $DB->get_record_select('scorm_scoes_track', - 'userid=? AND scormid=? AND scoid=? AND attempt=? '. - 'AND element=\'cmi.core.lesson_status\'', - array($userid, $scormid, $scoid, $attempt))) { + $tracktest = scorm_get_sco_value($scoid, $userid, 'cmi.core.lesson_status', $attempt->attempt); + if (!empty($tracktest)) { if ($tracktest->value == "incomplete") { - $tracktest->value = "completed"; - $DB->update_record('scorm_scoes_track', $tracktest); + $v = new stdClass(); + $v->id = $track->valueid; + $v->value = "completed"; + $DB->update_record('scorm_scoes_value', $v); } } } @@ -469,47 +486,40 @@ function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $valu if ($value == 'passed') { $objectivesatisfiedstatus = true; } - - if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid, - 'scormid' => $scormid, - 'scoid' => $scoid, - 'attempt' => $attempt, - 'element' => 'objectiveprogressstatus'))) { - $track->value = $objectiveprogressstatus; - $track->timemodified = time(); - $DB->update_record('scorm_scoes_track', $track); - $id = $track->id; + $track = scorm_get_sco_value($scoid, $userid, 'objectiveprogressstatus', $attempt->attempt); + if (!empty($track)) { + $v = new stdClass(); + $v->id = $track->valueid; + $v->value = $objectiveprogressstatus; + $v->timemodified = time(); + $DB->update_record('scorm_scoes_value', $v); + $id = $track->valueid; } else { $track = new stdClass(); - $track->userid = $userid; - $track->scormid = $scormid; $track->scoid = $scoid; - $track->attempt = $attempt; - $track->element = 'objectiveprogressstatus'; + $track->attemptid = $attempt->id; + $track->elementid = scorm_get_elementid('objectiveprogressstatus'); $track->value = $objectiveprogressstatus; $track->timemodified = time(); - $id = $DB->insert_record('scorm_scoes_track', $track); + $id = $DB->insert_record('scorm_scoes_value', $track); } if ($objectivesatisfiedstatus) { - if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid, - 'scormid' => $scormid, - 'scoid' => $scoid, - 'attempt' => $attempt, - 'element' => 'objectivesatisfiedstatus'))) { - $track->value = $objectivesatisfiedstatus; - $track->timemodified = time(); - $DB->update_record('scorm_scoes_track', $track); - $id = $track->id; + $track = scorm_get_sco_value($scoid, $userid, 'objectivesatisfiedstatus', $attempt->attempt); + if (!empty($track)) { + $v = new stdClass(); + $v->id = $track->valueid; + $v->value = $objectivesatisfiedstatus; + $v->timemodified = time(); + $DB->update_record('scorm_scoes_value', $v); + $id = $track->valueid; } else { $track = new stdClass(); - $track->userid = $userid; - $track->scormid = $scormid; $track->scoid = $scoid; - $track->attempt = $attempt; - $track->element = 'objectivesatisfiedstatus'; + $track->attemptid = $attempt->id; + $track->elementid = scorm_get_elementid('objectivesatisfiedstatus'); $track->value = $objectivesatisfiedstatus; $track->timemodified = time(); - $id = $DB->insert_record('scorm_scoes_track', $track); + $id = $DB->insert_record('scorm_scoes_value', $track); } } } @@ -523,31 +533,27 @@ function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $valu $track = $trackdata[$element]; } } else { - $track = $DB->get_record('scorm_scoes_track', array('userid' => $userid, - 'scormid' => $scormid, - 'scoid' => $scoid, - 'attempt' => $attempt, - 'element' => $element)); + $track = scorm_get_sco_value($scoid, $userid, $element, $attempt->attempt); } if ($track) { if ($element != 'x.start.time' ) { // Don't update x.start.time - keep the original value. if ($track->value != $value) { - $track->value = $value; - $track->timemodified = time(); - $DB->update_record('scorm_scoes_track', $track); + $v = new stdClass(); + $v->id = $track->valueid; + $v->value = $value; + $v->timemodified = time(); + $DB->update_record('scorm_scoes_value', $v); } - $id = $track->id; + $id = $track->valueid; } } else { $track = new stdClass(); - $track->userid = $userid; - $track->scormid = $scormid; $track->scoid = $scoid; - $track->attempt = $attempt; - $track->element = $element; + $track->attemptid = $attempt->id; + $track->elementid = scorm_get_elementid($element); $track->value = $value; $track->timemodified = time(); - $id = $DB->insert_record('scorm_scoes_track', $track); + $id = $DB->insert_record('scorm_scoes_value', $track); $track->id = $id; } @@ -570,7 +576,7 @@ function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $valu } $cm = get_coursemodule_from_instance('scorm', $scormid); $data = array( - 'other' => array('attemptid' => $attempt, 'cmielement' => $element, 'cmivalue' => $track->value), + 'other' => array('attemptid' => $attempt->id, 'cmielement' => $element, 'cmivalue' => $track->value), 'objectid' => $scorm->id, 'context' => context_module::instance($cm->id), 'relateduserid' => $userid @@ -584,13 +590,12 @@ function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $valu } // Fix the missing track keys when the SCORM track record already exists, see $trackdata in datamodel.php. // There, for performances reasons, columns are limited to: element, id, value, timemodified. - // Missing fields are: userid, scormid, scoid, attempt. - $track->userid = $userid; - $track->scormid = $scormid; + // Missing fields are: scoid, attempt. $track->scoid = $scoid; - $track->attempt = $attempt; + $track->attempt = $attempt->id; + $track->id = $id; // Trigger submitted event. - $event->add_record_snapshot('scorm_scoes_track', $track); + $event->add_record_snapshot('scorm_scoes_value', $track); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('scorm', $scorm); $event->trigger(); @@ -608,7 +613,7 @@ function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $valu */ function scorm_has_tracks($scormid, $userid) { global $DB; - return $DB->record_exists('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scormid)); + return $DB->record_exists('scorm_attempt', ['userid' => $userid, 'scormid' => $scormid]); } function scorm_get_tracks($scoid, $userid, $attempt='') { @@ -616,14 +621,19 @@ function scorm_get_tracks($scoid, $userid, $attempt='') { global $DB; if (empty($attempt)) { - if ($scormid = $DB->get_field('scorm_scoes', 'scorm', array('id' => $scoid))) { + if ($scormid = $DB->get_field('scorm_scoes', 'scorm', ['id' => $scoid])) { $attempt = scorm_get_last_attempt($scormid, $userid); } else { $attempt = 1; } } - if ($tracks = $DB->get_records('scorm_scoes_track', array('userid' => $userid, 'scoid' => $scoid, - 'attempt' => $attempt), 'element ASC')) { + $sql = "SELECT v.id, a.userid, a.scormid, v.scoid, a.attempt, v.value, v.timemodified, e.element + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v ON v.attemptid = a.id + JOIN {scorm_element} e ON e.id = v.elementid + WHERE a.userid = ? AND v.scoid = ? AND a.attempt = ? + ORDER BY e.element ASC"; + if ($tracks = $DB->get_records_sql($sql, [$userid, $scoid, $attempt])) { $usertrack = scorm_format_interactions($tracks); $usertrack->userid = $userid; $usertrack->scoid = $scoid; @@ -636,7 +646,7 @@ function scorm_get_tracks($scoid, $userid, $attempt='') { /** * helper function to return a formatted list of interactions for reports. * - * @param array $trackdata the records from scorm_scoes_track table + * @param array $trackdata the user tracking records from the database * @return object formatted list of interactions */ function scorm_format_interactions($trackdata) { @@ -693,27 +703,24 @@ function scorm_format_interactions($trackdata) { function scorm_get_sco_runtime($scormid, $scoid, $userid, $attempt=1) { global $DB; - $timedata = new stdClass(); $params = array('userid' => $userid, 'scormid' => $scormid, 'attempt' => $attempt); + $sql = "SELECT min(timemodified) as start, max(timemodified) as finish + FROM {scorm_scoes_value} v + JOIN {scorm_attempt} a on a.id = v.attemptid + WHERE a.userid = :userid AND a.scormid = :scormid AND a.attempt = :attempt"; if (!empty($scoid)) { $params['scoid'] = $scoid; + $sql .= " AND v.scoid = :scoid"; } - $tracks = $DB->get_records('scorm_scoes_track', $params, "timemodified ASC"); - if ($tracks) { - $tracks = array_values($tracks); - } - - if ($tracks) { - $timedata->start = $tracks[0]->timemodified; + $timedata = $DB->get_record_sql($sql, $params); + if (!empty($timedata)) { + return $timedata; } else { + $timedata = new stdClass(); $timedata->start = false; + + return $timedata; } - if ($tracks && $track = array_pop($tracks)) { - $timedata->finish = $track->timemodified; - } else { - $timedata->finish = $timedata->start; - } - return $timedata; } function scorm_grade_user_attempt($scorm, $userid, $attempt=1) { @@ -840,7 +847,7 @@ function scorm_get_last_attempt($scormid, $userid) { // Find the last attempt number for the given user id and scorm id. $sql = "SELECT MAX(attempt) - FROM {scorm_scoes_track} + FROM {scorm_attempt} WHERE userid = ? AND scormid = ?"; $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid)); if (empty($lastattempt)) { @@ -863,7 +870,7 @@ function scorm_get_first_attempt($scormid, $userid) { // Find the first attempt number for the given user id and scorm id. $sql = "SELECT MIN(attempt) - FROM {scorm_scoes_track} + FROM {scorm_attempt} WHERE userid = ? AND scormid = ?"; $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid)); @@ -886,12 +893,14 @@ function scorm_get_last_completed_attempt($scormid, $userid) { global $DB; // Find the last completed attempt number for the given user id and scorm id. - $sql = "SELECT MAX(attempt) - FROM {scorm_scoes_track} + $sql = "SELECT MAX(a.attempt) + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v ON v.attemptid = a.id + JOIN {scorm_element} e ON e.id = v.elementid WHERE userid = ? AND scormid = ? - AND (".$DB->sql_compare_text('value')." = ".$DB->sql_compare_text('?')." OR ". - $DB->sql_compare_text('value')." = ".$DB->sql_compare_text('?').")"; - $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid, 'completed', 'passed')); + AND (" . $DB->sql_compare_text('v.value') . " = " . $DB->sql_compare_text('?') . " OR ". + $DB->sql_compare_text('v.value') . " = " . $DB->sql_compare_text('?') . ")"; + $lastattempt = $DB->get_field_sql($sql, [$userid, $scormid, 'completed', 'passed']); if (empty($lastattempt)) { return '1'; } else { @@ -910,8 +919,8 @@ function scorm_get_last_completed_attempt($scormid, $userid) { function scorm_get_all_attempts($scormid, $userid) { global $DB; $attemptids = array(); - $sql = "SELECT DISTINCT attempt FROM {scorm_scoes_track} WHERE userid = ? AND scormid = ? ORDER BY attempt"; - $attempts = $DB->get_records_sql($sql, array($userid, $scormid)); + $sql = "SELECT DISTINCT attempt FROM {scorm_attempt} WHERE userid = ? AND scormid = ? ORDER BY attempt"; + $attempts = $DB->get_records_sql($sql, [$userid, $scormid]); foreach ($attempts as $attempt) { $attemptids[] = $attempt->attempt; } @@ -926,7 +935,7 @@ function scorm_get_all_attempts($scormid, $userid) { * @param string $action base URL for the organizations select box * @param stdClass $cm course module object */ -function scorm_print_launch ($user, $scorm, $action, $cm) { +function scorm_print_launch($user, $scorm, $action, $cm) { global $CFG, $DB, $OUTPUT; if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) { @@ -1084,7 +1093,7 @@ function scorm_get_count_users($scormid, $groupingid=null) { if (!empty($groupingid)) { $sql = "SELECT COUNT(DISTINCT st.userid) - FROM {scorm_scoes_track} st + FROM {scorm_attempt} st INNER JOIN {groups_members} gm ON st.userid = gm.userid INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid WHERE st.scormid = ? AND gg.groupingid = ? @@ -1092,7 +1101,7 @@ function scorm_get_count_users($scormid, $groupingid=null) { $params = array($scormid, $groupingid); } else { $sql = "SELECT COUNT(DISTINCT st.userid) - FROM {scorm_scoes_track} st + FROM {scorm_attempt} st WHERE st.scormid = ? "; $params = array($scormid); @@ -1334,7 +1343,7 @@ function scorm_get_attempt_status($user, $scorm, $cm='') { if (!empty($cm)) { $context = context_module::instance($cm->id); if (has_capability('mod/scorm:deleteownresponses', $context) && - $DB->record_exists('scorm_scoes_track', array('userid' => $user->id, 'scormid' => $scorm->id))) { + $DB->record_exists('scorm_attempt', ['userid' => $user->id, 'scormid' => $scorm->id])) { // Check to see if any data is stored for this user. $deleteurl = new moodle_url($PAGE->url, array('action' => 'delete', 'sesskey' => sesskey())); $result .= $OUTPUT->single_button($deleteurl, get_string('deleteallattempts', 'scorm')); @@ -1370,17 +1379,30 @@ function scorm_get_attempt_count($userid, $scorm, $returnobjects = false, $ignor $params = array('userid' => $userid, 'scormid' => $scorm->id); if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested. $params['element'] = $element; + $sql = "SELECT DISTINCT a.attempt AS attemptnumber + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v ON v.attemptid = a.id + JOIN {scorm_element} e ON e.id = v.elementid + WHERE a.userid = :userid AND a.scormid = :scormid AND e.element = :element ORDER BY a.attempt"; + $attempts = $DB->get_records_sql($sql, $params); + } else { + $attempts = $DB->get_records('scorm_attempt', $params, 'attempt', 'DISTINCT attempt AS attemptnumber'); } - $attempts = $DB->get_records('scorm_scoes_track', $params, 'attempt', 'DISTINCT attempt AS attemptnumber'); + return $attempts; } else { - $params = array($userid, $scorm->id); - $sql = "SELECT COUNT(DISTINCT attempt) - FROM {scorm_scoes_track} - WHERE userid = ? AND scormid = ?"; + $params = ['userid' => $userid, 'scormid' => $scorm->id]; if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested. - $sql .= ' AND element = ?'; - $params[] = $element; + $params['element'] = $element; + $sql = "SELECT COUNT(DISTINCT a.attempt) + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v ON v.attemptid = a.id + JOIN {scorm_element} e ON e.id = v.elementid + WHERE a.userid = :userid AND a.scormid = :scormid AND e.element = :element"; + } else { + $sql = "SELECT COUNT(DISTINCT attempt) + FROM {scorm_attempt} + WHERE userid = :userid AND scormid = :scormid"; } $attemptscount = $DB->count_records_sql($sql, $params); @@ -1453,22 +1475,26 @@ function scorm_delete_responses($attemptids, $scorm) { * * @param int $userid ID of User * @param stdClass $scorm Scorm object - * @param int $attemptid user attempt that need to be deleted + * @param int|stdClass $attemptornumber user attempt that need to be deleted * * @return bool true suceeded */ -function scorm_delete_attempt($userid, $scorm, $attemptid) { - global $DB; +function scorm_delete_attempt($userid, $scorm, $attemptornumber) { + if (is_object($attemptornumber)) { + $attempt = $attemptornumber; + } else { + $attempt = scorm_get_attempt($userid, $scorm->id, $attemptornumber, false); + } - $DB->delete_records('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id, 'attempt' => $attemptid)); + scorm_delete_tracks($scorm->id, null, $userid, $attempt->id); $cm = get_coursemodule_from_instance('scorm', $scorm->id); // Trigger instances list viewed event. - $event = \mod_scorm\event\attempt_deleted::create(array( - 'other' => array('attemptid' => $attemptid), + $event = \mod_scorm\event\attempt_deleted::create([ + 'other' => ['attemptid' => $attempt->attempt], 'context' => context_module::instance($cm->id), 'relateduserid' => $userid - )); + ]); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('scorm', $scorm); $event->trigger(); @@ -2496,6 +2522,123 @@ function scorm_update_calendar(stdClass $scorm, $cmid) { calendar_event::create($event, false); } } - - return true; +} + +/** + * Function to delete user tracks from tables. + * + * @param int $scormid - id from scorm. + * @param int $scoid - id of sco that needs to be deleted. + * @param int $userid - userid that needs to be deleted. + * @param int $attemptid - attemptid that should be deleted. + * @since Moodle 4.3 + */ +function scorm_delete_tracks($scormid, $scoid = null, $userid = null, $attemptid = null) { + global $DB; + + $usersql = ''; + $params = ['scormid' => $scormid]; + if (!empty($attemptid)) { + $params['attemptid'] = $attemptid; + $sql = "attemptid = :attemptid"; + } else { + if (!empty($userid)) { + $usersql = ' AND userid = :userid'; + $params['userid'] = $userid; + } + $sql = "attemptid in (SELECT id FROM {scorm_attempt} WHERE scormid = :scormid $usersql)"; + } + + if (!empty($scoid)) { + $params['scoid'] = $scoid; + $sql .= " AND scoid = :scoid"; + } + $DB->delete_records_select('scorm_scoes_value', $sql, $params); + + if (empty($scoid)) { + if (empty($attemptid)) { + // Scoid is empty so we delete the attempt as well. + $DB->delete_records('scorm_attempt', $params); + } else { + $DB->delete_records('scorm_attempt', ['id' => $attemptid]); + } + } +} + +/** + * Get specific scorm track data. + * Note: the $attempt var is optional as SCORM 2004 code doesn't always use it, probably a bug, + * but we do not want to change SCORM 2004 behaviour right now. + * + * @param int $scoid - scoid. + * @param int $userid - user id of user. + * @param string $element - name of element being requested. + * @param int $attempt - attempt number (not id) + * @since Moodle 4.3 + * @return mixed + */ +function scorm_get_sco_value($scoid, $userid, $element, $attempt = null): ?stdClass { + global $DB; + $params = ['scoid' => $scoid, 'userid' => $userid, 'element' => $element]; + + $sql = "SELECT a.id, a.userid, a.scormid, a.attempt, v.id as valueid, v.scoid, v.value, v.timemodified, e.element + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v ON v.attemptid = a.id + JOIN {scorm_element} e on e.id = v.elementid + WHERE v.scoid = :scoid AND a.userid = :userid AND e.element = :element"; + + if ($attempt !== null) { + $params['attempt'] = $attempt; + $sql .= " AND a.attempt = :attempt"; + } + $value = $DB->get_record_sql($sql, $params); + return $value ?: null; +} + +/** + * Get attempt record, allow one to be created if doesn't exist. + * + * @param int $userid - user id. + * @param int $scormid - SCORM id. + * @param int $attempt - attempt number. + * @param boolean $create - should an attempt record be created if it does not exist. + * @since Moodle 4.3 + * @return stdclass + */ +function scorm_get_attempt($userid, $scormid, $attempt, $create = true): ?stdClass { + global $DB; + $params = ['scormid' => $scormid, 'userid' => $userid, 'attempt' => $attempt]; + $attemptobject = $DB->get_record('scorm_attempt', $params); + if (empty($attemptobject) && $create) { + // Create new attempt. + $attemptobject = new stdClass(); + $attemptobject->userid = $userid; + $attemptobject->attempt = $attempt; + $attemptobject->scormid = $scormid; + $attemptobject->id = $DB->insert_record('scorm_attempt', $attemptobject); + } + return $attemptobject ?: null; +} + +/** + * Get Scorm element id from cache, allow one to be created if doesn't exist. + * + * @param string $elementname - name of element that is being requested. + * @since Moodle 4.3 + * @return int - element id. + */ +function scorm_get_elementid($elementname): ?int { + global $DB; + $cache = cache::make('mod_scorm', 'elements'); + $element = $cache->get($elementname); + if (empty($element)) { + // Create new attempt. + $element = new stdClass(); + $element->element = $elementname; + $elementid = $DB->insert_record('scorm_element', $element); + $cache->set($elementname, $elementid); + return $elementid; + } else { + return $element; + } } diff --git a/mod/scorm/report/basic/classes/report.php b/mod/scorm/report/basic/classes/report.php index a62e645ba46..1289a19ee8f 100644 --- a/mod/scorm/report/basic/classes/report.php +++ b/mod/scorm/report/basic/classes/report.php @@ -274,32 +274,32 @@ class report extends \mod_scorm\report { $csvexport->add_data($headers); } // Construct the SQL. - $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, '; + $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').' AS uniqueid, '; // TODO Does not support custom user profile fields (MDL-70456). $userfields = \core_user\fields::for_identity($coursecontext, false)->with_userpic()->including('idnumber'); $selectfields = $userfields->get_sql('u', false, '', 'userid')->selects; - $select .= 'st.scormid AS scormid, st.attempt AS attempt ' . $selectfields . ' '; + $select .= 'sa.scormid AS scormid, sa.attempt AS attempt ' . $selectfields . ' '; - // This part is the same for all cases - join users and scorm_scoes_track tables. + // This part is the same for all cases - join users and user tracking tables. $from = 'FROM {user} u '; - $from .= 'LEFT JOIN {scorm_scoes_track} st ON st.userid = u.id AND st.scormid = '.$scorm->id; + $from .= 'LEFT JOIN {scorm_attempt} sa ON sa.userid = u.id AND sa.scormid = '.$scorm->id; switch ($attemptsmode) { case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH: // Show only students with attempts. - $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NOT NULL"; + $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NOT NULL"; break; case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO: // Show only students without attempts. - $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NULL"; + $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NULL"; break; case SCORM_REPORT_ATTEMPTS_ALL_STUDENTS: // Show all students with or without attempts. - $where = " WHERE u.id IN ({$allowedlistsql}) AND (st.userid IS NOT NULL OR st.userid IS NULL)"; + $where = " WHERE u.id IN ({$allowedlistsql}) AND (sa.userid IS NOT NULL OR sa.userid IS NULL)"; break; } - $countsql = 'SELECT COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').')) AS nbresults, '; - $countsql .= 'COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'st.attempt').')) AS nbattempts, '; + $countsql = 'SELECT COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').')) AS nbresults, '; + $countsql .= 'COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'sa.attempt').')) AS nbattempts, '; $countsql .= 'COUNT(DISTINCT(u.id)) AS nbusers '; $countsql .= $from.$where; diff --git a/mod/scorm/report/graphs/classes/report.php b/mod/scorm/report/graphs/classes/report.php index f88f013e5e3..fbadd9a9e66 100644 --- a/mod/scorm/report/graphs/classes/report.php +++ b/mod/scorm/report/graphs/classes/report.php @@ -61,13 +61,14 @@ class report extends \mod_scorm\report { $params = array_merge($params, ['scoid' => $scoid]); // Construct the SQL. - $sql = "SELECT DISTINCT " . $DB->sql_concat('st.userid', '\'#\'', 'COALESCE(st.attempt, 0)') . " AS uniqueid, - st.userid AS userid, - st.scormid AS scormid, - st.attempt AS attempt, - st.scoid AS scoid - FROM {scorm_scoes_track} st - WHERE st.userid IN ({$allowedlist}) AND st.scoid = :scoid"; + $sql = "SELECT DISTINCT " . $DB->sql_concat('a.userid', '\'#\'', 'COALESCE(a.attempt, 0)') . " AS uniqueid, + a.userid AS userid, + a.scormid AS scormid, + a.attempt AS attempt, + v.scoid AS scoid + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v ON v.attemptid = a.id + WHERE a.userid IN ({$allowedlist}) AND v.scoid = :scoid"; $attempts = $DB->get_records_sql($sql, $params); $usergrades = []; diff --git a/mod/scorm/report/interactions/classes/report.php b/mod/scorm/report/interactions/classes/report.php index 24e195739c4..21402a2e658 100644 --- a/mod/scorm/report/interactions/classes/report.php +++ b/mod/scorm/report/interactions/classes/report.php @@ -163,32 +163,32 @@ class report extends \mod_scorm\report { } // Construct the SQL. - $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, '; + $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').' AS uniqueid, '; // TODO Does not support custom user profile fields (MDL-70456). $userfields = \core_user\fields::for_identity($coursecontext, false)->with_userpic()->including('idnumber'); $selectfields = $userfields->get_sql('u', false, '', 'userid')->selects; - $select .= 'st.scormid AS scormid, st.attempt AS attempt ' . $selectfields . ' '; + $select .= 'sa.scormid AS scormid, sa.attempt AS attempt ' . $selectfields . ' '; - // This part is the same for all cases - join users and scorm_scoes_track tables. + // This part is the same for all cases - join users and user tracking tables. $from = 'FROM {user} u '; - $from .= 'LEFT JOIN {scorm_scoes_track} st ON st.userid = u.id AND st.scormid = '.$scorm->id; + $from .= 'LEFT JOIN {scorm_attempt} sa ON sa.userid = u.id AND sa.scormid = '.$scorm->id; switch ($attemptsmode) { case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH: // Show only students with attempts. - $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NOT NULL"; + $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NOT NULL"; break; case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO: // Show only students without attempts. - $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NULL"; + $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NULL"; break; case SCORM_REPORT_ATTEMPTS_ALL_STUDENTS: // Show all students with or without attempts. - $where = " WHERE u.id IN ({$allowedlistsql}) AND (st.userid IS NOT NULL OR st.userid IS NULL)"; + $where = " WHERE u.id IN ({$allowedlistsql}) AND (sa.userid IS NOT NULL OR sa.userid IS NULL)"; break; } - $countsql = 'SELECT COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').')) AS nbresults, '; - $countsql .= 'COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'st.attempt').')) AS nbattempts, '; + $countsql = 'SELECT COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').')) AS nbresults, '; + $countsql .= 'COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'sa.attempt').')) AS nbattempts, '; $countsql .= 'COUNT(DISTINCT(u.id)) AS nbusers '; $countsql .= $from.$where; $questioncount = get_scorm_question_count($scorm->id); diff --git a/mod/scorm/report/objectives/classes/report.php b/mod/scorm/report/objectives/classes/report.php index eaaa4bb925b..15f58626932 100644 --- a/mod/scorm/report/objectives/classes/report.php +++ b/mod/scorm/report/objectives/classes/report.php @@ -157,32 +157,32 @@ class report extends \mod_scorm\report { } // Construct the SQL. - $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, '; + $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').' AS uniqueid, '; // TODO Does not support custom user profile fields (MDL-70456). $userfields = \core_user\fields::for_identity($coursecontext, false)->with_userpic()->including('idnumber'); $selectfields = $userfields->get_sql('u', false, '', 'userid')->selects; - $select .= 'st.scormid AS scormid, st.attempt AS attempt ' . $selectfields . ' '; + $select .= 'sa.scormid AS scormid, sa.attempt AS attempt ' . $selectfields . ' '; - // This part is the same for all cases - join users and scorm_scoes_track tables. + // This part is the same for all cases - join users and user tracking tables. $from = 'FROM {user} u '; - $from .= 'LEFT JOIN {scorm_scoes_track} st ON st.userid = u.id AND st.scormid = '.$scorm->id; + $from .= 'LEFT JOIN {scorm_attempt} sa ON sa.userid = u.id AND sa.scormid = '.$scorm->id; switch ($attemptsmode) { case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH: // Show only students with attempts. - $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NOT NULL"; + $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NOT NULL"; break; case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO: // Show only students without attempts. - $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NULL"; + $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NULL"; break; case SCORM_REPORT_ATTEMPTS_ALL_STUDENTS: // Show all students with or without attempts. - $where = " WHERE u.id IN ({$allowedlistsql}) AND (st.userid IS NOT NULL OR st.userid IS NULL)"; + $where = " WHERE u.id IN ({$allowedlistsql}) AND (sa.userid IS NOT NULL OR sa.userid IS NULL)"; break; } - $countsql = 'SELECT COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').')) AS nbresults, '; - $countsql .= 'COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'st.attempt').')) AS nbattempts, '; + $countsql = 'SELECT COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').')) AS nbresults, '; + $countsql .= 'COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'sa.attempt').')) AS nbattempts, '; $countsql .= 'COUNT(DISTINCT(u.id)) AS nbusers '; $countsql .= $from.$where; @@ -620,23 +620,22 @@ class report extends \mod_scorm\report { */ function get_scorm_objectives($scormid) { global $DB; - $objectives = array(); - $params = array(); - $select = "scormid = ? AND "; - $select .= $DB->sql_like("element", "?", false); - $params[] = $scormid; - $params[] = "cmi.objectives%.id"; - $value = $DB->sql_compare_text('value'); - $rs = $DB->get_recordset_select("scorm_scoes_track", $select, $params, 'value', "DISTINCT $value AS value, scoid"); - if ($rs->valid()) { - foreach ($rs as $record) { - $objectives[$record->scoid][] = $record->value; - } - // Now naturally sort the sco arrays. - foreach ($objectives as $scoid => $sco) { - natsort($objectives[$scoid]); - } + $objectives = []; + $params = ['scormid' => $scormid, 'search' => 'cmi.objectives%.id']; + + $value = $DB->sql_compare_text('v.value'); + $sql = "SELECT DISTINCT $value as value, ss.id + FROM {scorm_scoes_value} v + JOIN {scorm_scoes} ss ON ss.id = v.scoid AND ss.scorm = :scormid + JOIN {scorm_element} e ON v.elementid = e.id + WHERE ".$DB->sql_like("element", ":search", false); + $rs = $DB->get_records_sql($sql, $params); + foreach ($rs as $record) { + $objectives[$record->scoid][] = $record->value; + } + // Now naturally sort the sco arrays. + foreach ($objectives as $scoid => $sco) { + natsort($objectives[$scoid]); } - $rs->close(); return $objectives; } diff --git a/mod/scorm/report/reportlib.php b/mod/scorm/report/reportlib.php index 73c1aaf367a..2db553d3878 100644 --- a/mod/scorm/report/reportlib.php +++ b/mod/scorm/report/reportlib.php @@ -75,11 +75,16 @@ function get_scorm_question_count($scormid) { global $DB; $count = 0; $params = array(); - $select = "scormid = ? AND "; - $select .= $DB->sql_like("element", "?", false); + $sql = "SELECT DISTINCT e.id, e.element + FROM {scorm_element} e + JOIN {scorm_scoes_value} v ON e.id = v.elementid + JOIN {scorm_attempt} a ON a.id = v.attemptid + WHERE a.scormid = ? AND ". $DB->sql_like("element", "?", false) . + " ORDER BY e.element"; + $params[] = $scormid; $params[] = "cmi.interactions_%.id"; - $rs = $DB->get_recordset_select("scorm_scoes_track", $select, $params, 'element'); + $rs = $DB->get_recordset_sql($sql, $params); $keywords = array("cmi.interactions_", ".id"); if ($rs->valid()) { foreach ($rs as $record) { diff --git a/mod/scorm/report/userreportinteractions.php b/mod/scorm/report/userreportinteractions.php index 8016d5e726e..a00820a6ec5 100644 --- a/mod/scorm/report/userreportinteractions.php +++ b/mod/scorm/report/userreportinteractions.php @@ -69,8 +69,12 @@ $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('scorm', $scorm); $event->trigger(); -$trackdata = $DB->get_records('scorm_scoes_track', array('userid' => $user->id, 'scormid' => $scorm->id, - 'attempt' => $attempt)); +$sql = "SELECT a.id, a.userid, a.scormid, v.scoid, a.attempt, v.value, v.timemodified, e.element + FROM {scorm_attempt} a + JOIN {scorm_scoes_value} v ON v.attemptid = a.id + JOIN {scorm_element} e ON e.id = v.elementid + WHERE a.userid = :userid AND a.scormid = :scormid AND a.attempt = :attempt"; +$trackdata = $DB->get_records_sql($sql, ['userid' => $userid, 'scormid' => $scorm->id, 'attempt' => $attempt]); $usertrack = scorm_format_interactions($trackdata); $questioncount = get_scorm_question_count($scorm->id); diff --git a/mod/scorm/tests/backup/restore_date_test.php b/mod/scorm/tests/backup/restore_date_test.php index 55075b7cacf..d82abaf96a7 100644 --- a/mod/scorm/tests/backup/restore_date_test.php +++ b/mod/scorm/tests/backup/restore_date_test.php @@ -41,7 +41,7 @@ class restore_date_test extends \restore_date_testcase { scorm_insert_track($USER->id, $scorm->id, $sco->id, 4, 'cmi.core.score.raw', 10); // We do not want second differences to fail our test because of execution delays. - $DB->set_field('scorm_scoes_track', 'timemodified', $time); + $DB->set_field('scorm_scoes_value', 'timemodified', $time); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); @@ -51,7 +51,11 @@ class restore_date_test extends \restore_date_testcase { $props = ['timeopen', 'timeclose']; $this->assertFieldsRolledForward($scorm, $newscorm, $props); - $tracks = $DB->get_records('scorm_scoes_track', ['scormid' => $newscorm->id]); + $sql = "SELECT * + FROM {scorm_scoes_value} v + JOIN {scorm_attempt} a ON a.id = v.attemptid + WHERE a.scormid = ?"; + $tracks = $DB->get_records_sql($sql, [$newscorm->id]); foreach ($tracks as $track) { $this->assertEquals($time, $track->timemodified); } diff --git a/mod/scorm/tests/externallib_test.php b/mod/scorm/tests/externallib_test.php index deb7de9ec90..f6ef94c501a 100644 --- a/mod/scorm/tests/externallib_test.php +++ b/mod/scorm/tests/externallib_test.php @@ -504,9 +504,12 @@ class externallib_test extends externallib_advanced_testcase { $result = mod_scorm_external::insert_scorm_tracks($sco->id, 1, $tracks); $result = external_api::clean_returnvalue(mod_scorm_external::insert_scorm_tracks_returns(), $result); $this->assertCount(0, $result['warnings']); - - $trackids = $DB->get_records('scorm_scoes_track', array('userid' => $student->id, 'scoid' => $sco->id, - 'scormid' => $scorm->id, 'attempt' => 1)); + $sql = "SELECT v.id + FROM {scorm_scoes_value} v + JOIN {scorm_attempt} a ON a.id = v.attemptid + WHERE a.userid = :userid AND a.attempt = :attempt AND a.scormid = :scormid AND v.scoid = :scoid"; + $params = ['userid' => $student->id, 'scoid' => $sco->id, 'scormid' => $scorm->id, 'attempt' => 1]; + $trackids = $DB->get_records_sql($sql, $params); // We use asort here to prevent problems with ids ordering. $expectedkeys = array_keys($trackids); $this->assertEquals(asort($expectedkeys), asort($result['trackids'])); diff --git a/mod/scorm/tests/privacy/provider_test.php b/mod/scorm/tests/privacy/provider_test.php index 69318090898..8182ab90ed6 100644 --- a/mod/scorm/tests/privacy/provider_test.php +++ b/mod/scorm/tests/privacy/provider_test.php @@ -158,8 +158,8 @@ class provider_test extends provider_testcase { $this->setAdminUser(); $this->scorm_setup_test_scenario_data(); - // Before deletion, we should have 8 entries in the scorm_scoes_track table. - $count = $DB->count_records('scorm_scoes_track'); + // Before deletion, we should have 8 entries in the scorm_scoes_value table. + $count = $DB->count_records('scorm_scoes_value'); $this->assertEquals(8, $count); // Before deletion, we should have 4 entries in the scorm_aicc_session table. $count = $DB->count_records('scorm_aicc_session'); @@ -168,8 +168,8 @@ class provider_test extends provider_testcase { // Delete data based on the context. provider::delete_data_for_all_users_in_context($this->context); - // After deletion, the scorm_scoes_track entries should have been deleted. - $count = $DB->count_records('scorm_scoes_track'); + // After deletion, the scorm_scoes_value entries should have been deleted. + $count = $DB->count_records('scorm_scoes_value'); $this->assertEquals(0, $count); // After deletion, the scorm_aicc_session entries should have been deleted. $count = $DB->count_records('scorm_aicc_session'); @@ -186,8 +186,8 @@ class provider_test extends provider_testcase { $this->setAdminUser(); $this->scorm_setup_test_scenario_data(); - // Before deletion, we should have 8 entries in the scorm_scoes_track table. - $count = $DB->count_records('scorm_scoes_track'); + // Before deletion, we should have 8 entries in the scorm_scoes_value table. + $count = $DB->count_records('scorm_scoes_value'); $this->assertEquals(8, $count); // Before deletion, we should have 4 entries in the scorm_aicc_session table. $count = $DB->count_records('scorm_aicc_session'); @@ -196,10 +196,10 @@ class provider_test extends provider_testcase { $approvedcontextlist = new approved_contextlist($this->student1, 'scorm', [$this->context->id]); provider::delete_data_for_user($approvedcontextlist); - // After deletion, the scorm_scoes_track entries for the first student should have been deleted. - $count = $DB->count_records('scorm_scoes_track', ['userid' => $this->student1->id]); + // After deletion, the scorm_attempt entries for the first student should have been deleted. + $count = $DB->count_records('scorm_attempt', ['userid' => $this->student1->id]); $this->assertEquals(0, $count); - $count = $DB->count_records('scorm_scoes_track'); + $count = $DB->count_records('scorm_scoes_value'); $this->assertEquals(4, $count); // After deletion, the scorm_aicc_session entries for the first student should have been deleted. $count = $DB->count_records('scorm_aicc_session', ['userid' => $this->student1->id]); @@ -214,7 +214,7 @@ class provider_test extends provider_testcase { // Delete scoes_track for student0 (nothing has to be removed). $approvedcontextlist = new approved_contextlist($this->student0, 'scorm', [$this->context->id]); provider::delete_data_for_user($approvedcontextlist); - $count = $DB->count_records('scorm_scoes_track'); + $count = $DB->count_records('scorm_scoes_value'); $this->assertEquals(4, $count); $count = $DB->count_records('scorm_aicc_session'); $this->assertEquals(2, $count); @@ -231,8 +231,8 @@ class provider_test extends provider_testcase { $this->setAdminUser(); $this->scorm_setup_test_scenario_data(); - // Before deletion, we should have 8 entries in the scorm_scoes_track table. - $count = $DB->count_records('scorm_scoes_track'); + // Before deletion, we should have 8 entries in the scorm_scoes_value table. + $count = $DB->count_records('scorm_scoes_value'); $this->assertEquals(8, $count); // Before deletion, we should have 4 entries in the scorm_aicc_session table. $count = $DB->count_records('scorm_aicc_session'); @@ -243,10 +243,10 @@ class provider_test extends provider_testcase { $approvedlist = new approved_userlist($this->context, $component, $approveduserids); provider::delete_data_for_users($approvedlist); - // After deletion, the scorm_scoes_track entries for the first student should have been deleted. - $count = $DB->count_records('scorm_scoes_track', ['userid' => $this->student1->id]); + // After deletion, the scorm_attempt entries for the first student should have been deleted. + $count = $DB->count_records('scorm_attempt', ['userid' => $this->student1->id]); $this->assertEquals(0, $count); - $count = $DB->count_records('scorm_scoes_track'); + $count = $DB->count_records('scorm_scoes_value'); $this->assertEquals(4, $count); // After deletion, the scorm_aicc_session entries for the first student should have been deleted. @@ -264,7 +264,7 @@ class provider_test extends provider_testcase { $approvedlist = new approved_userlist($this->context, $component, $approveduserids); provider::delete_data_for_users($approvedlist); - $count = $DB->count_records('scorm_scoes_track'); + $count = $DB->count_records('scorm_scoes_value'); $this->assertEquals(4, $count); $count = $DB->count_records('scorm_aicc_session'); $this->assertEquals(2, $count); diff --git a/mod/scorm/version.php b/mod/scorm/version.php index 54988748a91..173cad2cc61 100644 --- a/mod/scorm/version.php +++ b/mod/scorm/version.php @@ -24,6 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2023042400; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2023042403; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2023041800; // Requires this Moodle version. -$plugin->component = 'mod_scorm'; // Full name of the plugin (used for diagnostics). \ No newline at end of file +$plugin->component = 'mod_scorm'; // Full name of the plugin (used for diagnostics). + diff --git a/mod/scorm/view.php b/mod/scorm/view.php index d8d24016f87..61e655cb777 100644 --- a/mod/scorm/view.php +++ b/mod/scorm/view.php @@ -152,7 +152,7 @@ if (!empty($action) && confirm_sesskey() && has_capability('mod/scorm:deleteownr exit; } else if ($action == 'deleteconfirm') { // Delete this users attempts. - $DB->delete_records('scorm_scoes_track', array('userid' => $USER->id, 'scormid' => $scorm->id)); + scorm_delete_tracks($scorm->id, null, $USER->id); scorm_update_grades($scorm, $USER->id, true); echo $OUTPUT->notification(get_string('scormresponsedeleted', 'scorm'), 'notifysuccess'); }