MDL-87356 qtype_random: orphaned random questions should be deleted

This patch reinstates the task from MDL-63260 and MDL-66273.
This commit is contained in:
James C
2025-12-04 23:00:49 +13:00
parent 299b171191
commit d36616757c
6 changed files with 290 additions and 1 deletions
+9
View File
@@ -5289,6 +5289,15 @@ class restore_create_categories_and_questions extends restore_structure_step {
$this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid);
}
if (
($data->qtype === 'random')
&& ($this->latestversion->status == \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN)
) {
// Ensure that this newly created question is considered by
// \qtype_random\task\remove_unused_questions.
$this->latestversion->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_DRAFT;
}
// Now store the question.
$newitemid = $DB->insert_record('question', $data);
$this->set_mapping('question', $oldid, $newitemid);
@@ -0,0 +1,98 @@
<?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/>.
/**
* A scheduled task to remove unneeded random questions.
*
* @package qtype_random
* @category task
* @copyright 2018 Bo Pierce <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qtype_random\task;
use core\task\manager;
/**
* A scheduled task to remove unneeded random questions.
*
* @copyright 2018 Bo Pierce <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class remove_unused_questions extends \core\task\scheduled_task {
/**
* Get a descriptive name for this task (shown to admins).
*
* @return string
*/
public function get_name(): string {
return get_string('taskunusedrandomscleanup', 'qtype_random');
}
/**
* Do the job.
*
* @return void
*/
public function execute() {
global $DB, $CFG;
require_once($CFG->libdir . '/questionlib.php');
// Confirm, that there is no restore in progress to make sure we do not
// clean up questions that have their quiz slots not restored yet.
$restoretasks = [
'\core\task\asynchronous_copy_task',
'\core\task\asynchronous_restore_task',
];
$running = manager::get_running_tasks();
foreach ($running as $task) {
if (in_array($task->classname, $restoretasks)) {
mtrace('Detected running async restore. Aborting the task.');
return;
}
}
// Find potentially unused random questions (up to 5000).
// Note, because we call question_delete_question below,
// the question will not actually be deleted if something else
// is using them, but nothing else in Moodle core uses qtype_random,
// and not many third-party plugins do.
$unusedrandomids = $DB->get_records_sql(
" SELECT DISTINCT q.id, 1
FROM {question} q
JOIN {question_versions} qv on qv.questionid = q.id
JOIN {question_bank_entries} qbe on qbe.id = qv.questionbankentryid
LEFT JOIN {question_references} qr on qr.questionbankentryid = qbe.id
WHERE qr.questionbankentryid IS NULL
AND q.qtype = ? AND qv.status <> ?",
['random', 'hidden'],
0,
5000
);
$count = 0;
foreach ($unusedrandomids as $unusedrandomid => $notused) {
question_delete_question($unusedrandomid);
// In case the question was not actually deleted (because it was in use somehow),
// it will be marked as hidden, so the query above will not return it again.
$count += 1;
}
mtrace('Cleaned up ' . $count . ' unused random questions.');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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 question/type/random scheduled tasks.
*
* @package qtype_random
* @category task
* @copyright 2018 Bo Pierce <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$tasks = [
[
'classname' => 'qtype_random\task\remove_unused_questions',
'blocking' => 0,
'minute' => 'R',
'hour' => '*',
'day' => '*',
'month' => '*',
'dayofweek' => '*',
],
];
@@ -0,0 +1,144 @@
<?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/>.
/**
* Tests of the scheduled task for cleaning up random questions.
*
* @package qtype_random
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qtype_random;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
/**
* Tests of the scheduled task for cleaning up random questions.
*
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \qtype_random\task\remove_unused_questions
*/
final class cleanup_task_test extends \advanced_testcase {
/**
* Test that remove_unused_questions deletes questions as appropriate.
*
* @covers ::execute
*/
public function test_cleanup_task_removes_unused_question(): void {
global $DB, $USER;
$this->resetAfterTest();
$this->setAdminUser();
// To do the test, we will be restoring a backup that contains 3 questions:
// A non-hidden broken question, a hidden broken question, and a non-broken question.
// Only the non-hidden broken question should be deleted,
// because questions are hidden when a delete was attempted but failed,
// and this is used to indicate we should skip over them in future deletes,
// to avoid continuingly attempting to delete undeletable questions.
// Extract backup file.
$backupid = 'test_cleanup_task_removes_unused_question';
$backuppath = make_backup_temp_directory($backupid);
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
__DIR__ . '/fixtures/broken_question_course.mbz',
$backuppath
);
// Do restore to new course with default settings.
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
$newcourseid = \restore_dbops::create_new_course('Broken Question Course', 'BQC', $categoryid);
$rc = new \restore_controller(
$backupid,
$newcourseid,
\backup::INTERACTIVE_NO,
\backup::MODE_GENERAL,
$USER->id,
\backup::TARGET_NEW_COURSE
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Check the hidden question was unhidden during the restore,
// to make it eligible for deletion.
$hiddenquestionid = $DB->get_field('question', 'id', ['name' => 'Random (BQC hidden broken question)']);
$this->assertNotEquals(
'hidden',
$DB->get_field('question_versions', 'status', ['questionid' => $hiddenquestionid])
);
// Revert the hidden question back to hidden, so we can check it isn't deleted.
$DB->set_field('question_versions', 'status', 'hidden', ['questionid' => $hiddenquestionid]);
// Run the scheduled task.
$task = new \qtype_random\task\remove_unused_questions();
$this->expectOutputString("Cleaned up 1 unused random questions.\n");
$task->execute();
// Verify.
$this->assertFalse(
$DB->record_exists('question', ['name' => 'Random (BQC non-hidden broken question)'])
);
$this->assertTrue(
$DB->record_exists('question', ['name' => 'Random (BQC hidden broken question)'])
);
$this->assertTrue(
$DB->record_exists('question', ['name' => 'BQC non-broken question'])
);
}
/**
* Test that remove_unused_questions aborts when there is a course restore in progress.
*
* @covers ::execute
*/
public function test_cleanup_task_checks_for_active_restores(): void {
$this->resetAfterTest();
// Get ready the tasks.
$cleanuptask = new \qtype_random\task\remove_unused_questions();
$restoretask = new \core\task\asynchronous_restore_task();
\core\task\manager::queue_adhoc_task($restoretask);
$copytask = new \core\task\asynchronous_copy_task();
\core\task\manager::queue_adhoc_task($copytask);
// Start the first adhoc task. This might be either restore or copy adhoc task.
$task1 = \core\task\manager::get_next_adhoc_task(time());
\core\task\manager::adhoc_task_starting($task1);
$cleanuptask->execute();
// Complete the first task and start the second one.
\core\task\manager::adhoc_task_complete($task1);
$task2 = \core\task\manager::get_next_adhoc_task(time());
\core\task\manager::adhoc_task_starting($task2);
$cleanuptask->execute();
// Complete the second adhoc task.
\core\task\manager::adhoc_task_complete($task2);
$cleanuptask->execute();
$aborted = 'Detected running async restore. Aborting the task.';
$completed = 'Cleaned up 0 unused random questions.';
$this->expectOutputRegex("/.*$aborted.*\s.*$aborted.*\s.*$completed.*/");
}
}
Binary file not shown.
+1 -1
View File
@@ -26,7 +26,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qtype_random';
$plugin->version = 2025041400;
$plugin->version = 2025041401;
$plugin->requires = 2025040800;