From 9b9655c5637bb17adc8b6c648c8e5ca99b6c24cc Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Fri, 7 Aug 2026 13:33:24 +1000 Subject: [PATCH 1/3] issue#14 exclude module types from the back up --- README.md | 25 +++ classes/coursemigration.php | 5 + classes/output/coursemigration_table.php | 13 ++ classes/privacy/provider.php | 4 +- classes/task/course_backup.php | 16 ++ classes/upload_course_list.php | 23 +++ db/install.xml | 3 +- db/upgrade.php | 14 ++ example.csv | 8 +- lang/en/tool_coursemigration.php | 3 + tests/privacy/provider_test.php | 3 + tests/task/course_backup_test.php | 200 +++++++++++++++++++++++ tests/upload_course_list_test.php | 36 +++- version.php | 2 +- 14 files changed, 345 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9847922..a951bae 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,30 @@ These two scheduled tasks are created and run every 1 minute by default. * Create backup adhoc tasks for course migration * Create restore adhoc tasks for course migration +## Exclude certain activity types ## + +The `excluded_mods` feature allows you to exclude specific activity types from the +backup on a per-course basis. This is useful when certain modules (e.g. third-party plugins +like `turnitintooltwo`) should not be included in the migrated +course backup. + +To use this feature, add an optional `excluded_mods` column to your CSV file containing a +comma-separated list of activity types to exclude. For example: + +``` +courseid,categoryid,excluded_mods +101,5,turnitintooltwo +102,5,"turnitintooltwo, quiz" +103,5, +``` + +* If the `excluded_mods` column is omitted or left empty for a row, no activity type are excluded + and a standard backup is performed. +* Activity type names must match the plugin's short module name (e.g. `quiz`, `forum`, + `turnitintooltwo`), without the `mod_` prefix. +* An example CSV file with the `excluded_mods` column is available on the + `Upload course list` page. + ## Quick start ## * Install plugin on source and taget sites. * Create a shared folder/disk accessible to both sites in their local file system. @@ -74,6 +98,7 @@ These two scheduled tasks are created and run every 1 minute by default. * Create a web service token _Site administration > Plugins > Web services > Manage tokens_ * Add the web service token to the source site configuration. * Create a CSV file with course id and category id of courses to migrate. + * An optional `excluded_mods` column can be added to exclude specific activity types from backup. * An example csv file is available on the `Upload course list` page. * Upload the csv file at _Site administration > Plugins > Admin tools > Course migration > Upload course list_ diff --git a/classes/coursemigration.php b/classes/coursemigration.php index 452b0c9..2faadb1 100644 --- a/classes/coursemigration.php +++ b/classes/coursemigration.php @@ -115,6 +115,11 @@ protected static function define_properties() { 'null' => NULL_ALLOWED, 'default' => null, ], + 'excluded_mods' => [ + 'type' => PARAM_TEXT, + 'null' => NULL_ALLOWED, + 'default' => null, + ], 'error' => [ 'type' => PARAM_TEXT, 'null' => NULL_ALLOWED, diff --git a/classes/output/coursemigration_table.php b/classes/output/coursemigration_table.php index f4eebad..b7f38f4 100644 --- a/classes/output/coursemigration_table.php +++ b/classes/output/coursemigration_table.php @@ -62,6 +62,7 @@ public function __construct(moodle_url $url, stdClass $filters, int $page = 0, i 'destinationcategory', 'status', 'filename', + 'excluded_mods', 'timecreated', 'timemodified', 'error', @@ -74,6 +75,7 @@ public function __construct(moodle_url $url, stdClass $filters, int $page = 0, i get_string('destinationcategory', 'tool_coursemigration'), get_string('status'), get_string('filename', 'tool_coursemigration'), + get_string('excluded_mods', 'tool_coursemigration'), get_string('timecreated', 'tool_coursemigration'), get_string('timemodified', 'tool_coursemigration'), get_string('error'), @@ -88,6 +90,7 @@ public function __construct(moodle_url $url, stdClass $filters, int $page = 0, i $this->no_sorting('destinationcategory'); $this->no_sorting('status'); $this->no_sorting('filename'); + $this->no_sorting('excluded_mods'); $this->no_sorting('error'); $this->pageable(!empty($pagesize)); @@ -257,6 +260,16 @@ public function col_filename(stdClass $row): string { return $row->filename ?? ''; } + /** + * Excluded mods column. + * + * @param stdClass $row + * @return string + */ + public function col_excluded_mods(stdClass $row): string { + return $row->excluded_mods ?? ''; + } + /** * Error column. * diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 608f04b..015fe94 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -45,6 +45,7 @@ public static function get_metadata(collection $collection): collection { 'action' => 'privacy:metadata:tool_coursemigration:action', 'courseid' => 'privacy:metadata:tool_coursemigration:courseid', 'destinationcategoryid' => 'privacy:metadata:tool_coursemigration:destinationcategoryid', + 'excluded_mods' => 'privacy:metadata:tool_coursemigration:excluded_mods', 'usermodified' => 'privacy:metadata:tool_coursemigration:usermodified', ], 'privacy:metadata:tool_coursemigration' @@ -100,7 +101,7 @@ public static function export_user_data(approved_contextlist $contextlist) { 'tool_coursemigration', ['usermodified' => $user->id], '', - 'action,courseid,destinationcategoryid' + 'action,courseid,destinationcategoryid,excluded_mods' ); foreach ($recordset as $record) { @@ -108,6 +109,7 @@ public static function export_user_data(approved_contextlist $contextlist) { 'action' => $record->action, 'courseid' => $record->courseid, 'destinationcategoryid' => $record->destinationcategoryid, + 'excluded_mods' => $record->excluded_mods, ]; } $recordset->close(); diff --git a/classes/task/course_backup.php b/classes/task/course_backup.php index 312b6b2..5ca5073 100644 --- a/classes/task/course_backup.php +++ b/classes/task/course_backup.php @@ -26,6 +26,7 @@ namespace tool_coursemigration\task; use backup; +use backup_activity_task; use backup_controller; use backup_plan_dbops; use core\task\adhoc_task; @@ -103,6 +104,21 @@ public function execute() { $bc->get_plan()->get_setting('users')->set_value(0); $bc->get_plan()->get_setting('anonymize')->set_value(0); + // Apply module exclusions if specified. + $excludedmods = $coursemigration->get('excluded_mods'); + if (!empty($excludedmods)) { + $exclusions = array_map('trim', explode(',', $excludedmods)); + $exclusions = array_map('strtolower', array_filter($exclusions)); + foreach ($bc->get_plan()->get_tasks() as $task) { + if ($task instanceof backup_activity_task) { + $modulename = strtolower($task->get_modulename()); + if (in_array($modulename, $exclusions)) { + $task->get_setting('included')->set_value(0); + } + } + } + } + $format = $bc->get_format(); $type = $bc->get_type(); $id = $bc->get_id(); diff --git a/classes/upload_course_list.php b/classes/upload_course_list.php index c6920c7..27c4018 100644 --- a/classes/upload_course_list.php +++ b/classes/upload_course_list.php @@ -46,6 +46,13 @@ class upload_course_list { 'destinationcategoryid' => ['categoryid'], ]; + /** + * Optional columns that may be present in the CSV but are not required. + */ + const OPTIONAL_COLUMNS = [ + 'excluded_mods', + ]; + /** * Function to process upload courses form. * @@ -150,6 +157,14 @@ private static function process_row(array $row, array $fields, int $rownumber): } } + // Handle optional columns. + foreach (self::OPTIONAL_COLUMNS as $optcol) { + if (isset($fields[$optcol])) { + $value = trim($row[$fields[$optcol]['columnindex']]); + $data[$optcol] = !empty($value) ? $value : null; + } + } + return [$status, (object) $data, $message]; } @@ -191,6 +206,14 @@ private static function csv_required_columns(array $columns): ?array { } $message = empty($errors) ? null : implode(" AND ", $errors); + + // Detect optional columns present in the CSV. + foreach (self::OPTIONAL_COLUMNS as $optcol) { + if (in_array($optcol, $columns)) { + $fields[$optcol] = ['columnname' => $optcol, 'columnindex' => array_search($optcol, $columns)]; + } + } + return [$status, $message, $fields]; } diff --git a/db/install.xml b/db/install.xml index 6e96367..a2fa154 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -12,6 +12,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index 180d9e1..92baf57 100755 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -51,5 +51,19 @@ function xmldb_tool_coursemigration_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2023080800, 'tool', 'coursemigration'); } + if ($oldversion < 2023081602) { + // Define field excluded_mods to be added to tool_coursemigration. + $table = new xmldb_table('tool_coursemigration'); + $field = new xmldb_field('excluded_mods', XMLDB_TYPE_TEXT, null, null, null, null, null, 'filename'); + + // Conditionally launch add field excluded_mods. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Coursemigration savepoint reached. + upgrade_plugin_savepoint(true, 2023081602, 'tool', 'coursemigration'); + } + return true; } diff --git a/example.csv b/example.csv index 8964bd1..0cf58fd 100644 --- a/example.csv +++ b/example.csv @@ -1,4 +1,4 @@ -courseid,categoryid -1,2 -2,2 -3,4 +courseid,categoryid,excluded_mods +2,2, +3,2,"turnitintooltwo, quiz" +4,4,forum diff --git a/lang/en/tool_coursemigration.php b/lang/en/tool_coursemigration.php index d35429b..59c1941 100644 --- a/lang/en/tool_coursemigration.php +++ b/lang/en/tool_coursemigration.php @@ -30,11 +30,14 @@ $string['filename'] = 'Filename'; $string['filters'] = 'Filters'; $string['migrationid'] = 'ID'; +$string['excluded_mods'] = 'Excluded activity types'; +$string['excluded_mods_help'] = 'Comma-separated list of activity types to exclude from backup (e.g. turnitintooltwo, quiz).'; $string['pluginname'] = 'Course migration'; $string['privacy:metadata:tool_coursemigration'] = 'Data relating users for the tool coursemigration plugin'; $string['privacy:metadata:tool_coursemigration:action'] = 'The action type for course migration'; $string['privacy:metadata:tool_coursemigration:courseid'] = 'The source/destination courseid'; $string['privacy:metadata:tool_coursemigration:destinationcategoryid'] = 'The destination categoryid'; +$string['privacy:metadata:tool_coursemigration:excluded_mods'] = 'Comma-separated list of activity types excluded from backup'; $string['privacy:metadata:tool_coursemigration:usermodified'] = 'The ID of the user who modified the record'; $string['settings:awss3'] = 'Amazon S3 Settings'; $string['settings:awss3_bucket'] = 'Bucket'; diff --git a/tests/privacy/provider_test.php b/tests/privacy/provider_test.php index 4d0d34d..cea1afb 100644 --- a/tests/privacy/provider_test.php +++ b/tests/privacy/provider_test.php @@ -68,6 +68,7 @@ public function setup_test_data(): void { $data->action = coursemigration::ACTION_RESTORE; $data->courseid = 123456; $data->destinationcategoryid = 654321; + $data->excluded_mods = 'turnitintooltwo,quiz'; $coursemigration1 = new coursemigration(0, $data); $coursemigration1->save(); @@ -168,6 +169,7 @@ public function test_export_user_data() { $this->assertEquals(coursemigration::ACTION_RESTORE, $coursemigrations['action']); $this->assertEquals(123456, $coursemigrations['courseid']); $this->assertEquals(654321, $coursemigrations['destinationcategoryid']); + $this->assertEquals('turnitintooltwo,quiz', $coursemigrations['excluded_mods']); writer::reset(); $writer = writer::with_context($context); @@ -187,6 +189,7 @@ public function test_export_user_data() { $this->assertEquals(coursemigration::ACTION_BACKUP, $coursemigrations['action']); $this->assertEquals(112233, $coursemigrations['courseid']); $this->assertEquals(332211, $coursemigrations['destinationcategoryid']); + $this->assertNull($coursemigrations['excluded_mods']); } /** diff --git a/tests/task/course_backup_test.php b/tests/task/course_backup_test.php index a1521a7..916554f 100644 --- a/tests/task/course_backup_test.php +++ b/tests/task/course_backup_test.php @@ -43,6 +43,39 @@ * @covers \tool_coursemigration\task\course_backup */ class course_backup_test extends advanced_testcase { + /** + * Opens a Moodle backup file (.mbz) and returns the list of activity module + * names recorded in moodle_backup.xml. + * + * @param string $filename Backup filename (as stored in the coursemigration record). + * @return string[] Module type names, e.g. ['forum', 'quiz']. + */ + private function get_backup_modulenames(string $filename): array { + $directory = get_config('tool_coursemigration', 'directory'); + $filepath = rtrim($directory, '/') . '/' . $filename; + + $this->assertFileExists($filepath, "Backup file not found at: $filepath"); + + $fp = get_file_packer('application/vnd.moodle.backup'); + $tmpdir = make_backup_temp_directory('test_excluded_mods_' . uniqid()); + $extracted = $fp->extract_to_pathname($filepath, $tmpdir, ['moodle_backup.xml']); + $xmlfile = $tmpdir . '/moodle_backup.xml'; + + $this->assertTrue( + !empty($extracted) && is_readable($xmlfile), + "Could not extract moodle_backup.xml from: $filepath" + ); + + $xml = simplexml_load_file($xmlfile); + remove_dir($tmpdir); + + $modulenames = []; + foreach (($xml->information->contents->activities->activity ?? []) as $activity) { + $modulenames[] = strtolower((string) $activity->modulename); + } + return $modulenames; + } + /** * Test backup. */ @@ -377,6 +410,173 @@ public function test_restore_not_configured_backup_directory() { $this->assertEquals(get_string('event:backup_failed', 'tool_coursemigration'), $event->get_name()); } + /** + * Test backup with excluded_mods set — backup completes and excluded module is skipped. + */ + public function test_course_backup_with_excluded_mods(): void { + global $CFG; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(['fullname' => 'Test module exclusion']); + + // Create a forum and a quiz in the course. + $generator->create_module('forum', ['course' => $course->id]); + $generator->create_module('quiz', ['course' => $course->id]); + + // Mock restore api. + $mockedrestoreapi = $this->createMock(restore_api::class); + $mockedrestoreapi->method('request_restore')->willReturn(true); + restore_api_factory::set_restore_api($mockedrestoreapi); + + // Create coursemigration record with excluded_mods = 'quiz'. + $coursemigration = new coursemigration(0, (object)[ + 'action' => coursemigration::ACTION_BACKUP, + 'courseid' => $course->id, + 'destinationcategoryid' => 1, + 'status' => coursemigration::STATUS_NOT_STARTED, + 'excluded_mods' => 'quiz', + ]); + $coursemigration->save(); + + // Confirm excluded_mods value. + $this->assertEquals('quiz', $coursemigration->get('excluded_mods')); + + // Configure backup directory. + set_config('directory', $CFG->tempdir, 'tool_coursemigration'); + + $task = new course_backup(); + $customdata = ['coursemigrationid' => $coursemigration->get('id')]; + $task->set_custom_data($customdata); + manager::queue_adhoc_task($task); + ob_start(); + $task->execute(); + $output = ob_get_clean(); + + $this->assertStringContainsString('Backup completed.', $output); + + // Confirm the status is now completed. + $currentcoursemigration = coursemigration::get_record(['id' => $coursemigration->get('id')]); + $this->assertEquals(coursemigration::STATUS_COMPLETED, $currentcoursemigration->get('status')); + $this->assertNotEmpty($currentcoursemigration->get('filename')); + $this->assertStringStartsWith($coursemigration->get('id'), $currentcoursemigration->get('filename')); + restore_api_factory::reset_restore_api(); + + // Confirm quiz is absent from the backup and forum is present. + $modulenames = $this->get_backup_modulenames($currentcoursemigration->get('filename')); + $this->assertNotContains('quiz', $modulenames, 'quiz should have been excluded from the backup.'); + $this->assertContains('forum', $modulenames, 'forum should be present in the backup.'); + } + + /** + * Test backup with excluded_mods containing multiple modules (comma-separated). + */ + public function test_course_backup_with_multiple_excluded_modss(): void { + global $CFG; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(['fullname' => 'Test multi-exclusion course']); + + // Create modules in the course. + $generator->create_module('forum', ['course' => $course->id]); + $generator->create_module('quiz', ['course' => $course->id]); + + // Mock restore api. + $mockedrestoreapi = $this->createMock(restore_api::class); + $mockedrestoreapi->method('request_restore')->willReturn(true); + restore_api_factory::set_restore_api($mockedrestoreapi); + + // Exclude both forum and quiz (space after comma tests trim() handling in task). + $coursemigration = new coursemigration(0, (object)[ + 'action' => coursemigration::ACTION_BACKUP, + 'courseid' => $course->id, + 'destinationcategoryid' => 1, + 'status' => coursemigration::STATUS_NOT_STARTED, + 'excluded_mods' => 'forum, quiz', + ]); + $coursemigration->save(); + + // Confirm excluded_mods value is persisted correctly. + $this->assertEquals('forum, quiz', $coursemigration->get('excluded_mods')); + + set_config('directory', $CFG->tempdir, 'tool_coursemigration'); + + $task = new course_backup(); + $task->set_custom_data(['coursemigrationid' => $coursemigration->get('id')]); + manager::queue_adhoc_task($task); + ob_start(); + $task->execute(); + $output = ob_get_clean(); + + $this->assertStringContainsString('Backup completed.', $output); + + $currentcoursemigration = coursemigration::get_record(['id' => $coursemigration->get('id')]); + $this->assertEquals(coursemigration::STATUS_COMPLETED, $currentcoursemigration->get('status')); + $this->assertNotEmpty($currentcoursemigration->get('filename')); + restore_api_factory::reset_restore_api(); + + // Confirm both forum and quiz are absent from the backup. + $modulenames = $this->get_backup_modulenames($currentcoursemigration->get('filename')); + $this->assertNotContains('forum', $modulenames, 'forum should have been excluded from the backup.'); + $this->assertNotContains('quiz', $modulenames, 'quiz should have been excluded from the backup.'); + } + + /** + * Test backup with no excluded_mods — standard backup still works. + */ + public function test_course_backup_without_excluded_mods(): void { + global $CFG; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(['fullname' => 'Test no exclusion course']); + $generator->create_module('forum', ['course' => $course->id]); + + // Mock restore api. + $mockedrestoreapi = $this->createMock(restore_api::class); + $mockedrestoreapi->method('request_restore')->willReturn(true); + restore_api_factory::set_restore_api($mockedrestoreapi); + + // Create coursemigration record with no excluded_mods. + $coursemigration = new coursemigration(0, (object)[ + 'action' => coursemigration::ACTION_BACKUP, + 'courseid' => $course->id, + 'destinationcategoryid' => 1, + 'status' => coursemigration::STATUS_NOT_STARTED, + ]); + $coursemigration->save(); + + // Confirm excluded_mods. + $this->assertNull($coursemigration->get('excluded_mods')); + + set_config('directory', $CFG->tempdir, 'tool_coursemigration'); + + $task = new course_backup(); + $task->set_custom_data(['coursemigrationid' => $coursemigration->get('id')]); + manager::queue_adhoc_task($task); + ob_start(); + $task->execute(); + $output = ob_get_clean(); + + $this->assertStringContainsString('Backup completed.', $output); + + $currentcoursemigration = coursemigration::get_record(['id' => $coursemigration->get('id')]); + $this->assertEquals(coursemigration::STATUS_COMPLETED, $currentcoursemigration->get('status')); + $this->assertNotEmpty($currentcoursemigration->get('filename')); + restore_api_factory::reset_restore_api(); + + // Confirm forum is present in the backup (no exclusions applied). + $modulenames = $this->get_backup_modulenames($currentcoursemigration->get('filename')); + $this->assertContains('forum', $modulenames, 'forum should be present in the backup when no exclusion is set.'); + } + /** * Test delete after fail. */ diff --git a/tests/upload_course_list_test.php b/tests/upload_course_list_test.php index 12ff632..9ddb51f 100644 --- a/tests/upload_course_list_test.php +++ b/tests/upload_course_list_test.php @@ -68,6 +68,14 @@ public function test_csv_content($input, $expected, $dbrecords) { 'courseid' => $record[0], 'destinationcategoryid' => $record[1], ])); + // Verify excluded_mods value if provided in the test record. + if (array_key_exists(2, $record)) { + $dbrecord = $DB->get_record('tool_coursemigration', [ + 'courseid' => $record[0], + 'destinationcategoryid' => $record[1], + ]); + $this->assertEquals($record[2], $dbrecord->excluded_mods); + } } $this->assertEquals($expected, $results->get_result_message()); @@ -84,7 +92,7 @@ public function csv_content_provider(): array { "2,1"], 'expected' => "File successfully processed.\nTotal rows: 1\nSuccess: 1\n" . "Failed: 0\nErrors in CSV file: 0\n", - 'dbrecords' => [[2, 1] ], + 'dbrecords' => [[2, 1, null] ], ], "One row, valid url and category" => [ 'input' => ["url,categoryid", @@ -92,7 +100,7 @@ public function csv_content_provider(): array { 'expected' => "File successfully processed.\nTotal rows: 1\nSuccess: 1\n" . "Failed: 0\nErrors in CSV file: 0\n", - 'dbrecords' => [[2, 1] ], + 'dbrecords' => [[2, 1, null] ], ], "Four rows, one valid and three errors" => [ 'input' => ["courseid,categoryid", @@ -104,7 +112,7 @@ public function csv_content_provider(): array { "Failed: 3\nErrors in CSV file: 3\n" . "Non integer value for courseid found on row 2Non integer value" . " for categoryid found on row 3Non integer value for courseid found on row 4", - 'dbrecords' => [[2, 1] ], + 'dbrecords' => [[2, 1, null] ], ], "Invalid columns" => [ 'input' => ["invalid,invalid", @@ -113,6 +121,28 @@ public function csv_content_provider(): array { " categoryid as column headings", 'dbrecords' => [], ], + "One row, with excluded_mods column" => [ + 'input' => ["courseid,categoryid,excluded_mods", + "2,1,\"turnitintooltwo, quiz\""], + 'expected' => "File successfully processed.\nTotal rows: 1\nSuccess: 1\n" . + "Failed: 0\nErrors in CSV file: 0\n", + 'dbrecords' => [[2, 1, 'turnitintooltwo, quiz']], + ], + "One row, with empty excluded_mods column" => [ + 'input' => ["courseid,categoryid,excluded_mods", + "2,1,"], + 'expected' => "File successfully processed.\nTotal rows: 1\nSuccess: 1\n" . + "Failed: 0\nErrors in CSV file: 0\n", + 'dbrecords' => [[2, 1, null]], + ], + "Multiple rows, some with excluded_mods" => [ + 'input' => ["courseid,categoryid,excluded_mods", + "2,1,quiz", + "3,2,"], + 'expected' => "File successfully processed.\nTotal rows: 2\nSuccess: 2\n" . + "Failed: 0\nErrors in CSV file: 0\n", + 'dbrecords' => [[2, 1, 'quiz'], [3, 2, null]], + ], ]; } } diff --git a/version.php b/version.php index cba17a8..616954d 100644 --- a/version.php +++ b/version.php @@ -26,7 +26,7 @@ $plugin->component = 'tool_coursemigration'; $plugin->release = '0.1.0'; -$plugin->version = 2023081601; +$plugin->version = 2023081602; $plugin->requires = 2020061500; $plugin->maturity = MATURITY_STABLE; $plugin->supported = [39, 401]; From a51e9a87bcf49900341fd36b0548c76ea15e90f7 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Mon, 10 Aug 2026 16:51:30 +1000 Subject: [PATCH 2/3] issue#14 fix review issue --- README.md | 2 +- classes/output/coursemigration_table.php | 3 ++- tests/task/course_backup_test.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a951bae..231c0e4 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ courseid,categoryid,excluded_mods 103,5, ``` -* If the `excluded_mods` column is omitted or left empty for a row, no activity type are excluded +* If the `excluded_mods` column is omitted or left empty for a row, no activity type is excluded and a standard backup is performed. * Activity type names must match the plugin's short module name (e.g. `quiz`, `forum`, `turnitintooltwo`), without the `mod_` prefix. diff --git a/classes/output/coursemigration_table.php b/classes/output/coursemigration_table.php index b7f38f4..1b45852 100644 --- a/classes/output/coursemigration_table.php +++ b/classes/output/coursemigration_table.php @@ -267,7 +267,8 @@ public function col_filename(stdClass $row): string { * @return string */ public function col_excluded_mods(stdClass $row): string { - return $row->excluded_mods ?? ''; + $value = $row->excluded_mods ?? ''; + return $this->is_downloading() ? $value : s($value); } /** diff --git a/tests/task/course_backup_test.php b/tests/task/course_backup_test.php index 916554f..6804a82 100644 --- a/tests/task/course_backup_test.php +++ b/tests/task/course_backup_test.php @@ -473,7 +473,7 @@ public function test_course_backup_with_excluded_mods(): void { /** * Test backup with excluded_mods containing multiple modules (comma-separated). */ - public function test_course_backup_with_multiple_excluded_modss(): void { + public function test_course_backup_with_multiple_excluded_mods(): void { global $CFG; $this->resetAfterTest(); From 244e4013d6dd7ecbfd672515edc52fa2850cb598 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Tue, 11 Aug 2026 10:05:58 +1000 Subject: [PATCH 3/3] Fix ci error --- .github/workflows/ci.yml | 2 +- version.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dde9114..2a1be4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,4 +6,4 @@ jobs: ci: uses: catalyst/catalyst-moodle-workflows/.github/workflows/ci.yml@main with: - extra_plugin_runners: 'moodle-plugin-ci add-plugin catalyst/moodle-local_aws' + extra_plugin_runners: 'git clone --branch MOODLE_33_STABLE https://github.com/catalyst/moodle-local_aws.git $GITHUB_WORKSPACE/moodle/local/aws' diff --git a/version.php b/version.php index 616954d..8595015 100644 --- a/version.php +++ b/version.php @@ -31,5 +31,5 @@ $plugin->maturity = MATURITY_STABLE; $plugin->supported = [39, 401]; $plugin->dependencies = [ - 'local_aws' => ANY_VERSION, + 'local_aws' => 2024050800, ];