From 5ccca059d9d9b1e833175ffd95bb00094508f535 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Mon, 12 Jan 2026 14:48:33 +0100 Subject: [PATCH] MDL-87281 subsection: Add description cleanup links to settings page --- public/mod/subsection/cleandescriptions.php | 52 +++++++++++++ public/mod/subsection/lang/en/subsection.php | 22 ++++++ public/mod/subsection/settings.php | 73 +++++++++++++++++- .../subsection_clean_descriptions.feature | 46 +++++++++++ .../subsections_with_descriptions.mbz | Bin 0 -> 8177 bytes 5 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 public/mod/subsection/cleandescriptions.php create mode 100644 public/mod/subsection/tests/behat/subsection_clean_descriptions.feature create mode 100644 public/mod/subsection/tests/fixtures/subsections_with_descriptions.mbz diff --git a/public/mod/subsection/cleandescriptions.php b/public/mod/subsection/cleandescriptions.php new file mode 100644 index 00000000000..f5170e0323a --- /dev/null +++ b/public/mod/subsection/cleandescriptions.php @@ -0,0 +1,52 @@ +. + +/** + * Remove or migrate subsection descriptions + * + * @copyright 2026 Sara Arjona + * @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'); +} diff --git a/public/mod/subsection/lang/en/subsection.php b/public/mod/subsection/lang/en/subsection.php index 3893fbe662a..b82aff3bd7a 100644 --- a/public/mod/subsection/lang/en/subsection.php +++ b/public/mod/subsection/lang/en/subsection.php @@ -23,6 +23,28 @@ * @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. +

This site has {$a->count} subsection descriptions that are no longer visible to users.

+

You can choose to: +

+

'; +$string['deleteconfirmbutton'] = 'Delete all descriptions'; +$string['deleteconfirmtext'] = 'This will permanently delete {$a} subsection descriptions from the database.

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: {$a}'; +$string['descriptionsmigratedsuccess'] = 'The migration task for all subsection descriptions has been created. 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.

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'; diff --git a/public/mod/subsection/settings.php b/public/mod/subsection/settings.php index 9025c8122d6..41ef83e9d16 100644 --- a/public/mod/subsection/settings.php +++ b/public/mod/subsection/settings.php @@ -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, + ], + ), + )); + } } } diff --git a/public/mod/subsection/tests/behat/subsection_clean_descriptions.feature b/public/mod/subsection/tests/behat/subsection_clean_descriptions.feature new file mode 100644 index 00000000000..7aa8f8ad2c8 --- /dev/null +++ b/public/mod/subsection/tests/behat/subsection_clean_descriptions.feature @@ -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" diff --git a/public/mod/subsection/tests/fixtures/subsections_with_descriptions.mbz b/public/mod/subsection/tests/fixtures/subsections_with_descriptions.mbz new file mode 100644 index 0000000000000000000000000000000000000000..2550090994f318dc2abe255605157d5ee9537170 GIT binary patch literal 8177 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 literal 0 HcmV?d00001