From fffeaafde416a6100ab544b1f960b5b13e53e408 Mon Sep 17 00:00:00 2001 From: Dmitrii Metelkin Date: Wed, 18 Oct 2023 22:06:17 +1100 Subject: [PATCH] issue #21: add support for mod_hvp and mod_h5pactivity --- ...backup_local_recompletion_plugin.class.php | 40 +++++ ...estore_local_recompletion_plugin.class.php | 63 ++++++++ classes/plugins/mod_h5pactivity.php | 153 ++++++++++++++++++ classes/plugins/mod_hvp.php | 141 ++++++++++++++++ classes/privacy/provider.php | 128 +++++++++++++-- db/install.xml | 61 ++++++- db/upgrade.php | 88 ++++++++++ lang/en/local_recompletion.php | 16 ++ locallib.php | 2 +- tests/observer_test.php | 2 +- tests/plugins/mod_h5pactivity_test.php | 138 ++++++++++++++++ version.php | 4 +- 12 files changed, 822 insertions(+), 14 deletions(-) create mode 100644 classes/plugins/mod_h5pactivity.php create mode 100644 classes/plugins/mod_hvp.php create mode 100644 tests/plugins/mod_h5pactivity_test.php diff --git a/backup/moodle2/backup_local_recompletion_plugin.class.php b/backup/moodle2/backup_local_recompletion_plugin.class.php index 137f5b8..8e2c118 100644 --- a/backup/moodle2/backup_local_recompletion_plugin.class.php +++ b/backup/moodle2/backup_local_recompletion_plugin.class.php @@ -144,6 +144,46 @@ protected function define_course_plugin_structure() { } $choiceanswer->annotate_ids('user', 'userid'); + // Now deal with hvp archive tables. + $hvpattempts = new backup_nested_element('hvpattempts'); + $hvpattempt = new backup_nested_element('hvpattempt', array('id'), array( + 'user_id', 'hvp_id', 'sub_content_id', 'data_id', 'data', 'preloaded', 'delete_on_content_change', 'course')); + + $recompletion->add_child($hvpattempts); + $hvpattempts->add_child($hvpattempt); + + if ($usercompletion) { + $hvpattempt->set_source_table('local_recompletion_hvp', array('course' => backup::VAR_COURSEID)); + } + $hvpattempt->annotate_ids('user', 'user_id'); + + // Now deal with h5p table. + $h5ps = new backup_nested_element('h5ps'); + $h5p = new backup_nested_element('h5p', array('id'), array( + 'originalattemptid', 'h5pactivityid', 'userid', 'timecreated', 'timemodified', + 'rawscore', 'maxscore', 'scaled', 'duration', 'completion', 'success', 'course')); + + // Now deal with h5p results table. + $h5presults = new backup_nested_element('h5presults'); + $h5presult = new backup_nested_element('h5presult', array('id'), array( + 'attemptid', 'subcontent', 'timecreated', 'interactiontype', 'description', + 'correctpattern', 'response', 'additionals', 'rawscore', 'maxscore', 'duration', 'completion', 'success', 'course')); + + $recompletion->add_child($h5ps); + $h5ps->add_child($h5p); + $h5p->add_child($h5presults); + $h5presults->add_child($h5presult); + + if ($usercompletion) { + $h5p->set_source_table('local_recompletion_h5p', array('course' => backup::VAR_COURSEID)); + $h5presult->set_source_table( + 'local_recompletion_h5pr', + array('course' => backup::VAR_COURSEID, 'attemptid' => backup::VAR_PARENTID) + ); + } + + $h5p->annotate_ids('user', 'userid'); + return $plugin; } diff --git a/backup/moodle2/restore_local_recompletion_plugin.class.php b/backup/moodle2/restore_local_recompletion_plugin.class.php index 15fc9fd..09c3b3b 100644 --- a/backup/moodle2/restore_local_recompletion_plugin.class.php +++ b/backup/moodle2/restore_local_recompletion_plugin.class.php @@ -48,6 +48,9 @@ protected function define_course_plugin_structure() { $paths[] = new restore_path_element('recompletion_qg', $elepath.'/quizgrades/grade'); $paths[] = new restore_path_element('recompletion_sst', $elepath.'/scormtracks/sco_track'); $paths[] = new restore_path_element('recompletion_cha', $elepath.'/choiceanswers/choiceanswer'); + $paths[] = new restore_path_element('recompletion_hvp', $elepath.'/hvpattempts/hvpattempt'); + $paths[] = new restore_path_element('recompletion_h5p', $elepath.'/h5ps/h5p'); + $paths[] = new restore_path_element('recompletion_h5pr', $elepath.'/h5ps/h5p/h5presults/h5presult'); return $paths; } @@ -164,6 +167,49 @@ public function process_recompletion_cha($data) { $DB->insert_record('local_recompletion_cha', $data); } + /** + * Process local_recompletion_hvp table. + * @param stdClass $data + */ + public function process_recompletion_hvp($data) { + global $DB; + + $data = (object) $data; + $data->course = $this->task->get_courseid(); + $data->user_id = $this->get_mappingid('user', $data->user_id); + + $DB->insert_record('local_recompletion_hvp', $data); + } + + /** + * Process local_recompletion_h5p table. + * @param stdClass $data + */ + public function process_recompletion_h5p($data) { + global $DB; + + $data = (object) $data; + $oldid = $data->id; + $data->course = $this->task->get_courseid(); + $data->userid = $this->get_mappingid('user', $data->userid); + + $newitemid = $DB->insert_record('local_recompletion_h5p', $data); + $this->set_mapping('recompletion_h5p', $oldid, $newitemid); + } + + /** + * Process local_recompletion_h5pr table. + * @param stdClass $data + */ + public function process_recompletion_h5pr($data) { + global $DB; + + $data = (object) $data; + $data->course = $this->task->get_courseid(); + $data->attemptid = $this->get_new_parentid('recompletion_h5p'); + $DB->insert_record('local_recompletion_h5pr', $data); + } + /** * We call the after restore_course to update the coursemodule ids we didn't know when creating. */ @@ -210,5 +256,22 @@ protected function after_restore_course() { } $rcm->close(); + // Fix hvp attempts. + $rcm = $DB->get_recordset('local_recompletion_hvp', array('course' => $this->task->get_courseid())); + foreach ($rcm as $rc) { + $rc->hvp_id = $this->get_mappingid('hvp', $rc->hvp_id); + $DB->update_record('local_recompletion_hvp', $rc); + } + $rcm->close(); + + // Fix h5p attempts. + $rcm = $DB->get_recordset('local_recompletion_h5p', array('course' => $this->task->get_courseid())); + foreach ($rcm as $rc) { + $rc->h5pactivityid = $this->get_mappingid('h5pactivity', $rc->h5pactivityid); + $rc->originalattemptid = 0; // Don't restore orginal attempt id. + + $DB->update_record('local_recompletion_h5p', $rc); + } + $rcm->close(); } } diff --git a/classes/plugins/mod_h5pactivity.php b/classes/plugins/mod_h5pactivity.php new file mode 100644 index 0000000..5cfec32 --- /dev/null +++ b/classes/plugins/mod_h5pactivity.php @@ -0,0 +1,153 @@ +. + +namespace local_recompletion\plugins; + +use stdClass; +use lang_string; +use admin_setting_configselect; +use admin_setting_configcheckbox; +use admin_settingpage; +use MoodleQuickForm; + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot.'/local/recompletion/locallib.php'); + +/** + * H5P handler event. + * + * @package local_recompletion + * @author 2023 Dmitrii Metelkin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_h5pactivity { + + /** + * Add params to form. + * + * @param MoodleQuickForm $mform + */ + public static function editingform(MoodleQuickForm $mform): void { + $config = get_config('local_recompletion'); + + $cba = []; + $cba[] = $mform->createElement('radio', 'h5pactivity', '', + get_string('donothing', 'local_recompletion'), LOCAL_RECOMPLETION_NOTHING); + $cba[] = $mform->createElement('radio', 'h5pactivity', '', + get_string('delete', 'local_recompletion'), LOCAL_RECOMPLETION_DELETE); + + $mform->addGroup($cba, 'h5pactivity', get_string('h5pattempts', 'local_recompletion'), [' '], false); + $mform->addHelpButton('h5pactivity', 'h5pattempts', 'local_recompletion'); + $mform->setDefault('h5pactivity', $config->h5pattempts); + + $mform->addElement('checkbox', 'archiveh5pactivity', get_string('archive', 'local_recompletion')); + $mform->setDefault('archiveh5pactivity', $config->archiveh5p); + + $mform->disabledIf('archiveh5pactivity', 'enable'); + $mform->hideIf('archiveh5pactivity', 'h5pactivity'); + $mform->disabledIf('h5pactivity', 'enable'); + } + + /** + * Add site level settings for this plugin. + * + * @param admin_settingpage $settings + */ + public static function settings(admin_settingpage $settings): void { + $choices = [ + LOCAL_RECOMPLETION_NOTHING => get_string('donothing', 'local_recompletion'), + LOCAL_RECOMPLETION_DELETE => get_string('delete', 'local_recompletion') + ]; + + $settings->add(new admin_setting_configselect('local_recompletion/h5pattempts', + new lang_string('h5pattempts', 'local_recompletion'), + new lang_string('h5pattempts_help', 'local_recompletion'), LOCAL_RECOMPLETION_NOTHING, $choices)); + + $settings->add(new admin_setting_configcheckbox('local_recompletion/archiveh5p', + new lang_string('archiveh5p', 'local_recompletion'), '', 1)); + } + + /** + * Reset h5pactivity attempt records. + * + * @param int $userid - user id + * @param stdClass $course - course record. + * @param stdClass $config - recompletion config. + */ + public static function reset(int $userid, stdClass $course, stdClass $config): void { + global $DB; + + if (empty($config->h5pactivity)) { + return; + } + + if ($config->h5pactivity == LOCAL_RECOMPLETION_DELETE) { + $params = [ + 'userid' => $userid, + 'course' => $course->id + ]; + + $attemptsselectsql = 'userid = :userid AND h5pactivityid IN (SELECT id FROM {h5pactivity} WHERE course = :course)'; + $resultsselectsql = 'attemptid IN (SELECT id FROM {h5pactivity_attempts} WHERE ' . $attemptsselectsql . ')'; + + if ($config->archiveh5pactivity) { + + // Archive attempts. + $attempts = $DB->get_records_select('h5pactivity_attempts', $attemptsselectsql, $params); + if (!empty($attempts)) { + $attemptids = array_keys($attempts); + + foreach ($attempts as $attempt) { + $attempt->course = $course->id; + $attempt->originalattemptid = $attempt->id; + } + + $DB->insert_records('local_recompletion_h5p', $attempts); + + // Archive results. + $results = $DB->get_records_select('h5pactivity_attempts_results', $resultsselectsql, $params); + if (!empty($results)) { + foreach ($results as $result) { + $result->course = $course->id; + } + $DB->insert_records('local_recompletion_h5pr', $results); + + // Update attemptid for just inserted attempt results with IDs of previously inserted attempts. + // We use temp originalattemptid here as it should be unique. + $sql = 'UPDATE {local_recompletion_h5pr} + SET attemptid = (SELECT id + FROM {local_recompletion_h5p} + WHERE originalattemptid = attemptid)'; + $DB->execute($sql); + } + + // Now reset originalattemptid as we don't need it anymore. + // As well as to avoid issues with backup and restore when potentially originalattemptid can clash + // if restoring a course from another Moodle instance. + list($insql, $inparams) = $DB->get_in_or_equal($attemptids, SQL_PARAMS_NAMED); + $sql = "UPDATE {local_recompletion_h5p} SET originalattemptid = 0 WHERE originalattemptid $insql"; + $DB->execute($sql, $inparams); + } + + } + + // Finally can delete records. + $DB->delete_records_select('h5pactivity_attempts_results', $resultsselectsql, $params); + $DB->delete_records_select('h5pactivity_attempts', $attemptsselectsql, $params); + } + } +} diff --git a/classes/plugins/mod_hvp.php b/classes/plugins/mod_hvp.php new file mode 100644 index 0000000..982f5dd --- /dev/null +++ b/classes/plugins/mod_hvp.php @@ -0,0 +1,141 @@ +. + +namespace local_recompletion\plugins; + +use stdClass; +use lang_string; +use admin_setting_configselect; +use admin_setting_configcheckbox; +use admin_settingpage; +use MoodleQuickForm; + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot.'/local/recompletion/locallib.php'); + +/** + * H5P handler event. + * + * @package local_recompletion + * @author 2023 Dmitrii Metelkin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_hvp { + + /** + * Add params to form. + * + * @param MoodleQuickForm $mform + */ + public static function editingform(MoodleQuickForm $mform): void { + if (!self::installed()) { + return; + } + + $config = get_config('local_recompletion'); + + $cba = []; + $cba[] = $mform->createElement('radio', 'hvp', '', + get_string('donothing', 'local_recompletion'), LOCAL_RECOMPLETION_NOTHING); + $cba[] = $mform->createElement('radio', 'hvp', '', + get_string('delete', 'local_recompletion'), LOCAL_RECOMPLETION_DELETE); + + $mform->addGroup($cba, 'hvp', get_string('hvpattempts', 'local_recompletion'), [' '], false); + $mform->addHelpButton('hvp', 'hvpattempts', 'local_recompletion'); + $mform->setDefault('hvp', $config->hvpattempts); + + $mform->addElement('checkbox', 'archivehvp', get_string('archive', 'local_recompletion')); + $mform->setDefault('archivehvp', $config->archivehvp); + + $mform->disabledIf('archivehvp', 'enable'); + $mform->hideIf('archivehvp', 'hvp'); + $mform->disabledIf('hvp', 'enable'); + } + + /** + * Add site level settings for this plugin. + * + * @param admin_settingpage $settings + */ + public static function settings(admin_settingpage $settings): void { + if (!self::installed()) { + return; + } + + $choices = [ + LOCAL_RECOMPLETION_NOTHING => get_string('donothing', 'local_recompletion'), + LOCAL_RECOMPLETION_DELETE => get_string('delete', 'local_recompletion') + ]; + + $settings->add(new admin_setting_configselect('local_recompletion/hvpattempts', + new lang_string('hvpattempts', 'local_recompletion'), + new lang_string('hvpattempts_help', 'local_recompletion'), LOCAL_RECOMPLETION_NOTHING, $choices)); + + $settings->add(new admin_setting_configcheckbox('local_recompletion/archivehvp', + new lang_string('archivehvp', 'local_recompletion'), '', 1)); + } + + /** + * Reset hvp attempt records. + * + * @param int $userid - user id + * @param stdClass $course - course record. + * @param stdClass $config - recompletion config. + */ + public static function reset(int $userid, stdClass $course, stdClass $config): void { + global $DB; + + if (!self::installed()) { + return; + } + + if (empty($config->hvp)) { + return; + } + + if ($config->hvp == LOCAL_RECOMPLETION_DELETE) { + $params = [ + 'userid' => $userid, + 'course' => $course->id + ]; + + $selectsql = 'user_id = :userid AND hvp_id IN (SELECT id FROM {hvp} WHERE course = :course)'; + + if ($config->archivehvp) { + $records = $DB->get_records_select('hvp_content_user_data', $selectsql, $params); + foreach ($records as $record) { + $record->course = $course->id; + } + $DB->insert_records('local_recompletion_hvp', $records); + } + + $DB->delete_records_select('hvp_content_user_data', $selectsql, $params); + } + } + + /** + * Helper function to check if the plugin is installed. + * @return bool + */ + public static function installed(): bool { + global $CFG; + if (!file_exists($CFG->dirroot . '/mod/hvp/version.php')) { + return false; + } + return true; + } +} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index ebc6ef7..4408e24 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -139,6 +139,27 @@ public static function get_metadata(collection $collection) : collection { 'course' => 'privacy:metadata:course', ], 'privacy:metadata:local_recompletion_ccert_is'); + $collection->add_database_table('local_recompletion_hvp', [ + 'user_id' => 'privacy:metadata:userid', + 'hvp_id' => 'privacy:metadata:local_recompletion_hvp:hvp_id', + 'data' => 'privacy:metadata:local_recompletion_hvp:data', + 'course' => 'privacy:metadata:course', + ], 'privacy:metadata:local_recompletion_hvp'); + + $collection->add_database_table('local_recompletion_h5p', [ + 'userid' => 'privacy:metadata:userid', + 'attempt' => 'privacy:metadata:attempt', + 'timecreated' => 'privacy:metadata:timecreated', + 'timemodified' => 'privacy:metadata:timemodified', + 'rawscore' => 'privacy:metadata:rawscore', + ], 'privacy:metadata:local_recompletion_h5p'); + + $collection->add_database_table('local_recompletion_h5pr', [ + 'attempt' => 'privacy:metadata:attempt', + 'timecreated' => 'privacy:metadata:timecreated', + 'rawscore' => 'privacy:metadata:rawscore', + ], 'privacy:metadata:local_recompletion_h5pr'); + return $collection; } @@ -235,6 +256,38 @@ public static function export_user_data(approved_contextlist $contextlist) { [get_string('recompletion', 'local_recompletion'), 'recompletion_cha'], (object)[array_map([self::class, 'transform_db_row_to_session_data'], $records)]); } + + $records = $DB->get_records('local_recompletion_hvp', array('user_id' => $userid, 'course' => $context->instanceid)); + foreach ($records as $record) { + $context = \context_course::instance($record->course); + writer::with_context($context)->export_data( + [get_string('recompletion', 'local_recompletion'), 'recompletion_hvp'], + (object)[array_map([self::class, 'transform_db_row_to_session_data'], $record)]); + } + + $sql = "SELECT har.id, + ha.attempt, + har.description, + har.interactiontype, + har.response, + har.additionals, + har.rawscore, + har.maxscore, + har.duration, + har.timecreated, + ha.course + FROM {local_recompletion_h5pr} har + JOIN {local_recompletion_h5p} ha ON har.attemptid = ha.id + WHERE ha.course = :course + AND ha.userid = :userid"; + + $records = $DB->get_records($sql, $params); + foreach ($records as $record) { + $context = \context_course::instance($record->course); + writer::with_context($context)->export_data( + [get_string('recompletion', 'local_recompletion'), 'recompletion_h5pr'], + (object)[array_map([self::class, 'transform_db_row_to_session_data'], $record)]); + } } } @@ -282,6 +335,8 @@ public static function delete_data_for_all_users_in_context(\context $context) { $DB->delete_records('local_recompletion_sst', $params); $DB->delete_records('local_recompletion_qr', $params); $DB->delete_records('local_recompletion_cha', $params); + $DB->delete_records('local_recompletion_hvp', $params); + self::delete_hp5_activity_records($courseid); } /** @@ -308,6 +363,8 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { $DB->delete_records('local_recompletion_ltia', ['userid' => $userid]); $DB->delete_records('local_recompletion_qr', $params); $DB->delete_records('local_recompletion_cha', $params); + $DB->delete_records('local_recompletion_hvp', ['user_id' => $userid, 'course' => $courseid]); + self::delete_hp5_activity_records($courseid, $userid); } } @@ -361,6 +418,17 @@ public static function get_contexts_for_userid(int $userid) : contextlist { JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel JOIN {local_recompletion_cha} rc ON rc.course = c.id and rc.userid = :userid"; $contextlist->add_from_sql($sql, $params); + $sql = "SELECT ctx.id + FROM {course} c + JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {local_recompletion_hvp} rc ON rc.course = c.id and rc.user_id = :userid"; + $contextlist->add_from_sql($sql, $params); + $sql = "SELECT ctx.id + FROM {course} c + JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {local_recompletion_h5p} rc ON rc.course = c.id and rc.userid = :userid"; + $contextlist->add_from_sql($sql, $params); + return $contextlist; } /** @@ -430,6 +498,20 @@ public static function get_users_in_context(userlist $userlist) { JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel WHERE ctx.id = :contextid"; $userlist->add_from_sql('userid', $sql, $params); + + $sql = "SELECT rc.user_id + FROM {local_recompletion_hvp} rc + JOIN {course} c ON rc.course = c.id + JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + WHERE ctx.id = :contextid"; + $userlist->add_from_sql('user_id', $sql, $params); + + $sql = "SELECT rc.userid + FROM {local_recompletion_h5p} rc + JOIN {course} c ON rc.course = c.id + JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + WHERE ctx.id = :contextid"; + $userlist->add_from_sql('userid', $sql, $params); } /** * Delete multiple users within a single context. @@ -445,6 +527,7 @@ public static function delete_data_for_users(approved_userlist $userlist) { // Prepare SQL to gather all completed IDs. $userids = $userlist->get_userids(); list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + $params = array_merge($inparams, ['contextlevel' => CONTEXT_COURSE, 'contextid' => $context->id]); // Should probably make this simpler using some helper functions... but for now... @@ -453,7 +536,6 @@ public static function delete_data_for_users(approved_userlist $userlist) { JOIN {course} c ON rc.course = c.id JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel WHERE ctx.id = :contextid AND rc.userid $insql"; - $params = array_merge($inparams, ['contextid' => $context->id]); $DB->delete_records_select('local_recompletion_cc', "id $sql", $params); $sql = "SELECT rc.id @@ -461,7 +543,6 @@ public static function delete_data_for_users(approved_userlist $userlist) { JOIN {course} c ON rc.course = c.id JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel WHERE ctx.id = :contextid AND rc.userid $insql"; - $params = array_merge($inparams, ['contextid' => $context->id]); $DB->delete_records_select('local_recompletion_cc_cc', "id $sql", $params); $sql = "SELECT rc.id @@ -469,7 +550,6 @@ public static function delete_data_for_users(approved_userlist $userlist) { JOIN {course} c ON rc.course = c.id JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel WHERE ctx.id = :contextid AND rc.userid $insql"; - $params = array_merge($inparams, ['contextid' => $context->id]); $DB->delete_records_select('local_recompletion_cmc', "id $sql", $params); $sql = "SELECT rv.id @@ -477,7 +557,6 @@ public static function delete_data_for_users(approved_userlist $userlist) { JOIN {course} c ON rc.course = c.id JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel WHERE ctx.id = :contextid AND rv.userid $insql"; - $params = array_merge($inparams, ['contextid' => $context->id]); $DB->delete_records_select('local_recompletion_cmv', "id $sql", $params); $sql = "SELECT rc.id @@ -485,7 +564,6 @@ public static function delete_data_for_users(approved_userlist $userlist) { JOIN {course} c ON rc.course = c.id JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel WHERE ctx.id = :contextid AND rc.userid $insql"; - $params = array_merge($inparams, ['contextid' => $context->id]); $DB->delete_records_select('local_recompletion_qa', "id $sql", $params); $sql = "SELECT rc.id @@ -493,7 +571,6 @@ public static function delete_data_for_users(approved_userlist $userlist) { JOIN {course} c ON rc.course = c.id JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel WHERE ctx.id = :contextid AND rc.userid $insql"; - $params = array_merge($inparams, ['contextid' => $context->id]); $DB->delete_records_select('local_recompletion_qg', "id $sql", $params); $sql = "SELECT rc.id @@ -501,7 +578,6 @@ public static function delete_data_for_users(approved_userlist $userlist) { JOIN {course} c ON rc.course = c.id JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel WHERE ctx.id = :contextid AND rc.userid $insql"; - $params = array_merge($inparams, ['contextid' => $context->id]); $DB->delete_records_select('local_recompletion_sst', "id $sql", $params); $sql = "SELECT rc.id @@ -514,7 +590,6 @@ public static function delete_data_for_users(approved_userlist $userlist) { JOIN {course} c ON rc.course = c.id JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel WHERE ctx.id = :contextid AND rc.userid $insql"; - $params = array_merge($inparams, ['contextid' => $context->id]); $DB->delete_records_select('local_recompletion_qr', "id $sql", $params); $sql = "SELECT rc.id @@ -522,7 +597,42 @@ public static function delete_data_for_users(approved_userlist $userlist) { JOIN {course} c ON rc.course = c.id JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel WHERE ctx.id = :contextid AND rc.userid $insql"; - $params = array_merge($inparams, ['contextid' => $context->id]); $DB->delete_records_select('local_recompletion_cha', "id $sql", $params); + + $sql = "SELECT rc.id + FROM {local_recompletion_hvp} rc + JOIN {course} c ON rc.course = c.id + JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + WHERE ctx.id = :contextid AND rc.user_id $insql"; + $params = array_merge($inparams, ['contextlevel' => CONTEXT_COURSE, 'contextid' => $context->id]); + $DB->delete_records_select('local_recompletion_hvp', "id $sql", $params); + + foreach ($userids as $userid) { + self::delete_hp5_activity_records($context->instanceid, $userid); + } + } + + /** + * Wipe all attempt data for specific course and an optional user. + * + * @param int $courseid a course_id + * @param int|null $userid a user id + */ + private static function delete_hp5_activity_records(int $courseid, int $userid = null): void { + global $DB; + + $where = 'a.course = :course'; + $conditions = ['course' => $courseid]; + if (!empty($user)) { + $where .= ' AND a.userid = :userid'; + $conditions['userid'] = $userid; + } + + $DB->delete_records_select('local_recompletion_h5pr', "attemptid IN ( + SELECT a.id + FROM {local_recompletion_h5p} a + WHERE $where)", $conditions); + + $DB->delete_records('local_recompletion_h5p', $conditions); } } diff --git a/db/install.xml b/db/install.xml index a4ddb3e..bbb775c 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -325,5 +325,64 @@ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
diff --git a/db/upgrade.php b/db/upgrade.php index 0f8791b..1ddda85 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -694,5 +694,93 @@ function xmldb_local_recompletion_upgrade($oldversion) { } + if ($oldversion < 2023101800) { + // Define table local_recompletion_hvp to be created. + $table = new xmldb_table('local_recompletion_hvp'); + + // Adding fields to table local_recompletion_hvp. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('user_id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('hvp_id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('sub_content_id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('data_id', XMLDB_TYPE_CHAR, '127', null, null, null, null); + $table->add_field('data', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('preloaded', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null); + $table->add_field('delete_on_content_change', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null); + $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table local_recompletion_hvp. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for local_recompletion_hvp. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Recompletion savepoint reached. + upgrade_plugin_savepoint(true, 2023101800, 'local', 'recompletion'); + } + + if ($oldversion < 2023101900) { + + // Define table local_recompletion_h5p to be created. + $table = new xmldb_table('local_recompletion_h5p'); + + // Adding fields to table local_recompletion_h5p. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('originalattemptid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('h5pactivityid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '20', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('attempt', XMLDB_TYPE_INTEGER, '6', null, XMLDB_NOTNULL, null, '1'); + $table->add_field('rawscore', XMLDB_TYPE_INTEGER, '10', null, null, null, '0'); + $table->add_field('maxscore', XMLDB_TYPE_INTEGER, '10', null, null, null, '0'); + $table->add_field('scaled', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('duration', XMLDB_TYPE_INTEGER, '10', null, null, null, '0'); + $table->add_field('completion', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('success', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table local_recompletion_h5p. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for local_recompletion_h5p. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define table local_recompletion_h5pr to be created. + $table = new xmldb_table('local_recompletion_h5pr'); + + // Adding fields to table local_recompletion_h5pr. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('attemptid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('subcontent', XMLDB_TYPE_CHAR, '128', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('interactiontype', XMLDB_TYPE_CHAR, '128', null, null, null, null); + $table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('correctpattern', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('response', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('additionals', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('rawscore', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('maxscore', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('duration', XMLDB_TYPE_INTEGER, '10', null, null, null, '0'); + $table->add_field('completion', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('success', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table local_recompletion_h5pr. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for local_recompletion_h5pr. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Recompletion savepoint reached. + upgrade_plugin_savepoint(true, 2023101900, 'local', 'recompletion'); + } + return true; } diff --git a/lang/en/local_recompletion.php b/lang/en/local_recompletion.php index efd34f7..578bccc 100644 --- a/lang/en/local_recompletion.php +++ b/lang/en/local_recompletion.php @@ -143,6 +143,16 @@ $string['privacy:metadata:local_recompletion_qr'] = 'Recompletion Questionnaire response table'; $string['privacy:metadata:local_recompletion_ccert_is:emailed'] = 'If the customcert issue was emailed.'; $string['privacy:metadata:local_recompletion_ccert_is:timecreated'] = 'The time that the customcert issue created'; +$string['privacy:metadata:local_recompletion_hvp'] = 'Archive for H5P (mod_hvp) user data.'; +$string['privacy:metadata:local_recompletion_hvp:hvp_id'] = 'Id of hvp content'; +$string['privacy:metadata:local_recompletion_hvp:data'] = 'The actual user data that was stored.'; +$string['privacy:metadata:local_recompletion_h5p'] = 'Archive for H5P (mod_h5pactivity) attempt tracking information.'; +$string['privacy:metadata:local_recompletion_h5pr'] = 'Archive for H5P (mod_h5pactivity) attempt results tracking information.'; +$string['privacy:metadata:attempt'] = 'The attempt number'; +$string['privacy:metadata:rawscore'] = 'The score obtained'; +$string['privacy:metadata:timecreated'] = 'The time when the tracked element was created'; +$string['privacy:metadata:timemodified'] = 'The last time element was tracked'; +$string['privacy:metadata:userid'] = 'The ID of the user who accessed the H5P activity'; $string['noassigngradepermission'] = 'Your completion was reset, but this course contains an assignment that could not be reset, please ask your teacher to do this for you if required.'; $string['editcompletion'] = 'Edit course completion date'; $string['editcompletion_desc'] = 'Modify the course completion date for the following users:'; @@ -180,3 +190,9 @@ $string['archivecustomcertcertificates_help'] = 'Should issued custom certificates be archived?'; $string['recompletionunenrolenable'] = 'Reset completion on un-enrolment'; $string['recompletionunenrolenable_help'] = 'Enable to trigger completion reset on user un-enrolment'; +$string['hvpattempts'] = 'H5P attempts (mod_hvp)'; +$string['hvpattempts_help'] = 'How to handle H5P attempts within the course. If archive is selected, the old H5P attempts will be archived in the local_recompletion_hvp table.'; +$string['archivehvp'] = 'Archive old H5P attempts (mod_hvp)'; +$string['h5pattempts'] = 'H5P attempts (mod_h5pactivity)'; +$string['h5pattempts_help'] = 'How to handle H5P attempts within the course. If archive is selected, the old H5P attempts will be archived in the local_recompletion_h5p and local_recompletion_h5pr tables.'; +$string['archiveh5p'] = 'Archive old H5P attempts (mod_h5pactivity)'; diff --git a/locallib.php b/locallib.php index c30aaa7..8e69e52 100644 --- a/locallib.php +++ b/locallib.php @@ -38,7 +38,7 @@ function local_recompletion_get_supported_plugins() { $plugins = []; $files = scandir($CFG->dirroot. '/local/recompletion/classes/plugins'); foreach ($files as $file) { - $component = clean_param(str_replace('.php', '', $file), PARAM_ALPHAEXT); + $component = clean_param(str_replace('.php', '', $file), PARAM_ALPHANUMEXT); list($plugin, $type) = core_component::normalize_component($component); if (!core_component::is_valid_plugin_name($type, $plugin)) { diff --git a/tests/observer_test.php b/tests/observer_test.php index a50b8bd..501ade0 100644 --- a/tests/observer_test.php +++ b/tests/observer_test.php @@ -17,7 +17,7 @@ namespace local_recompletion; /** - * Class dialogue_test. + * Observer_test. * * @package local_recompletion * @copyright 2023 Dmitrii Metelkin diff --git a/tests/plugins/mod_h5pactivity_test.php b/tests/plugins/mod_h5pactivity_test.php new file mode 100644 index 0000000..5679615 --- /dev/null +++ b/tests/plugins/mod_h5pactivity_test.php @@ -0,0 +1,138 @@ +. + +namespace local_recompletion\plugins; + +/** + * Tests for mod_h5pactivity. + * + * @package local_recompletion + * @copyright 2023 Dmitrii Metelkin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \local_recompletion\plugins\mod_h5pactivity + */ +class mod_h5pactivity_test extends \advanced_testcase { + + /** + * Test mod_h5pactivity recompletion. + */ + public function test_mod_h5pactivity() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $h5p = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]); + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + // Check that tables are empty initially. + $this->assertFalse($DB->record_exists('h5pactivity_attempts', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertFalse($DB->record_exists('h5pactivity_attempts_results', [])); + $this->assertFalse($DB->record_exists('local_recompletion_h5p', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertFalse($DB->record_exists('local_recompletion_h5pr', [])); + + // Generate an attempt for user 1. + $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); + $generator->create_attempt(['h5pactivityid' => $h5p->id, 'userid' => $user1->id]); + + // Reset user 2 without any attempts to make sure that it doesn't explode. + mod_h5pactivity::reset($user2->id, $course, (object)['h5pactivity' => 1, 'archiveh5pactivity' => 1]); + + // Check that data is created in original tables. + $this->assertTrue($DB->record_exists('h5pactivity_attempts', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertTrue($DB->record_exists('h5pactivity_attempts_results', [])); + $this->assertFalse($DB->record_exists('local_recompletion_h5p', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertFalse($DB->record_exists('local_recompletion_h5pr', [])); + + // Reset data with "do nothing". + mod_h5pactivity::reset($user1->id, $course, (object)['h5pactivity' => 0, 'archiveh5pactivity' => 0]); + + // Check that nothing happened. + $this->assertTrue($DB->record_exists('h5pactivity_attempts', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertTrue($DB->record_exists('h5pactivity_attempts_results', [])); + $this->assertFalse($DB->record_exists('local_recompletion_h5p', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertFalse($DB->record_exists('local_recompletion_h5pr', [])); + + // Reset data with "do nothing", but archiving enabled. + mod_h5pactivity::reset($user1->id, $course, (object)['h5pactivity' => 0, 'archiveh5pactivity' => 1]); + + // Check that nothing happened. + $this->assertTrue($DB->record_exists('h5pactivity_attempts', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertTrue($DB->record_exists('h5pactivity_attempts_results', [])); + $this->assertFalse($DB->record_exists('local_recompletion_h5p', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertFalse($DB->record_exists('local_recompletion_h5pr', [])); + + // Reset data for user 2. + mod_h5pactivity::reset($user2->id, $course, (object)['h5pactivity' => 1, 'archiveh5pactivity' => 0]); + + // Check that nothing happened for user 1. + $this->assertTrue($DB->record_exists('h5pactivity_attempts', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertTrue($DB->record_exists('h5pactivity_attempts_results', [])); + $this->assertFalse($DB->record_exists('local_recompletion_h5p', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertFalse($DB->record_exists('local_recompletion_h5pr', [])); + + // Reset data without archiving. + mod_h5pactivity::reset($user1->id, $course, (object)['h5pactivity' => 1, 'archiveh5pactivity' => 0]); + + // Check data is gone from original tables. + $this->assertFalse($DB->record_exists('h5pactivity_attempts', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertFalse($DB->record_exists('h5pactivity_attempts_results', [])); + $this->assertFalse($DB->record_exists('local_recompletion_h5p', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertFalse($DB->record_exists('local_recompletion_h5pr', [])); + + // Create a new attempt for user 1. + $generator->create_attempt(['h5pactivityid' => $h5p->id, 'userid' => $user1->id]); + + // Check that data is created in original tables. + $this->assertTrue($DB->record_exists('h5pactivity_attempts', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertTrue($DB->record_exists('h5pactivity_attempts_results', [])); + $this->assertFalse($DB->record_exists('local_recompletion_h5p', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertFalse($DB->record_exists('local_recompletion_h5pr', [])); + + $originalattempt = $DB->get_record('h5pactivity_attempts', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id]); + $originalresult = $DB->get_record('h5pactivity_attempts_results', ['attemptid' => $originalattempt->id]); + + // Reset with archiving. + mod_h5pactivity::reset($user1->id, $course, (object)['h5pactivity' => 1, 'archiveh5pactivity' => 1]); + + // Check that data is created in archived tables. + $this->assertFalse($DB->record_exists('h5pactivity_attempts', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertFalse($DB->record_exists('h5pactivity_attempts_results', [])); + $this->assertTrue($DB->record_exists('local_recompletion_h5p', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id])); + $this->assertTrue($DB->record_exists('local_recompletion_h5pr', [])); + + // Validate data. + $attempt = $DB->get_record('local_recompletion_h5p', ['userid' => $user1->id, 'h5pactivityid' => $h5p->id]); + $result = $DB->get_record('local_recompletion_h5pr', ['attemptid' => $attempt->id]); + + $this->assertNotEmpty($result); + $this->assertEquals(0, $attempt->originalattemptid); + $this->assertEquals($course->id, $attempt->course); + $this->assertEquals($course->id, $result->course); + + foreach (['id', 'originalattemptid', 'course', 'attemptid'] as $field) { + unset($originalattempt->$field); + unset($attempt->$field); + unset($originalresult->$field); + unset($result->$field); + } + + $this->assertEquals($originalattempt, $attempt); + $this->assertEquals($originalresult, $result); + } +} diff --git a/version.php b/version.php index 3fd8e7a..36c79cf 100644 --- a/version.php +++ b/version.php @@ -24,8 +24,8 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2023101702; -$plugin->release = 2023101702; +$plugin->version = 2023101900; +$plugin->release = 2023101900; $plugin->maturity = MATURITY_STABLE; $plugin->requires = 2022112806; // Requires 4.1 $plugin->component = 'local_recompletion';