diff --git a/public/course/lib.php b/public/course/lib.php index 508a5ded148..a7add6de71e 100644 --- a/public/course/lib.php +++ b/public/course/lib.php @@ -746,6 +746,8 @@ function course_delete_module($cmid, $async = false) { "Cannot delete this module as the file mod/$modulename/lib.php is missing."); } + // Warning! there is very similar code in remove_course_contents. + // If you are changing this code, you probably need to change that too. $deleteinstancefunction = $modulename . '_delete_instance'; // Ensure the delete_instance function exists for this module. @@ -774,6 +776,9 @@ function course_delete_module($cmid, $async = false) { "Cannot delete the module $modulename (instance)."); } + // We delete the questions after the activity database is removed, + // because questions are referenced via question reference tables + // and cannot be deleted while the activities that use them still exist. question_delete_activity($cm); // Remove all module files in case modules forget to do that. diff --git a/public/lib/moodlelib.php b/public/lib/moodlelib.php index c3e44f9dfbf..b528e9543b0 100644 --- a/public/lib/moodlelib.php +++ b/public/lib/moodlelib.php @@ -4834,16 +4834,8 @@ function remove_course_contents($courseid, $showfeedback = true, ?array $options if ($instances) { foreach ($instances as $cm) { - if ($cm->id) { - // Delete activity context questions and question categories. - question_delete_activity($cm, coursedeletion: $coursedeletion); - // Notify the competency subsystem. - \core_competency\api::hook_course_module_deleted($cm); - - // Delete all tag instances associated with the instance of this module. - core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id); - core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id); - } + // Warning! there is very similar code in course_delete_module. + // If you are changing this code, you probably need to change that too. if (function_exists($moddelete)) { // This purges all module data in related tables, extra user prefs, settings, etc. $moddelete($cm->modinstance); @@ -4854,6 +4846,16 @@ function remove_course_contents($courseid, $showfeedback = true, ?array $options } if ($cm->id) { + // Delete activity context questions and question categories. + // We delete the questions after the activity database is removed, + // because questions are referenced via question reference tables + // and cannot be deleted while the activities that use them still exist. + question_delete_activity($cm, coursedeletion: $coursedeletion); + // Delete all tag instances associated with the instance of this module. + core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id); + core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id); + // Notify the competency subsystem. + \core_competency\api::hook_course_module_deleted($cm); // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition. context_helper::delete_instance(CONTEXT_MODULE, $cm->id); $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]); diff --git a/public/mod/quiz/tests/lib_test.php b/public/mod/quiz/tests/lib_test.php index 1cdde5fa2b0..d9fb41f37da 100644 --- a/public/mod/quiz/tests/lib_test.php +++ b/public/mod/quiz/tests/lib_test.php @@ -139,6 +139,47 @@ final class lib_test extends \advanced_testcase { $this->assertEquals(0, $DB->count_records('question_set_references', ['usingcontextid' => $context->id])); } + /** + * Test deleting a quiz when the course is deleted. + * + * @covers ::quiz_delete_instance + */ + public function test_quiz_when_delete_course(): void { + global $DB, $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Step 1: Create one course and a user with editing teacher capabilities. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $teacher = $USER; + $generator->enrol_user($teacher->id, $course->id, 'editingteacher'); + + // Create a quiz with questions in the first course. + $quiz = $this->create_test_quiz($course); + $context = \context_module::instance($quiz->cmid); + // Create questions. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); + $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); + + // Add to the quiz. + quiz_add_quiz_question($saq->id, $quiz); + // Delete the course. + delete_course($course, false); + + // Check that the question was deleted. + $this->assertFalse($DB->record_exists('question', ['id' => $saq->id])); + // Check that all the slots were removed. + $this->assertFalse($DB->record_exists('quiz_slots', ['quizid' => $quiz->id])); + // Check that the quiz was removed. + $this->assertFalse($DB->record_exists('quiz', ['id' => $quiz->id])); + // Check that any question references linked to this quiz are gone. + $this->assertFalse($DB->record_exists('question_references', ['usingcontextid' => $context->id])); + $this->assertFalse($DB->record_exists('question_set_references', ['usingcontextid' => $context->id])); + } + public function test_quiz_get_user_attempts(): void { global $DB; $this->resetAfterTest();