From fa3fd42f0136d2df1eaa3aa8ac41cb6e1094137f Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Wed, 21 Jan 2026 08:08:02 +0100 Subject: [PATCH] MDL-87621 subsection: Add task to remove subsection descriptions --- .upgradenotes/MDL-87621-2026012107064366.yml | 9 + .../remove_subsection_descriptions_task.php | 72 +++++ ...move_subsection_descriptions_task_test.php | 276 ++++++++++++++++++ 3 files changed, 357 insertions(+) create mode 100644 .upgradenotes/MDL-87621-2026012107064366.yml create mode 100644 public/mod/subsection/classes/task/remove_subsection_descriptions_task.php create mode 100644 public/mod/subsection/tests/task/remove_subsection_descriptions_task_test.php diff --git a/.upgradenotes/MDL-87621-2026012107064366.yml b/.upgradenotes/MDL-87621-2026012107064366.yml new file mode 100644 index 00000000000..8adbb8796fd --- /dev/null +++ b/.upgradenotes/MDL-87621-2026012107064366.yml @@ -0,0 +1,9 @@ +issueNumber: MDL-87621 +notes: + mod_subsection: + - message: >- + A new ad-hoc task, `remove_existing_descriptions`, has been added. This + task will remove the descriptions for all existing subsection instances. + To ensure system stability, the task processes records in batches of 100 + and clears the original description upon completion. + type: improved diff --git a/public/mod/subsection/classes/task/remove_subsection_descriptions_task.php b/public/mod/subsection/classes/task/remove_subsection_descriptions_task.php new file mode 100644 index 00000000000..d4c47ebf431 --- /dev/null +++ b/public/mod/subsection/classes/task/remove_subsection_descriptions_task.php @@ -0,0 +1,72 @@ +. + +namespace mod_subsection\task; + +use core\task\adhoc_task; +use mod_subsection\manager; + +/** + * An ad-hoc task to remove existing descriptions from subsection instances. + * + * NOTE: + * - This task processes subsections in batches of 100 to reduce server overload. + * - It will be removed in Moodle 7.0. By then, the remaining descriptions will be removed. + * + * @package mod_subsection + * @copyright 2026 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class remove_subsection_descriptions_task extends adhoc_task { + /** + * Execute the task. + */ + public function execute(): void { + global $DB; + + // Process subsections in batches to reduce server overload. + $removedcount = 0; + $subsections = $DB->get_recordset_select( + table: 'course_sections', + select: 'component = :component AND summary != :empty', + params: ['component' => 'mod_subsection', 'empty' => ''], + limitnum: 100, + ); + $transaction = $DB->start_delegated_transaction(); + foreach ($subsections as $subsection) { + manager::create_from_id($subsection->course, $subsection->itemid)->clear_description(); + $removedcount++; + } + $transaction->allow_commit(); + if ($removedcount > 0) { + mtrace('Subsection descriptions removal task completed. Total removed subsection descriptions: ' . $removedcount); + } else { + mtrace('No subsection descriptions found to remove.'); + } + $subsections->close(); + + $pendingcount = $DB->count_records_select( + table: 'course_sections', + select: 'component = :component AND summary != :empty', + params: ['component' => 'mod_subsection', 'empty' => ''], + ); + if ($pendingcount > 0) { + $task = new self(); + \core\task\manager::queue_adhoc_task($task); + mtrace('Subsection descriptions removal task pending subsections: ' . $pendingcount . '. Scheduled new ad-hoc task.'); + } + } +} diff --git a/public/mod/subsection/tests/task/remove_subsection_descriptions_task_test.php b/public/mod/subsection/tests/task/remove_subsection_descriptions_task_test.php new file mode 100644 index 00000000000..e8f14d7a110 --- /dev/null +++ b/public/mod/subsection/tests/task/remove_subsection_descriptions_task_test.php @@ -0,0 +1,276 @@ +. + +namespace mod_subsection\task; + +/** + * Class containing unit tests for the remove existing subsection descriptions task. + * + * @package mod_subsection + * @copyright 2026 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\PHPUnit\Framework\Attributes\CoversClass(remove_subsection_descriptions_task::class)] +final class remove_subsection_descriptions_task_test extends \advanced_testcase { + /** + * Test remove_subsection_descriptions task. + */ + public function test_remove_subsection_descriptions(): void { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]); + // Add subsection with description. + $summarytext = 'Section with description'; + $this->getDataGenerator()->create_module( + 'subsection', + ['course' => $course->id, 'section' => 1, 'summary' => $summarytext], + ); + // Add another subsection without description. + $this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 1]); + + // Check only 1 subsection has description. + $this->assertEquals( + 2, + $DB->count_records_select( + 'course_sections', + 'course = :courseid AND component = \'mod_subsection\'', + ['courseid' => $course->id], + ), + ); + $this->assertEquals( + 1, + $DB->count_records_select( + 'course_sections', + 'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'', + ['courseid' => $course->id], + ), + ); + + // Run the task. + $task = new remove_subsection_descriptions_task(); + \core\task\manager::queue_adhoc_task($task); + ob_start(); + $this->runAdhocTasks(remove_subsection_descriptions_task::class); + $output = ob_get_contents(); + ob_end_clean(); + + // Check one subsection removed message shown. + $this->assertStringContainsString( + 'Subsection descriptions removal task completed. Total removed subsection descriptions: 1', + trim($output), + ); + // Check no subsection has description after running the task. + $this->assertEquals( + 0, + $DB->count_records_select( + 'course_sections', + 'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'', + ['courseid' => $course->id], + ), + ); + + // Check no subsections left to remove. + $task = new remove_subsection_descriptions_task(); + \core\task\manager::queue_adhoc_task($task); + ob_start(); + $this->runAdhocTasks(remove_subsection_descriptions_task::class); + $output = ob_get_contents(); + ob_end_clean(); + $this->assertStringContainsString( + 'No subsection descriptions found to remove.', + trim($output), + ); + } + + /** + * Test remove_subsection_descriptions task with attached files. + */ + public function test_remove_subsection_descriptions_with_files(): void { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]); + // Add subsection with file in the description. + $summarytext = 'Subsection text with link'; + $this->getDataGenerator()->create_module( + 'subsection', + ['course' => $course->id, 'section' => 1, 'summary' => $summarytext], + ); + $subsection = $DB->get_record( + 'course_sections', + ['course' => $course->id, 'section' => 2], + ); + $filerecord = [ + 'component' => 'course', + 'filearea' => 'section', + 'contextid' => \context_course::instance($course->id)->id, + 'itemid' => $subsection->id, + 'filename' => 'intro.txt', + 'filepath' => '/', + ]; + $fs = get_file_storage(); + $fs->create_file_from_string($filerecord, 'Test intro file'); + + // Check subsection has description with file. + $this->assertEquals( + 1, + $DB->count_records_select( + 'course_sections', + 'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'', + ['courseid' => $course->id], + ), + ); + $this->assertEquals( + 1, + $DB->count_records_select( + 'files', + 'component = :component AND filearea = :filearea AND filename = :filename', + [ + 'component' => 'course', + 'filearea' => 'section', + 'filename' => 'intro.txt', + ], + ), + ); + $this->assertEquals( + 0, + $DB->count_records_select( + 'files', + 'component = :component AND filearea = :filearea AND filename = :filename', + [ + 'component' => 'mod_label', + 'filearea' => 'intro', + 'filename' => 'intro.txt', + ], + ), + ); + + // Run the task. + $task = new remove_subsection_descriptions_task(); + \core\task\manager::queue_adhoc_task($task); + ob_start(); + $this->runAdhocTasks(remove_subsection_descriptions_task::class); + ob_end_clean(); + + // Check no subsection has description after running the task. + $this->assertEquals( + 0, + $DB->count_records_select( + 'course_sections', + 'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'', + ['courseid' => $course->id], + ), + ); + + // Check the file has been removed too. + $this->assertEquals( + 0, + $DB->count_records_select( + 'files', + 'component = :component AND filearea = :filearea AND filename = :filename', + [ + 'component' => 'course', + 'filearea' => 'section', + 'filename' => 'intro.txt', + ], + ), + ); + } + + /** + * Test remove_subsection_descriptions task reschedule when more than 100 subsections to process. + */ + public function test_remove_subsection_descriptions_rescheduletask(): void { + global $DB; + + if (!PHPUNIT_LONGTEST) { + $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); + } + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]); + // Add subsections with description. + for ($i = 0; $i < 101; $i++) { + $this->getDataGenerator()->create_module( + 'subsection', + ['course' => $course->id, 'section' => 1, 'summary' => 'Summary text'], + ); + } + + // Check all subsections have description. + $this->assertEquals( + 101, + $DB->count_records_select( + 'course_sections', + 'course = :courseid AND component = \'mod_subsection\'', + ['courseid' => $course->id], + ), + ); + + // Run the task. + $task = new remove_subsection_descriptions_task(); + \core\task\manager::queue_adhoc_task($task); + ob_start(); + $this->runAdhocTasks(remove_subsection_descriptions_task::class); + $output = ob_get_contents(); + ob_end_clean(); + + // Check subsection removed message shown. + $this->assertStringContainsString( + 'Subsection descriptions removal task completed. Total removed subsection descriptions: 100', + trim($output), + ); + $this->assertStringContainsString( + 'Subsection descriptions removal task pending subsections: 1. Scheduled new ad-hoc task.', + trim($output), + ); + // Check only 1 subsection keep having description after running the task. + $this->assertEquals( + 1, + $DB->count_records_select( + 'course_sections', + 'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'', + ['courseid' => $course->id], + ), + ); + + // Re-run the task to process the remaining subsection (it should have been queued by the previous run). + ob_start(); + $this->runAdhocTasks(remove_subsection_descriptions_task::class); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertStringContainsString( + 'Subsection descriptions removal task completed. Total removed subsection descriptions: 1', + trim($output), + ); + // Check no subsections keep having description after running the task. + $this->assertEquals( + 0, + $DB->count_records_select( + 'course_sections', + 'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'', + ['courseid' => $course->id], + ), + ); + } +}