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

This commit is contained in:
Amaia Anabitarte
2026-01-21 15:36:16 +01:00
16 changed files with 894 additions and 198 deletions
@@ -1,8 +0,0 @@
issueNumber: MDL-87280
notes:
mod_subsection:
- message: >-
A new scheduled task, `remove_existing_descriptions`, has been added.
Once enabled, this task will remove the descriptions for all existing
subsection instances.
type: improved
@@ -0,0 +1,10 @@
issueNumber: MDL-87281
notes:
mod_subsection:
- message: >-
A new ad-hoc task, `migrate_subsection_descriptions_task`, has been
added. This task will migrate all existing subsection descriptions
into Text and media. To ensure system stability, the task processes
records in batches of 100 and clears the original description upon
successful migration.
type: improved
@@ -0,0 +1,9 @@
issueNumber: MDL-87281
notes:
core:
- message: >-
The __construct() method of the confirm_action class now accepts two
optional new parameters: `$title` (string) to set the dialogue's
heading, and `$dialogtype` (string) to specify the visual style of the
action ('delete' for displaying the danger button).
type: improved
@@ -34,8 +34,17 @@ class confirm_action extends component_action {
* @param string $callback Deprecated since 2.7
* @param string $continuelabel The string to use for he continue button
* @param string $cancellabel The string to use for the cancel button
* @param string|null $title The title of the confirmation dialog
* @param string|null $dialogtype The type of dialog (primary button style). E.g. 'delete'
*/
public function __construct($message, $callback = null, $continuelabel = null, $cancellabel = null) {
public function __construct(
$message,
$callback = null,
$continuelabel = null,
$cancellabel = null,
?string $title = null,
?string $dialogtype = null,
) {
if ($callback !== null) {
debugging(
'The callback argument to new confirm_action() has been deprecated.' .
@@ -48,6 +57,8 @@ class confirm_action extends component_action {
'message' => $message,
'continuelabel' => $continuelabel,
'cancellabel' => $cancellabel,
'title' => $title,
'dialogtype' => $dialogtype,
]);
}
}
+10 -2
View File
@@ -205,6 +205,8 @@ M.util.CollapsibleRegion.prototype.icon = null;
* @param {Object} [args.callbackargs] Any arguments to pass to the callback.
* @param {String} [args.cancellabel] The label to use on the cancel button.
* @param {String} [args.continuelabel] The label to use on the continue button.
* @param {String} [args.title] The title of the confirmation dialog.
* @param {String} [args.dialogtype] The type of dialog - 'delete' shows a delete/cancel dialog.
*/
M.util.show_confirm_dialog = (e, {
message,
@@ -212,6 +214,8 @@ M.util.show_confirm_dialog = (e, {
callback = null,
scope = null,
callbackargs = [],
title = null,
dialogtype = null,
} = {}) => {
if (e.preventDefault) {
e.preventDefault();
@@ -226,8 +230,12 @@ M.util.show_confirm_dialog = (e, {
scope = e.target;
}
Notification.saveCancelPromise(
Str.get_string('confirmation', 'admin'),
let method = 'saveCancelPromise';
if (dialogtype === 'delete') {
method = 'deleteCancelPromise';
}
Notification[method](
title || Str.get_string('confirmation', 'admin'),
message,
continuelabel || Str.get_string('yes', 'moodle'),
)
@@ -0,0 +1,130 @@
<?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 core_courseformat\formatactions;
/**
* A task to migrate to text and media and remove existing descriptions from subsection instances.
*
* NOTE:
* - This task requires that both the label and subsection modules are enabled.
* - It processes subsections in batches of 100 to reduce server overload.
* - This task will be removed in Moodle 7.0. By then, the remaining descriptions will be removed.
*
* @package mod_subsection
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class migrate_subsection_descriptions_task extends adhoc_task {
/**
* Execute the task.
*/
public function execute(): void {
global $CFG, $DB;
$enabledplugins = \core\plugininfo\mod::get_enabled_plugins();
if (!isset($enabledplugins['label']) || !isset($enabledplugins['subsection'])) {
// The label or subsection module is not enabled, nothing to do.
mtrace('Text and media area or Subsection module is not enabled. Skipping migration task.');
return;
}
require_once($CFG->dirroot . '/course/lib.php');
// Process subsections in batches to reduce server overload.
$migratedcount = 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) {
// Create a label with the subsection summary as intro.
$label = [
'modulename' => 'label',
'course' => $subsection->course,
'section' => $subsection->section,
'visible' => 1,
'introeditor' => [
'text' => $subsection->summary,
'format' => $subsection->summaryformat,
'itemid' => 0,
],
];
$label = \create_module((object) $label);
// Move the files from the subsection summary to the label intro.
$fs = get_file_storage();
$files = $fs->get_area_files(
contextid: \context_course::instance($subsection->course)->id,
component: 'course',
filearea: 'section',
itemid: $subsection->id,
);
foreach ($files as $file) {
$filerecord = [
'contextid' => \context_module::instance($label->coursemodule)->id,
'component' => 'mod_label',
'filearea' => 'intro',
'itemid' => 0,
'timemodified' => time(),
];
if ($fs->create_file_from_storedfile($filerecord, $file)) {
// Remove the file from the subsection area.
$file->delete();
}
}
// Move the label at the beginning of the subsection.
$section = get_fast_modinfo($subsection->course)->get_section_info($subsection->section);
$beforemod = explode(',', trim($section->sequence))[0] ?? null;
if ($beforemod) {
formatactions::cm($subsection->course)->move_before($label->coursemodule, $beforemod);
}
// Clear the summary field.
$DB->set_field(
table: 'course_sections',
newfield: 'summary',
newvalue: '',
conditions: ['id' => $subsection->id],
);
$migratedcount++;
}
$transaction->allow_commit();
if ($migratedcount > 0) {
mtrace('Subsection descriptions migration task completed. Total migrated subsections: ' . $migratedcount);
} else {
mtrace('No subsection descriptions found to migrate.');
}
$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 migration task pending subsections: ' . $pendingcount . '. Scheduled new ad-hoc task.');
}
}
}
@@ -1,46 +0,0 @@
<?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\scheduled_task;
/**
* A scheduled task to remove existing descriptions from subsection instances.
*
* @package mod_subsection
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class remove_existing_descriptions_task extends scheduled_task {
/**
* Return the task name.
*
* @return string The name of the task.
*/
public function get_name(): string {
return get_string('removeexistingdescriptions', 'mod_subsection');
}
/**
* Execute the task.
*/
public function execute(): void {
global $DB;
$DB->set_field('course_sections', 'summary', '', ['component' => 'mod_subsection']);
}
}
@@ -0,0 +1,52 @@
<?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/>.
/**
* Remove or migrate subsection descriptions
*
* @copyright 2026 Sara Arjona <[email protected]>
* @package mod_subsection
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
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');
$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
);
} 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);
redirect($return);
} else {
throw new moodle_exception('invalidaction', 'mod_subsection');
}
-39
View File
@@ -1,39 +0,0 @@
<?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/>.
/**
* Definition of Subsection scheduled tasks.
*
* @package mod_subsection
* @category task
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$tasks = [
[
'classname' => '\mod_subsection\task\remove_existing_descriptions_task',
'blocking' => 0,
'minute' => 'R',
'hour' => '0',
'day' => '*',
'dayofweek' => '*',
'month' => '*',
'disabled' => true,
],
];
+22 -1
View File
@@ -23,13 +23,34 @@
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['cleandescriptionsdetail'] = 'Subsection pages and descriptions are no longer supported in Moodle 5.2.
<p>This site has <strong>{$a->count} subsection descriptions</strong> that are no longer visible to users.</p>
<p>You can choose to:
<ul>
<li><strong>Migrate these descriptions to Text and Media areas</strong>.<br/> The Text and media areas will be displayed at the top of each subsection.<br/>{$a->migratelink}<br/><br/>
</li>
<li><strong>Delete these descriptions permanently</strong>.<br/> This will completely erase descriptions from the database.<br/>{$a->deletelink}
</li>
</ul>
</p>';
$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['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.';
$string['migrateconfirmbutton'] = 'Migrate all descriptions';
$string['migrateconfirmtext'] = 'This will migrate {$a} subsection descriptions to Text and Media areas.<br/><br/>You can\'t undo this. Are you sure you want to migrate all descriptions?';
$string['migrateconfirmtitle'] = 'Migrate all subsection descriptions?';
$string['migratelinktext'] = 'Migrate descriptions';
$string['modulename'] = 'Subsection';
$string['modulenameplural'] = 'Subsections';
$string['pluginadministration'] = 'Subsection administration';
$string['pluginname'] = 'Subsection';
$string['privacy:metadata'] = 'Subsection does not store any personal data';
$string['quickcreatename'] = 'New subsection';
$string['removeexistingdescriptions'] = 'Remove existing descriptions from subsection instances';
$string['subsection:addinstance'] = 'Add subsection';
$string['subsection:view'] = 'View subsection';
$string['subsectionname'] = 'Name';
+72 -1
View File
@@ -28,7 +28,78 @@ defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig) {
$settings = new admin_settingpage('mod_subsection_settings', new lang_string('pluginname', 'mod_subsection'));
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
if ($ADMIN->fulltree) {
// Add description cleanup and migration links.
$count = $DB->count_records_select(
table: 'course_sections',
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) {
// There is a pending migration task, show notification and pending count.
$notification = $OUTPUT->notification(
get_string('descriptionsmigratedsuccess', 'mod_subsection'),
\core\output\notification::NOTIFY_SUCCESS,
);
$settings->add(new admin_setting_heading(
'migratedescriptionsnotification',
'',
$notification,
));
$settings->add(new admin_setting_heading(
'pendingcleandescriptions',
'',
new lang_string('descriptionsmigratedpending', 'mod_subsection', $count),
));
} else if ($count > 0) {
// Show migration and deletion links.
$migrateaction = new \confirm_action(
message: get_string('migrateconfirmtext', 'mod_subsection', $count),
continuelabel: get_string('migrateconfirmbutton', 'mod_subsection'),
title: get_string('migrateconfirmtitle', 'mod_subsection'),
);
$migrateurl = new moodle_url(
'/mod/subsection/cleandescriptions.php',
['action' => 'migrate', 'count' => $count, 'sesskey' => sesskey()],
);
$migratelink = $OUTPUT->action_link(
url: $migrateurl,
text: get_string('migratelinktext', 'mod_subsection'),
action: $migrateaction,
attributes: ['class' => 'btn btn-secondary'],
);
$deleteaction = new \confirm_action(
message: get_string('deleteconfirmtext', 'mod_subsection', $count),
continuelabel: get_string('deleteconfirmbutton', 'mod_subsection'),
title: get_string('deleteconfirmtitle', 'mod_subsection'),
dialogtype: 'delete',
);
$deleteurl = new moodle_url(
'/mod/subsection/cleandescriptions.php',
['action' => 'delete', 'count' => $count, 'sesskey' => sesskey()],
);
$deletelink = $OUTPUT->action_link(
url: $deleteurl,
text: get_string('deletelinktext', 'mod_subsection'),
action: $deleteaction,
attributes: ['class' => 'btn btn-secondary'],
);
$settings->add(new admin_setting_heading(
'cleandescriptions',
'',
new lang_string(
'cleandescriptionsdetail',
'mod_subsection',
[
'count' => $count,
'migratelink' => $migratelink,
'deletelink' => $deletelink,
],
),
));
}
}
}
@@ -0,0 +1,46 @@
@mod @mod_subsection @_file_upload
Feature: Subsection clean descriptions
In order to manage subsection descriptions
As an administrator
I want to be able to delete or migrate subsection descriptions
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"
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 |
@javascript
Scenario: Migrate subsection descriptions
Given 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"
And I click on "Migrate all descriptions" "button" in the "Migrate all subsection descriptions?" "dialogue"
Then I should see "The migration task for all subsection descriptions has been created." in the "region-main" "region"
And I should see "Subsection descriptions waiting to be migrated: 2" in the "region-main" "region"
And I reload the page
And I should see "The migration task for all subsection descriptions has been created." in the "region-main" "region"
And I should see "Subsection descriptions waiting to be migrated: 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 migrated:" in the "region-main" "region"
@javascript
Scenario: Delete subsection descriptions
Given 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"
And I reload the page
And I should not see "Subsection pages and descriptions are no longer supported in Moodle 5.2"
@@ -0,0 +1,530 @@
<?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 migrate subsection descriptions task.
*
* @package mod_subsection
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\PHPUnit\Framework\Attributes\CoversClass(migrate_subsection_descriptions_task::class)]
final class migrate_subsection_descriptions_task_test extends \advanced_testcase {
/**
* Test migrate_subsection_descriptions task.
*/
public function test_migrate_subsection_descriptions(): void {
global $DB;
$this->resetAfterTest();
$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 forum to the subsection to test the order of the modules is preserved.
$this->getDataGenerator()->create_module(
'forum',
[
'course' => $course->id,
'name' => 'Forum in subsection',
'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(
'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],
),
);
$this->assertEquals(
0,
$DB->count_records_select(
'label',
'course = :courseid',
['courseid' => $course->id],
),
);
$cms = get_fast_modinfo($course->id)->get_cms();
// Check that the activities are in the expected initial order.
$this->assertEquals(
[
'Subsection 1',
'Subsection 2',
'Forum in subsection',
],
array_values(array_map(fn($cminfo) => $cminfo->name, $cms))
);
// Run the task.
$task = new migrate_subsection_descriptions_task();
\core\task\manager::queue_adhoc_task($task);
ob_start();
$this->runAdhocTasks(migrate_subsection_descriptions_task::class);
$output = ob_get_contents();
ob_end_clean();
// Check one subsection migrated message shown.
$this->assertStringContainsString(
'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,
$DB->count_records_select(
'course_sections',
'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'',
['courseid' => $course->id],
),
);
// Check text&media module created for the migrated description.
$this->assertEquals(
1,
$DB->count_records_select(
'label',
'course = :courseid',
['courseid' => $course->id],
),
);
$this->assertEquals(
$summarytext,
$DB->get_field_select(
'label',
'intro',
'course = :courseid',
['courseid' => $course->id],
),
);
$cms = get_fast_modinfo($course->id)->get_cms();
// Check that the label is in the expected position.
$this->assertEquals(
[
'Subsection 1',
'Subsection 2',
'label',
'Forum in subsection',
],
array_values(array_map(fn($cminfo) => $cminfo->name, $cms))
);
// Check no subsections left to migrate.
$task = new migrate_subsection_descriptions_task();
\core\task\manager::queue_adhoc_task($task);
ob_start();
$this->runAdhocTasks(migrate_subsection_descriptions_task::class);
$output = ob_get_contents();
ob_end_clean();
$this->assertStringContainsString(
'No subsection descriptions found to migrate.',
trim($output),
);
}
/**
* Test migrate_subsection_descriptions task with attached files.
*/
public function test_migrate_subsection_descriptions_with_files(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]);
$summarytext = 'Subsection text with <a href="@@PLUGINFILE@@/intro.txt">link</a>';
$this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 1]);
$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',
'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, and there is no label.
$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(
'label',
'course = :courseid',
['courseid' => $course->id],
),
);
$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 migrate_subsection_descriptions_task();
\core\task\manager::queue_adhoc_task($task);
ob_start();
$this->runAdhocTasks(migrate_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 text&media module created for the migrated description.
$this->assertEquals(
1,
$DB->count_records_select(
'label',
'course = :courseid',
['courseid' => $course->id],
),
);
$this->assertEquals(
$summarytext,
$DB->get_field_select(
'label',
'intro',
'course = :courseid',
['courseid' => $course->id],
),
);
// Check the file has been migrated too.
$this->assertEquals(
1,
$DB->count_records_select(
'files',
'component = :component AND filearea = :filearea AND filename = :filename',
[
'component' => 'mod_label',
'filearea' => 'intro',
'filename' => 'intro.txt',
],
),
);
$this->assertEquals(
0,
$DB->count_records_select(
'files',
'component = :component AND filearea = :filearea AND filename = :filename',
[
'component' => 'course',
'filearea' => 'section',
'filename' => 'intro.txt',
],
),
);
}
/**
* Test migrate_subsection_descriptions task when label or subsection module is not enabled.
*/
public function test_migrate_subsection_descriptions_modules_not_enabled(): void {
global $DB;
$this->resetAfterTest();
$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],
),
);
$this->assertEquals(
1,
$DB->count_records_select(
'course_sections',
'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'',
['courseid' => $course->id],
),
);
$this->assertEquals(
0,
$DB->count_records_select(
'label',
'course = :courseid',
['courseid' => $course->id],
),
);
// Disable label module.
\core\plugininfo\mod::enable_plugin('label', 0);
// Run the task.
$task = new migrate_subsection_descriptions_task();
\core\task\manager::queue_adhoc_task($task);
ob_start();
$this->runAdhocTasks(migrate_subsection_descriptions_task::class);
$output = ob_get_contents();
ob_end_clean();
// Check one subsection migrated message shown.
$this->assertStringContainsString(
'Text and media area or Subsection module is not enabled. Skipping migration task.',
trim($output),
);
// Check nothing has changed.
$this->assertEquals(
1,
$DB->count_records_select(
'course_sections',
'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'',
['courseid' => $course->id],
),
);
$this->assertEquals(
0,
$DB->count_records_select(
'label',
'course = :courseid',
['courseid' => $course->id],
),
);
// Enable label and disable subsection module.
\core\plugininfo\mod::enable_plugin('label', 1);
\core\plugininfo\mod::enable_plugin('subsection', 0);
// Run the task.
ob_start();
$task = new migrate_subsection_descriptions_task();
$task->execute();
ob_end_clean();
// Check nothing has changed.
$this->assertEquals(
1,
$DB->count_records_select(
'course_sections',
'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'',
['courseid' => $course->id],
),
);
$this->assertEquals(
0,
$DB->count_records_select(
'label',
'course = :courseid',
['courseid' => $course->id],
),
);
}
/**
* Test migrate_subsection_descriptions task reschedule when more than 100 subsections to process.
*/
public function test_migrate_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]);
for ($i = 0; $i < 101; $i++) {
$this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 1]);
}
// Add description to course sections and subsections.
$DB->set_field(
'course_sections',
'summary',
'Section with description',
['course' => $course->id],
);
// Check 101 subsections have description.
$this->assertEquals(
101,
$DB->count_records_select(
'course_sections',
'course = :courseid AND component = \'mod_subsection\'',
['courseid' => $course->id],
),
);
$this->assertEquals(
0,
$DB->count_records_select(
'label',
'course = :courseid',
['courseid' => $course->id],
),
);
// Run the task.
$task = new migrate_subsection_descriptions_task();
\core\task\manager::queue_adhoc_task($task);
ob_start();
$this->runAdhocTasks(migrate_subsection_descriptions_task::class);
$output = ob_get_contents();
ob_end_clean();
// Check subsection migrated message shown.
$this->assertStringContainsString(
'Subsection descriptions migration task completed. Total migrated subsections: 100',
trim($output),
);
$this->assertStringContainsString(
'Subsection descriptions migration 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],
),
);
// Check text&media module created for the migrated description subsections.
$this->assertEquals(
100,
$DB->count_records_select(
'label',
'course = :courseid',
['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(migrate_subsection_descriptions_task::class);
$output = ob_get_contents();
ob_end_clean();
$this->assertStringContainsString(
'Subsection descriptions migration task completed. Total migrated subsections: 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],
),
);
// Check text&media module created for the migrated description subsections.
$this->assertEquals(
101,
$DB->count_records_select(
'label',
'course = :courseid',
['courseid' => $course->id],
),
);
}
}
@@ -1,99 +0,0 @@
<?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 descriptions task.
*
* @package mod_subsection
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\PHPUnit\Framework\Attributes\CoversClass(remove_existing_descriptions_task::class)]
final class remove_existing_descriptions_task_test extends \advanced_testcase {
/**
* Test remove_existing_descriptions task.
*/
public function test_remove_existing_descriptions(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]);
$this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 1]);
// Add description to course sections and the subsection.
$DB->set_field(
'course_sections',
'summary',
'Section with description',
['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(
'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.
ob_start();
$task = new remove_existing_descriptions_task();
$task->execute();
ob_end_clean();
// 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,
$DB->count_records_select(
'course_sections',
'course = :courseid AND component = \'mod_subsection\' AND summary != \'\'',
['courseid' => $course->id],
),
);
}
}
+1 -1
View File
@@ -25,6 +25,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_subsection';
$plugin->version = 2025100601;
$plugin->version = 2025100602;
$plugin->requires = 2025092600;
$plugin->maturity = MATURITY_STABLE;