diff --git a/classes/task/process_duplicated_event_module_fix.php b/classes/task/process_duplicated_event_module_fix.php index d887e113..9d480530 100644 --- a/classes/task/process_duplicated_event_module_fix.php +++ b/classes/task/process_duplicated_event_module_fix.php @@ -76,7 +76,7 @@ public function execute() { $deleted = importvideosmanager::delete_import_mapping_record(['id' => $mapping->id]); $logmessage = "The import mapping record for event id: {$mapping->sourceeventid}" . " with ocworkflowid: {$mapping->ocworkflowid} & duplicated event id: {$data->duplicatedeventid}" . - " in course (ID: {$mapping->targetcourseid}) after {$mapping->attempcount} attempt(s) %s"; + " in course (ID: {$mapping->targetcourseid}) after {$mapping->attemptcount} attempt(s) %s"; $status = 'was successful'; if ($mapping->status == importvideosmanager::MAPPING_STATUS_FAILED) { $status = 'failed'; @@ -143,6 +143,7 @@ public function execute() { if ($mapping->status != importvideosmanager::MAPPING_STATUS_PENDING) { $this->set_next_run_time(strtotime("+1 min")); $this->set_custom_data($data); + throw new moodle_exception('importmapping_modulesfixtaskretry', 'block_opencast', '', 'performing cleanup repeat...'); } } } diff --git a/tests/backup_test.php b/tests/backup_test.php index f211f892..6e3b739c 100644 --- a/tests/backup_test.php +++ b/tests/backup_test.php @@ -31,6 +31,10 @@ use backup_controller; use block_opencast\local\apibridge; use block_opencast\task\process_duplicate_event; +use block_opencast\task\process_duplicated_event_module_fix; +use block_opencast\local\activitymodulemanager; +use block_opencast\local\ltimodulemanager; +use mod_opencast\local\opencasttype; use block_opencast_apibridge_testable; use coding_exception; use context_course; @@ -67,6 +71,21 @@ class backup_test extends advanced_testcase { /** @var string for the testcase, must NOT be a real server! */ private $apiurl = 'http://server.opencast.testcase'; + /** @var int the episode lti module id */ + private $episodeltimoduleid = 0; + /** @var int the series lti module id */ + private $seriesltimoduleid = 0; + /** @var string old series id */ + private $oldseriesid = '1234-5678-abcd-efgh'; + /** @var string old episode id */ + private $oldepisodeid = 'c0c8c98d-ad90-445c-b1be-be4944779a24'; + /** @var string new series id */ + private $newseriesid = '1234-1234-1234-1234'; + /** @var string new episode id */ + private $newepisodeid = '66c59fbc-77ca-401a-bb0f-3362bcd27b62'; + /** @var int new course id created after restore */ + private $newcourseid = 0; + public function setUp(): void { parent::setUp(); apibridge::set_testing(true); @@ -148,6 +167,10 @@ protected function restore_course($backupid, $courseid, $includevideos, $userid) $courseid = restore_dbops::create_new_course('Tmp', 'tmp', $categoryid); } + $this->newcourseid = $courseid; + // Simulate the faulty LTI and Activity module creation in new course. + $this->restore_modules($courseid); + $rc = new restore_controller($backupid, $courseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $userid, $target); $target == backup::TARGET_NEW_COURSE ?: $rc->get_plan()->get_setting('overwrite_conf')->set_value(true); $this->assertTrue($rc->execute_precheck()); @@ -166,6 +189,166 @@ protected function restore_course($backupid, $courseid, $includevideos, $userid) return $course; } + /** + * Simulate the creation of a new modules in new course with faulty data. + * + * @param int $courseid COurse id + */ + private function restore_modules($courseid) { + global $USER, $DB, $CFG; + require_once($CFG->dirroot.'/course/modlib.php'); + require_once($CFG->dirroot . '/mod/lti/locallib.php'); + + // Get plugin ids. + $activitypluginid = $DB->get_field('modules', 'id', ['name' => 'opencast']); + $ltipluginid = $DB->get_field('modules', 'id', ['name' => 'lti']); + + // Get new course object. + $course = \get_course($courseid); + + // Prepare Dummy data. + $seriesmoduletitle = 'Test Series module to repair'; + $episodemoduletitle = 'Test Episode module to repair'; + + // Build activity modinfo. + // Series activity module. + $activityseriesmoduleinfo = activitymodulemanager::build_activity_modinfo( + $activitypluginid, + $course, + 1, + $seriesmoduletitle, + 0, + $this->oldseriesid, + opencasttype::SERIES + ); + $addedactivityseriesmoduleinfo = add_moduleinfo($activityseriesmoduleinfo, $course); + $this->assertNotEmpty($addedactivityseriesmoduleinfo); + + // Episode activity module. + $activityepisodemoduleinfo = activitymodulemanager::build_activity_modinfo( + $activitypluginid, + $course, + 1, + $episodemoduletitle, + 0, + $this->oldepisodeid, + opencasttype::EPISODE + ); + $addedactivityepisodemoduleinfo = add_moduleinfo($activityepisodemoduleinfo, $course); + $this->assertNotEmpty($addedactivityepisodemoduleinfo); + + // Build LTI modules. + $sitetoolinfo = [ + 'baseurl' => rtrim($this->apiurl, '/') . '/lti', + 'createdby' => $USER->id, + 'course' => SITEID, + 'ltiversion' => 'LTI-1p0', + 'timecreated' => time(), + 'timemodified' => time(), + 'state' => LTI_TOOL_STATE_CONFIGURED, + 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER + ]; + $seriessitetool = $sitetoolinfo; + $seriessitetool['name'] = 'Opencast Series'; + $seriessitetoolrecord = (object) $seriessitetool; + $seriestoolid = $DB->insert_record('lti_types', $seriessitetoolrecord); + set_config('addltipreconfiguredtool_1', $seriestoolid, 'block_opencast'); + + $episodesitetool = $sitetoolinfo; + $episodesitetool['name'] = 'Opencast Episode'; + $episodesitetoolrecord = (object) $episodesitetool; + $episodetoolid = $DB->insert_record('lti_types', $episodesitetoolrecord); + set_config('addltiepisodepreconfiguredtool_1', $episodetoolid, 'block_opencast'); + + // Get the id of the installed LTI plugin. + // Series LTI module. + $ltiseriesmoduleinfo = ltimodulemanager::build_lti_modinfo( + $ltipluginid, + $course, + $seriesmoduletitle, + 0, + $seriestoolid, + 'series=' . $this->oldseriesid + ); + $addedltiseriesmoduleinfo = add_moduleinfo($ltiseriesmoduleinfo, $course); + $this->seriesltimoduleid = $addedltiseriesmoduleinfo->coursemodule; + $this->assertNotEmpty($addedltiseriesmoduleinfo); + + // Episode LTI module. + $ltiepisodemoduleinfo = ltimodulemanager::build_lti_modinfo( + $ltipluginid, + $course, + $episodemoduletitle, + 0, + $episodetoolid, + 'id=' . $this->oldepisodeid + ); + $addedltiepisodemoduleinfo = add_moduleinfo($ltiepisodemoduleinfo, $course); + $this->episodeltimoduleid = $addedltiepisodemoduleinfo->coursemodule; + $this->assertNotEmpty($addedltiepisodemoduleinfo); + } + + /** + * Checks whether the imported modules are fixed. + * + * @return boolean true if all modules are fixed, false otherwise + */ + private function check_fixed_modules() { + global $CFG, $DB; + + $seriesltimoduleisfixed = false; + $episodeltimoduleisfixed = false; + $episodeactivitymoduleisfixed = false; + $seriesactivitymoduleisfixed = false; + + // Require grade library. For an unknown reason, this is needed when updating the module. + require_once($CFG->libdir . '/gradelib.php'); + + $courseobject = get_course($this->newcourseid); + + // LTI modules. + if (!empty($this->seriesltimoduleid)) { + $seriesmoduleobject = get_coursemodule_from_id('lti', $this->seriesltimoduleid, $this->newcourseid); + list($unusedcm, $unusedcontext, $unusedmodule, $seriesmoduledata, $unusedcw) = + get_moduleinfo_data($seriesmoduleobject, $courseobject); + + if (strpos($seriesmoduledata->instructorcustomparameters, $this->newseriesid) !== false) { + $seriesltimoduleisfixed = true; + } + } + + if (!empty($this->episodeltimoduleid)) { + $episodemoduleobject = get_coursemodule_from_id('lti', $this->episodeltimoduleid, $this->newcourseid); + list($unusedcm, $unusedcontext, $unusedmodule, $episodemoduledata, $unusedcw) = + get_moduleinfo_data($episodemoduleobject, $courseobject); + + if (strpos($episodemoduledata->instructorcustomparameters, $this->newepisodeid) !== false) { + $episodeltimoduleisfixed = true; + } + } + + // Activity modules. + $seriesrecord = [ + 'ocinstanceid' => 1, + 'course' => $this->newcourseid, + 'type' => opencasttype::SERIES, + 'opencastid' => $this->newseriesid + ]; + $seriesactivitymoduleisfixed = $DB->record_exists('opencast', $seriesrecord); + + $episoderecord = [ + 'ocinstanceid' => 1, + 'course' => $this->newcourseid, + 'type' => opencasttype::EPISODE, + 'opencastid' => $this->newepisodeid + ]; + + $episodeactivitymoduleisfixed = $DB->record_exists('opencast', $episoderecord); + + return $seriesltimoduleisfixed && $episodeltimoduleisfixed && + $episodeactivitymoduleisfixed && $seriesactivitymoduleisfixed; + } + /** * Execute an adhoc task like via cron function. * @param stdClass $taskrecord @@ -195,6 +378,35 @@ private function execute_adhoc_task($taskrecord) { return ob_get_clean(); } + /** + * Execute an adhoc task to fix modules. + * @param stdClass $taskrecord + */ + private function execute_moduel_fix_adhoc_task($taskrecord) { + global $CFG; + + $task = new process_duplicated_event_module_fix(); + $task->set_id($taskrecord->id); + $task->set_custom_data_as_string($taskrecord->customdata); + + $cronlockfactory = lock_config::get_lock_factory('cron'); + $lock = $cronlockfactory->get_lock('adhoc_' . $taskrecord->id, 0); + $lock->release(); + + $task->set_lock($lock); + + $this->preventResetByRollback(); + ob_start(); + + // In Moodle 4.2 version cron_run_inner_adhoc_task is depricated. + if ($CFG->version < 2023042400) { + cron_run_inner_adhoc_task($task); + } else { + cron::run_inner_adhoc_task($task); + } + return ob_get_clean(); + } + /** * Check if task failed with error. * @param string $expectederrortextkey @@ -256,20 +468,21 @@ public function test_adhoctask_execution() { $mapping = new seriesmapping(); $mapping->set('ocinstanceid', 1); $mapping->set('courseid', $course->id); - $mapping->set('series', '1234-5678-abcd-efgh'); + $mapping->set('series', $this->oldseriesid); $mapping->set('isdefault', 1); $mapping->create(); // Setup simulation data for api. $apibridge->set_testdata('get_course_videos', $course->id, 'file'); - $apibridge->set_testdata('get_series_videos', '1234-5678-abcd-efgh', 'file'); + $apibridge->set_testdata('get_series_videos', $this->oldseriesid, 'file'); + $apibridge->set_testdata('get_series_by_identifier', $this->oldseriesid, 'file'); // Backup the course with videos. $backupid = $this->backup_course($course->id, true, $USER->id); // Prepare server simulation (via apibridge). $apibridge->set_testdata('supports_api_level', 'level', 'v1.1.0'); - $apibridge->set_testdata('create_course_series', 'newcourse', '1234-1234-1234-1234'); + $apibridge->set_testdata('create_course_series', 'newcourse', $this->newseriesid); // Restore the course with videos. $newcourse = $this->restore_course($backupid, 0, true, $USER->id); @@ -328,18 +541,39 @@ public function test_adhoctask_execution() { // Setup the mockuped workflow again. $apibridge->set_testdata('check_if_workflow_exists', $apibridge::DUPLICATE_WORKFLOW, true); - $apibridge->set_testdata('get_opencast_video', 'c0c8c98d-ad90-445c-b1be-be4944779a24', 'file'); + $apibridge->set_testdata('get_opencast_video', $this->oldepisodeid, 'file'); // The workflow exists now, but it is not started. $this->check_task_fail_with_error('error_workflow_not_started', 6); // Setup succesful start workflow in opencast system. - $apibridge->set_testdata('start_workflow', $apibridge::DUPLICATE_WORKFLOW, true); + // $apibridge->set_testdata('start_workflow', $apibridge::DUPLICATE_WORKFLOW, true); + $dummyworkflowid = 1234; + $apibridge->set_testdata('start_workflow', $apibridge::DUPLICATE_WORKFLOW, $dummyworkflowid); $taskrecords = $DB->get_records('task_adhoc', ['classname' => '\\block_opencast\\task\\process_duplicate_event']); $taskrecord = array_shift($taskrecords); $output = $this->execute_adhoc_task($taskrecord); + // Run adhoc task to fix modules. + $apibridge->set_testdata('get_duplicated_episodeid', $dummyworkflowid, $this->newepisodeid); + $modulefixtaskrecords = $DB->get_records('task_adhoc', ['classname' => '\\block_opencast\\task\\process_duplicated_event_module_fix']); + $modulefixtaskrecord = array_shift($modulefixtaskrecords); + $modulefixoutput = $this->execute_moduel_fix_adhoc_task($modulefixtaskrecord); + + // Run adhoc task again to go to cleaup process. + $modulefixtaskrecords = $DB->get_records('task_adhoc', ['classname' => '\\block_opencast\\task\\process_duplicated_event_module_fix']); + $modulefixtaskrecord = array_shift($modulefixtaskrecords); + $modulefixoutput = $this->execute_moduel_fix_adhoc_task($modulefixtaskrecord); + + // Check if the module fix adhoc task was successfully terminated. + $modulefixtaskrecords = $DB->get_records('task_adhoc', ['classname' => '\\block_opencast\\task\\process_duplicated_event_module_fix']); + $this->assertEquals(0, count($modulefixtaskrecords)); + + // Check if modules are fixed. + $modulesfixed = $this->check_fixed_modules(); + $this->assertEquals(true, $modulesfixed); + // Check that the task is deleted. $taskrecords = $DB->get_records('task_adhoc', ['classname' => '\\block_opencast\\task\\process_duplicate_event']); $this->assertEquals(0, count($taskrecords)); diff --git a/tests/fixtures/get_series_by_identifier.js b/tests/fixtures/get_series_by_identifier.js new file mode 100644 index 00000000..f740db30 --- /dev/null +++ b/tests/fixtures/get_series_by_identifier.js @@ -0,0 +1,2 @@ +{"identifier":"1234-5678-abcd-efgh","title":"My Test Series"} + diff --git a/tests/helper/apibridge_testable.php b/tests/helper/apibridge_testable.php index 29fd5191..2a40e1de 100644 --- a/tests/helper/apibridge_testable.php +++ b/tests/helper/apibridge_testable.php @@ -336,4 +336,43 @@ public function start_workflow($eventid, $duplicateworkflow, $params = [], $retu return $this->get_testdata('start_workflow', $duplicateworkflow); } + /** + * Simulates an API call to check, whether series exists in opencast system. + * + * @param int $seriesid + * @param bool $withacl If true, ACLs are included + * @return null|stdClass series if it exists in the opencast system. + */ + public function get_series_by_identifier($seriesid, bool $withacl = false) { + if (empty($seriesid)) { + return null; + } + + if ($value = $this->get_testdata('get_series_by_identifier', $seriesid)) { + return json_decode($value); + } + + return null; + } + + /** + * Simulates the check if the series ID exists in the Opencast system. + * @param string $seriesid Series id + * @return bool true, if the series exists. Otherwise false. + */ + public function ensure_series_is_valid($seriesid) { + return true; + } + + /** + * Simulate getting the episode id of the episode which was created in a duplication workflow. + * + * @param int $workflowid The workflow ID of the dupliation workflow. + * + * @return string|bool The episode ID, false if not found. + */ + public function get_duplicated_episodeid($workflowid) { + + return $value = $this->get_testdata('get_duplicated_episodeid', $workflowid); + } }