Merge branch 'MDL-87621-main' of https://github.com/sarjona/moodle
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -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],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user