Merge branch 'MDL-87621-main' of https://github.com/sarjona/moodle

This commit is contained in:
Sara Arjona
2026-01-30 12:11:27 +07:00
committed by Huong Nguyen
17 changed files with 750 additions and 88 deletions
@@ -0,0 +1,8 @@
issueNumber: MDL-87621
notes:
mod_subsection:
- message: >-
When restoring backups, subsection descriptions are now ignored. This
change ensures that subsection do not incorrectly restore legacy
summary.
type: changed
@@ -0,0 +1,9 @@
issueNumber: MDL-87621
notes:
mod_subsection:
- message: >-
The subsection generator now includes support for the `summary` field.
This has been added specifically to test migration tool compatibility
and will be removed in Moodle 7.0. Developers should use this field only
for testing migration workflows.
type: improved
@@ -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
@@ -0,0 +1,8 @@
issueNumber: MDL-87621
notes:
mod_subsection:
- message: >-
The `manager::clear_description()` method has been added to remove
legacy data. When called, it deletes the description text associated
with a subsection and any files linked to that description.
type: improved
@@ -91,4 +91,14 @@ class restore_subsection_activity_task extends restore_activity_task {
return $rules;
}
/**
* This function, executed after all the tasks in the plan have been executed.
* This must be done here and not in normal execution steps because the subsection can be restored after the section.
*/
public function after_restore() {
// Clear subsection descriptions (they were removed from mod_subsection in Moodle 5.2).
mod_subsection\manager::create_from_id($this->get_courseid(), $this->get_activityid())
->clear_description();
}
}
+39
View File
@@ -205,4 +205,43 @@ class manager {
}
return $delegatedsection;
}
/**
* Deletes the subsection description and its associated files.
* Descriptions are no longer supported for subsections since Moodle 5.2.
*/
public function clear_description(): void {
global $DB;
// Find and delete the files from the subsection summary.
$fs = get_file_storage();
$coursesectionid = $DB->get_field(
'course_sections',
'id',
[
'component' => 'mod_subsection',
'itemid' => $this->instance->id,
],
);
$files = $fs->get_area_files(
contextid: \context_course::instance($this->cm->course)->id,
component: 'course',
filearea: 'section',
itemid: $coursesectionid,
);
foreach ($files as $file) {
$file->delete();
}
// Remove the subsection summary.
$DB->set_field(
'course_sections',
'summary',
'',
[
'component' => 'mod_subsection',
'itemid' => $this->instance->id,
],
);
}
}
@@ -0,0 +1,72 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <[email protected]>
* @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.');
}
}
}
+3 -9
View File
@@ -27,7 +27,6 @@ require_once('../../config.php');
require_admin();
$action = required_param('action', PARAM_ALPHA);
$count = optional_param('count', 0, PARAM_INT);
$return = new moodle_url('/admin/settings.php', ['section' => 'mod_subsection_settings']);
$PAGE->set_url('/mod/subsection/cleandescriptions.php');
@@ -35,14 +34,9 @@ $PAGE->set_context(context_system::instance());
require_sesskey();
if ($action === 'delete') {
// Remove all existing subsection descriptions.
$DB->set_field('course_sections', 'summary', '', ['component' => 'mod_subsection']);
redirect(
$return,
get_string('descriptionsdeletedsuccess', 'mod_subsection', $count),
null,
\core\output\notification::NOTIFY_SUCCESS
);
// Schedule the ad-hoc task to remove subsection descriptions.
\core\task\manager::queue_adhoc_task(new \mod_subsection\task\remove_subsection_descriptions_task(), true);
redirect($return);
} else if ($action === 'migrate') {
// Schedule the ad-hoc task to migrate subsection descriptions.
\core\task\manager::queue_adhoc_task(new \mod_subsection\task\migrate_subsection_descriptions_task(), true);
+2 -1
View File
@@ -37,7 +37,8 @@ $string['deleteconfirmbutton'] = 'Delete all descriptions';
$string['deleteconfirmtext'] = 'This will permanently delete {$a} subsection descriptions from the database.<br/><br/>You can\'t undo this. Are you sure you want to delete all descriptions?';
$string['deleteconfirmtitle'] = 'Delete all subsection descriptions?';
$string['deletelinktext'] = 'Delete descriptions';
$string['descriptionsdeletedsuccess'] = '{$a} subsection descriptions deleted.';
$string['descriptionsdeletedpending'] = 'Subsection descriptions waiting to be deleted: <strong>{$a}</strong>';
$string['descriptionsdeletedsuccess'] = '<strong>The removal task for all subsection descriptions has been created</strong>. This task will run in the background and may take a few minutes.';
$string['descriptionsmigratedpending'] = 'Subsection descriptions waiting to be migrated: <strong>{$a}</strong>';
$string['descriptionsmigratedsuccess'] = '<strong>The migration task for all subsection descriptions has been created</strong>. This task will run in the background and may take a few minutes.';
$string['invalidaction'] = 'Invalid action specified.';
+23 -4
View File
@@ -35,8 +35,9 @@ if ($hassiteconfig) {
select: 'component = :component AND summary != :empty',
params: ['component' => 'mod_subsection', 'empty' => ''],
);
$task = \core\task\manager::get_queued_adhoc_task_record(new \mod_subsection\task\migrate_subsection_descriptions_task());
if ($task) {
if (
\core\task\manager::get_queued_adhoc_task_record(new \mod_subsection\task\migrate_subsection_descriptions_task())
) {
// There is a pending migration task, show notification and pending count.
$notification = $OUTPUT->notification(
get_string('descriptionsmigratedsuccess', 'mod_subsection'),
@@ -52,6 +53,24 @@ if ($hassiteconfig) {
'',
new lang_string('descriptionsmigratedpending', 'mod_subsection', $count),
));
} else if (
\core\task\manager::get_queued_adhoc_task_record(new \mod_subsection\task\remove_subsection_descriptions_task())
) {
// There is a pending removal task, show notification and pending count.
$notification = $OUTPUT->notification(
get_string('descriptionsdeletedsuccess', 'mod_subsection'),
\core\output\notification::NOTIFY_SUCCESS,
);
$settings->add(new admin_setting_heading(
'removedescriptionsnotification',
'',
$notification,
));
$settings->add(new admin_setting_heading(
'pendingcleandescriptions',
'',
new lang_string('descriptionsdeletedpending', 'mod_subsection', $count),
));
} else if ($count > 0) {
// Show migration and deletion links.
$migrateaction = new \confirm_action(
@@ -61,7 +80,7 @@ if ($hassiteconfig) {
);
$migrateurl = new moodle_url(
'/mod/subsection/cleandescriptions.php',
['action' => 'migrate', 'count' => $count, 'sesskey' => sesskey()],
['action' => 'migrate', 'sesskey' => sesskey()],
);
$migratelink = $OUTPUT->action_link(
url: $migrateurl,
@@ -78,7 +97,7 @@ if ($hassiteconfig) {
);
$deleteurl = new moodle_url(
'/mod/subsection/cleandescriptions.php',
['action' => 'delete', 'count' => $count, 'sesskey' => sesskey()],
['action' => 'delete', 'sesskey' => sesskey()],
);
$deletelink = $OUTPUT->action_link(
url: $deleteurl,
@@ -1,4 +1,4 @@
@mod @mod_subsection @_file_upload
@mod @mod_subsection
Feature: Subsection clean descriptions
In order to manage subsection descriptions
As an administrator
@@ -8,19 +8,17 @@ Feature: Subsection clean descriptions
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following config values are set as admin:
| enableasyncbackup | 0 |
And I am on the "Course 1" "restore" page logged in as "admin"
And I press "Manage course backups"
And I upload "mod/subsection/tests/fixtures/subsections_with_descriptions.mbz" file to "Files" filemanager
And I press "Save changes"
And I restore "subsections_with_descriptions.mbz" backup into a new course using this options:
| Schema | Course name | Course 2 |
| Schema | Course short name | C2 |
| Course 2 | C2 | 0 |
And the following "activities" exist:
| activity | name | course | idnumber | section | summary |
| subsection | Subsection1 | C1 | subsection1 | 1 | Test Subsection1 summary |
| subsection | subsection3 | C1 | subsection3 | 1 | |
| subsection | subsection2 | C2 | subsection2 | 1 | Test Subsection2 summary |
@javascript
Scenario: Migrate subsection descriptions
Given I navigate to "Plugins > Activity modules > Subsection" in site administration
Given I log in as "admin"
And I navigate to "Plugins > Activity modules > Subsection" in site administration
And I should see "This site has 2 subsection descriptions that are no longer visible to users."
When I click on "Migrate descriptions" "link" in the "region-main" "region"
And I should see "This will migrate 2 subsection descriptions to Text and Media areas." in the "Migrate all subsection descriptions?" "dialogue"
@@ -36,11 +34,17 @@ Feature: Subsection clean descriptions
@javascript
Scenario: Delete subsection descriptions
Given I navigate to "Plugins > Activity modules > Subsection" in site administration
Given I log in as "admin"
And I navigate to "Plugins > Activity modules > Subsection" in site administration
And I should see "This site has 2 subsection descriptions that are no longer visible to users."
When I click on "Delete descriptions" "link" in the "region-main" "region"
And I should see "This will permanently delete 2 subsection descriptions from the database." in the "Delete all subsection descriptions?" "dialogue"
And I click on "Delete all descriptions" "button" in the "Delete all subsection descriptions?" "dialogue"
Then I should see "2 subsection descriptions deleted." in the "region-main" "region"
Then I should see "The removal task for all subsection descriptions has been created." in the "region-main" "region"
And I should see "Subsection descriptions waiting to be deleted: 2" in the "region-main" "region"
And I reload the page
And I should not see "Subsection pages and descriptions are no longer supported in Moodle 5.2"
And I should see "The removal task for all subsection descriptions has been created." in the "region-main" "region"
And I should see "Subsection descriptions waiting to be deleted: 2" in the "region-main" "region"
And I run all adhoc tasks
And I reload the page
And I should not see "Subsection descriptions waiting to be deleted:" in the "region-main" "region"
@@ -0,0 +1,25 @@
@mod @mod_subsection @_file_upload
Feature: Subsection restore backup with descriptions
In order to manage subsection descriptions
As an administrator
I want to be able to restore backups containing subsection descriptions ignoring them
Background:
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following config values are set as admin:
| enableasyncbackup | 0 |
And I am on the "Course 1" "restore" page logged in as "admin"
And I press "Manage course backups"
And I upload "mod/subsection/tests/fixtures/subsections_with_descriptions.mbz" file to "Files" filemanager
And I press "Save changes"
@javascript
Scenario: Check subsection descriptions are not restored
Given I restore "subsections_with_descriptions.mbz" backup into a new course using this options:
| Schema | Course name | Course 2 |
| Schema | Course short name | C2 |
When I navigate to "Plugins > Activity modules > Subsection" in site administration
# If this message appears, it means that the descriptions were restored.
Then I should not see "This site has 2 subsection descriptions that are no longer visible to users."
@@ -23,4 +23,29 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_subsection_generator extends testing_module_generator {
#[\Override]
public function create_instance($record = null, ?array $options = null): stdClass {
global $DB;
// Ensure the record can be modified without affecting calling code.
$record = (object)(array)$record;
// Create the subsection instance.
$instance = parent::create_instance($record, (array)$options);
// Update the delegated section summary if needed.
if (isset($record->summary)) {
$DB->set_field(
'course_sections',
'summary',
$record->summary,
[
'component' => 'mod_subsection',
'itemid' => $instance->id,
],
);
}
return $instance;
}
}
@@ -0,0 +1,67 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_subsection;
/**
* Generator tests class for mod_subsection.
*
* @package mod_subsection
* @category test
* @copyright 2026 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\PHPUnit\Framework\Attributes\CoversClass(mod_subsection_generator::class)]
final class generator_test extends \advanced_testcase {
/**
* Test on subsection creation.
*/
public function test_create_instance(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
// Create one subsection activity (empty summary by default).
$this->assertFalse($DB->record_exists('subsection', ['course' => $course->id]));
$activity1 = $this->getDataGenerator()->create_module('subsection', ['course' => $course]);
$records = $DB->get_records('subsection', ['course' => $course->id], 'id');
$this->assertEquals(1, count($records));
$this->assertTrue(array_key_exists($activity1->id, $records));
$section = $DB->get_record('course_sections', [
'component' => 'mod_subsection',
'itemid' => $activity1->id,
]);
$this->assertEquals('', $section->summary);
// Create another subsection activity with a specific summary.
$summarytext = 'This is a test summary';
$activity2 = $this->getDataGenerator()->create_module('subsection', [
'course' => $course,
'summary' => $summarytext,
]);
$records = $DB->get_records('subsection', ['course' => $course->id], 'id');
$this->assertEquals(2, count($records));
$this->assertTrue(array_key_exists($activity2->id, $records));
// Check that the delegated section summary has been correctly set.
$section = $DB->get_record('course_sections', [
'component' => 'mod_subsection',
'itemid' => $activity2->id,
]);
$this->assertEquals($summarytext, $section->summary);
}
}
@@ -126,4 +126,138 @@ final class manager_test extends \advanced_testcase {
];
}
/**
* Test clear_description.
*/
public function test_clear_description(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
// Add a couple of subsections with file in the description.
$module1 = $this->getDataGenerator()->create_module('subsection', [
'course' => $course->id,
'section' => 1,
'summary' => 'Subsection text with <a href="@@PLUGINFILE@@/intro1.txt">link</a>',
]);
$subsection1 = $DB->get_record(
'course_sections',
['course' => $course->id, 'itemid' => $module1->id],
);
$filerecord = [
'component' => 'course',
'filearea' => 'section',
'contextid' => \context_course::instance($course->id)->id,
'itemid' => $subsection1->id,
'filename' => 'intro1.txt',
'filepath' => '/',
];
$fs = get_file_storage();
$fs->create_file_from_string($filerecord, 'Test intro file');
$module2 = $this->getDataGenerator()->create_module('subsection', [
'course' => $course->id,
'section' => 1,
'summary' => 'Subsection text with <a href="@@PLUGINFILE@@/intro2.txt">link</a>',
]);
$subsection2 = $DB->get_record(
'course_sections',
['course' => $course->id, 'itemid' => $module2->id],
);
$filerecord = [
'component' => 'course',
'filearea' => 'section',
'contextid' => \context_course::instance($course->id)->id,
'itemid' => $subsection2->id,
'filename' => 'intro2.txt',
'filepath' => '/',
];
$fs = get_file_storage();
$fs->create_file_from_string($filerecord, 'Test intro file');
// Add one more subsection with description but no files.
$module3 = $this->getDataGenerator()->create_module('subsection', [
'course' => $course->id,
'section' => 1,
'summary' => 'Subsection text with no files',
]);
// Check subsections have descriptions.
$this->assertEquals(
3,
$DB->count_records_select(
'course_sections',
'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'',
['courseid' => $course->id],
),
);
$this->assertEquals(
2,
$DB->count_records_select(
'files',
'component = :component AND filearea = :filearea AND (filename != :filename)',
[
'component' => 'course',
'filearea' => 'section',
'filename' => '.',
],
),
);
// Clear the description for subsection with files.
$manager = manager::create_from_id($course->id, $module1->id);
$manager->clear_description();
// Check only subsection description and its files have been removed.
$this->assertEquals(
2,
$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(
1,
$DB->count_records_select(
'files',
'component = :component AND filearea = :filearea AND (filename != :filename)',
[
'component' => 'course',
'filearea' => 'section',
'filename' => '.',
],
),
);
// Clear the description for subsection without files.
$manager = manager::create_from_id($course->id, $module3->id);
$manager->clear_description();
// Check only subsection3 description has been removed.
$this->assertEquals(
1,
$DB->count_records_select(
'course_sections',
'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'',
['courseid' => $course->id],
),
);
// Check no files have been removed.
$this->assertEquals(
1,
$DB->count_records_select(
'files',
'component = :component AND filearea = :filearea AND (filename != :filename)',
[
'component' => 'course',
'filearea' => 'section',
'filename' => '.',
],
),
);
}
}
@@ -35,8 +35,12 @@ final class migrate_subsection_descriptions_task_test extends \advanced_testcase
$this->setAdminUser();
$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]);
$this->getDataGenerator()->create_module(
'subsection',
['course' => $course->id, 'section' => 1, 'summary' => $summarytext],
);
// Add forum to the subsection to test the order of the modules is preserved.
$this->getDataGenerator()->create_module(
'forum',
@@ -46,25 +50,9 @@ final class migrate_subsection_descriptions_task_test extends \advanced_testcase
'section' => 2,
],
);
// Add description to course sections and the subsection.
$DB->set_field(
'course_sections',
'summary',
$summarytext,
['course' => $course->id],
);
// Add another subsection without description.
$this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 1]);
// Check only 2 sections and 1 subsection have description.
$this->assertEquals(
3,
$DB->count_records_select(
'course_sections',
'course = :courseid AND summary != \'\'',
['courseid' => $course->id],
),
);
$this->assertEquals(
2,
$DB->count_records_select(
@@ -113,15 +101,6 @@ final class migrate_subsection_descriptions_task_test extends \advanced_testcase
'Subsection descriptions migration task completed. Total migrated subsections: 1',
trim($output),
);
// Check only 2 sections keep having description after running the task.
$this->assertEquals(
2,
$DB->count_records_select(
'course_sections',
'course = :courseid AND summary != \'\'',
['courseid' => $course->id],
),
);
// Check no subsection has description after running the task.
$this->assertEquals(
0,
@@ -184,19 +163,16 @@ final class migrate_subsection_descriptions_task_test extends \advanced_testcase
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]);
// Add subsection with file in the description.
$summarytext = 'Subsection text with <a href="@@PLUGINFILE@@/intro.txt">link</a>';
$this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 1]);
$this->getDataGenerator()->create_module(
'subsection',
['course' => $course->id, 'section' => 1, 'summary' => $summarytext],
);
$subsection = $DB->get_record(
'course_sections',
['course' => $course->id, 'section' => 2],
);
// Add description to the subsection.
$DB->set_field(
'course_sections',
'summary',
$summarytext,
['course' => $course->id, 'section' => $subsection->section],
);
$filerecord = [
'component' => 'course',
'filearea' => 'section',
@@ -321,24 +297,13 @@ final class migrate_subsection_descriptions_task_test extends \advanced_testcase
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]);
$summarytext = 'Section with description';
$this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 1]);
// Add description to course sections and the subsection.
$DB->set_field(
'course_sections',
'summary',
$summarytext,
['course' => $course->id],
);
// Check only 2 sections and 1 subsection have description.
$this->assertEquals(
3,
$DB->count_records_select(
'course_sections',
'course = :courseid AND summary != \'\'',
['courseid' => $course->id],
),
// Add subsection with description.
$this->getDataGenerator()->create_module(
'subsection',
['course' => $course->id, 'section' => 1, 'summary' => 'Summary text'],
);
// Check subsection has description.
$this->assertEquals(
1,
$DB->count_records_select(
@@ -433,18 +398,15 @@ final class migrate_subsection_descriptions_task_test extends \advanced_testcase
$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]);
$this->getDataGenerator()->create_module(
'subsection',
['course' => $course->id, 'section' => 1, 'summary' => 'Summary text'],
);
}
// Add description to course sections and subsections.
$DB->set_field(
'course_sections',
'summary',
'Section with description',
['course' => $course->id],
);
// Check 101 subsections have description.
// Check all subsections have description.
$this->assertEquals(
101,
$DB->count_records_select(
@@ -0,0 +1,276 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_subsection\task;
/**
* Class containing unit tests for the remove existing subsection descriptions task.
*
* @package mod_subsection
* @copyright 2026 Sara Arjona <[email protected]>
* @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 <a href="@@PLUGINFILE@@/intro.txt">link</a>';
$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],
),
);
}
}