diff --git a/admin/cli/fix_orphaned_question_categories.php b/admin/cli/fix_orphaned_question_categories.php new file mode 100644 index 00000000000..eeac16f3031 --- /dev/null +++ b/admin/cli/fix_orphaned_question_categories.php @@ -0,0 +1,103 @@ +. + +/** + * This script fixes orphaned question categories. + * + * Orphaned question categories have had their associated context deleted + * but the category itself remains in the database with an invalid context. + * + * @package core + * @subpackage cli + * @copyright 2013 Tyler Bannister (tyler.bannister@remote-learner.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); + +require(__DIR__.'/../../config.php'); +require_once($CFG->libdir.'/clilib.php'); +require_once($CFG->libdir.'/questionlib.php'); + +$long = array('fix' => false, 'help' => false); +$short = array('f' => 'fix', 'h' => 'help'); + +// Now get cli options. +list($options, $unrecognized) = cli_get_params($long, $short); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + +if ($options['help']) { + $help = + "Fix orphaned question categories. + + This scripts detects question categories that have had their + context deleted, thus severing them from their original purpose. + + This script will find the orphaned categories and delete the unused + questions in each category found. Used questions will not be + deleted, instead they will be moved to a rescue question category. + + Options: + -h, --help Print out this help + -f, --fix Fix the orphaned question categories in the DB. + If not specified only check and report problems to STDERR. + Example: + \$sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_question_categories.php + \$sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_question_categories.php -f + "; + + echo $help; + die; +} + +cli_heading('Checking for orphaned categories'); + + +$sql = 'SELECT qc.id, qc.contextid, qc.name + FROM {question_categories} qc + LEFT JOIN {context} c ON qc.contextid = c.id + WHERE c.id IS NULL'; +$categories = $DB->get_recordset_sql($sql); + +$i = 0; +foreach ($categories as $category) { + $i += 1; + echo "Found orphaned category: {$category->name}\n"; + if (!empty($options['fix'])) { + echo "Cleaning..."; + // One transaction per category. + $transaction = $DB->start_delegated_transaction(); + question_category_delete_safe($category); + $transaction->allow_commit(); + echo " Done!\n"; + } +} + +if (($i > 0) && !empty($options['fix'])) { + echo "Found and removed {$i} orphaned question categories\n"; +} else if ($i > 0) { + echo "Found {$i} orphaned question categories. To fix, run:\n"; + echo "\$sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_question_categories.php --fix\n"; +} else { + echo "No orphaned question categories found.\n"; +} + + +$categories->close(); diff --git a/course/lib.php b/course/lib.php index e49bdf1cc84..7c891279390 100644 --- a/course/lib.php +++ b/course/lib.php @@ -1633,7 +1633,7 @@ function set_coursemodule_visible($id, $visible) { } /** - * This function will handles the whole deletion process of a module. This includes calling + * This function will handle the whole deletion process of a module. This includes calling * the modules delete_instance function, deleting files, events, grades, conditional data, * the data in the course_module and course_sections table and adding a module deletion * event to the DB. @@ -1645,9 +1645,10 @@ function course_delete_module($cmid) { global $CFG, $DB; require_once($CFG->libdir.'/gradelib.php'); + require_once($CFG->libdir.'/questionlib.php'); require_once($CFG->dirroot.'/blog/lib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); - require_once($CFG->dirroot . '/tag/lib.php'); + require_once($CFG->dirroot.'/tag/lib.php'); // Get the course module. if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) { @@ -1679,6 +1680,9 @@ function course_delete_module($cmid) { "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php."); } + // Delete activity context questions and question categories. + question_delete_activity($cm); + // Call the delete_instance function, if it returns false throw an exception. if (!$deleteinstancefunction($cm->instance)) { throw new moodle_exception('cannotdeletemoduleinstance', '', '', null, diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php index a40bea6f647..27b26039bc5 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -539,7 +539,23 @@ class core_course_courselib_testcase extends advanced_testcase { return $moduleinfo; } + /** + * Data provider for course_delete module + * + * @return array An array of arrays contain test data + */ + public function provider_course_delete_module() { + $data = array(); + $data['assign'] = array('assign', array('duedate' => time())); + $data['quiz'] = array('quiz', array('duedate' => time())); + + return $data; + } + + /** + * Test the create_course function + */ public function test_create_course() { global $DB; $this->resetAfterTest(true); @@ -1476,51 +1492,92 @@ class core_course_courselib_testcase extends advanced_testcase { $this->assertEquals($pagecm->visible, 0); } - public function test_course_delete_module() { + /** + * Tests the function that deletes a course module + * + * @param string $type The type of module for the test + * @param array $options The options for the module creation + * @dataProvider provider_course_delete_module + */ + public function test_course_delete_module($type, $options) { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); // Create course and modules. $course = $this->getDataGenerator()->create_course(array('numsections' => 5)); + $options['course'] = $course->id; // Generate an assignment with due date (will generate a course event). - $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(), 'course' => $course->id)); + $module = $this->getDataGenerator()->create_module($type, $options); // Get the module context. - $modcontext = context_module::instance($assign->cmid); + $modcontext = context_module::instance($module->cmid); // Verify context exists. $this->assertInstanceOf('context_module', $modcontext); - // Add some tags to this assignment. - tag_set('assign', $assign->id, array('Tag 1', 'Tag 2', 'Tag 3'), 'mod_assign', $modcontext->id); + // Make module specific messes. + switch ($type) { + case 'assign': + // Add some tags to this assignment. + tag_set('assign', $module->id, array('Tag 1', 'Tag 2', 'Tag 3'), 'mod_assign', $modcontext->id); - // Confirm the tag instances were added. - $this->assertEquals(3, $DB->count_records('tag_instance', array('component' => 'mod_assign', 'contextid' => - $modcontext->id))); + // Confirm the tag instances were added. + $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id); + $this->assertEquals(3, $DB->count_records('tag_instance', $criteria)); - // Verify event assignment event has been generated. - $eventcount = $DB->count_records('event', array('instance' => $assign->id, 'modulename' => 'assign')); - $this->assertEquals(1, $eventcount); + // Verify event assignment event has been generated. + $eventcount = $DB->count_records('event', array('instance' => $module->id, 'modulename' => $type)); + $this->assertEquals(1, $eventcount); + + break; + case 'quiz': + $qgen = $this->getDataGenerator()->get_plugin_generator('core_question'); + $qcat = $qgen->create_question_category(array('contextid' => $modcontext->id)); + $questions = array( + $qgen->create_question('shortanswer', null, array('category' => $qcat->id)), + $qgen->create_question('shortanswer', null, array('category' => $qcat->id)), + ); + $this->expectOutputRegex('/'.get_string('unusedcategorydeleted', 'question').'/'); + break; + default: + break; + } // Run delete.. - course_delete_module($assign->cmid); + course_delete_module($module->cmid); // Verify the context has been removed. - $this->assertFalse(context_module::instance($assign->cmid, IGNORE_MISSING)); + $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING)); // Verify the course_module record has been deleted. - $cmcount = $DB->count_records('course_modules', array('id' => $assign->cmid)); + $cmcount = $DB->count_records('course_modules', array('id' => $module->cmid)); $this->assertEmpty($cmcount); - // Verify event assignment events have been removed. - $eventcount = $DB->count_records('event', array('instance' => $assign->id, 'modulename' => 'assign')); - $this->assertEmpty($eventcount); + // Test clean up of module specific messes. + switch ($type) { + case 'assign': + // Verify event assignment events have been removed. + $eventcount = $DB->count_records('event', array('instance' => $module->id, 'modulename' => $type)); + $this->assertEmpty($eventcount); - // Verify the tag instances were deleted. - $this->assertEquals(0, $DB->count_records('tag_instance', array('component' => 'mod_assign', 'contextid' => - $modcontext->id))); + // Verify the tag instances were deleted. + $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id); + $this->assertEquals(0, $DB->count_records('tag_instance', $criteria)); + break; + case 'quiz': + // Verify category deleted. + $criteria = array('contextid' => $modcontext->id); + $this->assertEquals(0, $DB->count_records('question_categories', $criteria)); + + // Verify questions deleted. + $criteria = array('category' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question', $criteria)); + break; + default: + break; + } } /** diff --git a/lib/questionlib.php b/lib/questionlib.php index 912557c0e06..2a2d106f86e 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -220,6 +220,52 @@ function match_grade_options($gradeoptionsfull, $grade, $matchgrades = 'error') } } +/** + * Category is about to be deleted, + * 1/ All questions are deleted for this question category. + * 2/ Any questions that can't be deleted are moved to a new category + * NOTE: this function is called from lib/db/upgrade.php + * + * @param object|coursecat $category course category object + */ +function question_category_delete_safe($category) { + global $DB; + $criteria = array('category' => $category->id); + $context = context::instance_by_id($category->contextid, IGNORE_MISSING); + $rescue = null; // See the code around the call to question_save_from_deletion. + + // Deal with any questions in the category. + if ($questions = $DB->get_records('question', $criteria, '', 'id,qtype')) { + + // Try to delete each question. + foreach ($questions as $question) { + question_delete_question($question->id); + } + + // Check to see if there were any questions that were kept because + // they are still in use somehow, even though quizzes in courses + // in this category will already have been deleted. This could + // happen, for example, if questions are added to a course, + // and then that course is moved to another category (MDL-14802). + $questionids = $DB->get_records_menu('question', $criteria, '', 'id, 1'); + if (!empty($questionids)) { + $parentcontextid = SYSCONTEXTID; + $name = get_string('unknown', 'question'); + if ($context !== false) { + $name = $context->get_context_name(); + $parentcontext = $context->get_parent_context(); + if ($parentcontext) { + $parentcontextid = $parentcontext->id; + } + } + question_save_from_deletion(array_keys($questionids), $parentcontextid, $name, $rescue); + } + } + + // Now delete the category. + $DB->delete_records('question_categories', array('id' => $category->id)); +} + /** * Tests whether any question in a category is used by any part of Moodle. * @@ -305,6 +351,36 @@ function question_delete_question($questionid) { question_bank::notify_question_edited($questionid); } +/** + * All question categories and their questions are deleted for this context id. + * + * @param object $contextid The contextid to delete question categories from + * @return array Feedback from deletes (if any) + */ +function question_delete_context($contextid) { + global $DB; + + //To store feedback to be showed at the end of the process + $feedbackdata = array(); + + //Cache some strings + $strcatdeleted = get_string('unusedcategorydeleted', 'question'); + $fields = 'id, parent, name, contextid'; + if ($categories = $DB->get_records('question_categories', array('contextid' => $contextid), 'parent', $fields)) { + //Sort categories following their tree (parent-child) relationships + //this will make the feedback more readable + $categories = sort_categories_by_tree($categories); + + foreach ($categories as $category) { + question_category_delete_safe($category); + + //Fill feedback + $feedbackdata[] = array($category->name, $strcatdeleted); + } + } + return $feedbackdata; +} + /** * All question categories and their questions are deleted for this course. * @@ -313,47 +389,15 @@ function question_delete_question($questionid) { * @return boolean */ function question_delete_course($course, $feedback=true) { - global $DB, $OUTPUT; - - //To store feedback to be showed at the end of the process - $feedbackdata = array(); - - //Cache some strings - $strcatdeleted = get_string('unusedcategorydeleted', 'question'); $coursecontext = context_course::instance($course->id); - $categoriescourse = $DB->get_records('question_categories', - array('contextid' => $coursecontext->id), 'parent', 'id, parent, name, contextid'); + $feedbackdata = question_delete_context($coursecontext->id, $feedback); - if ($categoriescourse) { - - //Sort categories following their tree (parent-child) relationships - //this will make the feedback more readable - $categoriescourse = sort_categories_by_tree($categoriescourse); - - foreach ($categoriescourse as $category) { - - //Delete it completely (questions and category itself) - //deleting questions - if ($questions = $DB->get_records('question', - array('category' => $category->id), '', 'id,qtype')) { - foreach ($questions as $question) { - question_delete_question($question->id); - } - $DB->delete_records("question", array("category" => $category->id)); - } - //delete the category - $DB->delete_records('question_categories', array('id' => $category->id)); - - //Fill feedback - $feedbackdata[] = array($category->name, $strcatdeleted); - } - //Inform about changes performed if feedback is enabled - if ($feedback) { - $table = new html_table(); - $table->head = array(get_string('category', 'question'), get_string('action')); - $table->data = $feedbackdata; - echo html_writer::table($table); - } + // Inform about changes performed if feedback is enabled. + if ($feedback && $feedbackdata) { + $table = new html_table(); + $table->head = array(get_string('category', 'question'), get_string('action')); + $table->data = $feedbackdata; + echo html_writer::table($table); } return true; } @@ -374,58 +418,10 @@ function question_delete_course_category($category, $newcategory, $feedback=true $context = context_coursecat::instance($category->id); if (empty($newcategory)) { - $feedbackdata = array(); // To store feedback to be showed at the end of the process - $rescueqcategory = null; // See the code around the call to question_save_from_deletion. - $strcatdeleted = get_string('unusedcategorydeleted', 'question'); - - // Loop over question categories. - if ($categories = $DB->get_records('question_categories', - array('contextid'=>$context->id), 'parent', 'id, parent, name')) { - foreach ($categories as $category) { - - // Deal with any questions in the category. - if ($questions = $DB->get_records('question', - array('category' => $category->id), '', 'id,qtype')) { - - // Try to delete each question. - foreach ($questions as $question) { - question_delete_question($question->id); - } - - // Check to see if there were any questions that were kept because - // they are still in use somehow, even though quizzes in courses - // in this category will already have been deleted. This could - // happen, for example, if questions are added to a course, - // and then that course is moved to another category (MDL-14802). - $questionids = $DB->get_records_menu('question', - array('category'=>$category->id), '', 'id, 1'); - if (!empty($questionids)) { - $parentcontextid = false; - $parentcontext = $context->get_parent_context(); - if ($parentcontext) { - $parentcontextid = $parentcontext->id; - } - if (!$rescueqcategory = question_save_from_deletion( - array_keys($questionids), $parentcontextid, - $context->get_context_name(), $rescueqcategory)) { - return false; - } - $feedbackdata[] = array($category->name, - get_string('questionsmovedto', 'question', $rescueqcategory->name)); - } - } - - // Now delete the category. - if (!$DB->delete_records('question_categories', array('id'=>$category->id))) { - return false; - } - $feedbackdata[] = array($category->name, $strcatdeleted); - - } // End loop over categories. - } + $feedbackdata = question_delete_context($context->id, $feedback); // Output feedback if requested. - if ($feedback and $feedbackdata) { + if ($feedback && $feedbackdata) { $table = new html_table(); $table->head = array(get_string('questioncategory', 'question'), get_string('action')); $table->data = $feedbackdata; @@ -460,7 +456,7 @@ function question_delete_course_category($category, $newcategory, $feedback=true * Enter description here... * * @param array $questionids of question ids - * @param object $newcontext the context to create the saved category in. + * @param object $newcontextid the context to create the saved category in. * @param string $oldplace a textual description of the think being deleted, * e.g. from get_context_name * @param object $newcategory @@ -497,44 +493,16 @@ function question_save_from_deletion($questionids, $newcontextid, $oldplace, * @return boolean */ function question_delete_activity($cm, $feedback=true) { - global $DB, $OUTPUT; + global $DB; - //To store feedback to be showed at the end of the process - $feedbackdata = array(); - - //Cache some strings - $strcatdeleted = get_string('unusedcategorydeleted', 'question'); $modcontext = context_module::instance($cm->id); - if ($categoriesmods = $DB->get_records('question_categories', - array('contextid' => $modcontext->id), 'parent', 'id, parent, name, contextid')) { - //Sort categories following their tree (parent-child) relationships - //this will make the feedback more readable - $categoriesmods = sort_categories_by_tree($categoriesmods); - - foreach ($categoriesmods as $category) { - - //Delete it completely (questions and category itself) - //deleting questions - if ($questions = $DB->get_records('question', - array('category' => $category->id), '', 'id,qtype')) { - foreach ($questions as $question) { - question_delete_question($question->id); - } - $DB->delete_records("question", array("category"=>$category->id)); - } - //delete the category - $DB->delete_records('question_categories', array('id'=>$category->id)); - - //Fill feedback - $feedbackdata[] = array($category->name, $strcatdeleted); - } - //Inform about changes performed if feedback is enabled - if ($feedback) { - $table = new html_table(); - $table->head = array(get_string('category', 'question'), get_string('action')); - $table->data = $feedbackdata; - echo html_writer::table($table); - } + $feedbackdata = question_delete_context($modcontext->id, $feedback); + // Inform about changes performed if feedback is enabled. + if ($feedback && $feedbackdata) { + $table = new html_table(); + $table->head = array(get_string('category', 'question'), get_string('action')); + $table->data = $feedbackdata; + echo html_writer::table($table); } return true; } @@ -1343,7 +1311,7 @@ function question_has_capability_on($question, $cap, $cachecat = -1) { if (!isset($categories[$question->category])) { if (!$categories[$question->category] = $DB->get_record('question_categories', array('id'=>$question->category))) { - print_error('invalidcategory', 'quiz'); + print_error('invalidcategory', 'question'); } } $category = $categories[$question->category]; diff --git a/lib/tests/questionlib_test.php b/lib/tests/questionlib_test.php index 94bb71e12d7..6455616711f 100644 --- a/lib/tests/questionlib_test.php +++ b/lib/tests/questionlib_test.php @@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/mod/quiz/locallib.php'); require_once($CFG->dirroot . '/tag/lib.php'); // Get the necessary files to perform backup and restore. @@ -58,6 +59,61 @@ class core_questionlib_testcase extends advanced_testcase { gc_collect_cycles(); } + /** + * Return true and false to test functions with feedback on and off. + * + * @return array Test data + */ + public function provider_feedback() { + return array( + 'Feedback test' => array(true), + 'No feedback test' => array(false) + ); + } + + /** + * Setup a course, a quiz, a question category and a question for testing. + * + * @param string $type The type of question category to create. + * @return array The created data objects + */ + public function setup_quiz_and_questions($type = 'module') { + // Create course category. + $category = $this->getDataGenerator()->create_category(); + + // Create course. + $course = $this->getDataGenerator()->create_course(array('numsections' => 5)); + + $options = array( + 'course' => $course->id, + 'duedate' => time(), + ); + + // Generate an assignment with due date (will generate a course event). + $quiz = $this->getDataGenerator()->create_module('quiz', $options); + + $qgen = $this->getDataGenerator()->get_plugin_generator('core_question'); + + if ('course' == $type) { + $context = context_course::instance($course->id); + } else if ('category' == $type) { + $context = context_coursecat::instance($category->id); + } else { + $context = context_module::instance($quiz->cmid); + } + + $qcat = $qgen->create_question_category(array('contextid' => $context->id)); + + $questions = array( + $qgen->create_question('shortanswer', null, array('category' => $qcat->id)), + $qgen->create_question('shortanswer', null, array('category' => $qcat->id)), + ); + + quiz_add_quiz_question($questions[0]->id, $quiz); + + return array($category, $course, $quiz, $qcat, $questions); + } + public function test_question_reorder_qtypes() { $this->assertEquals( array(0 => 't2', 1 => 't1', 2 => 't3'), @@ -205,4 +261,140 @@ class core_questionlib_testcase extends advanced_testcase { // Check that there are two questions in the restored to course's context. $this->assertEquals(2, $DB->count_records('question', array('category' => $restoredcategory->id))); } + + /** + * This function tests the question_category_delete_safe function. + */ + public function test_question_category_delete_safe() { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + + list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions(); + + question_category_delete_safe($qcat); + + // Verify category deleted. + $criteria = array('id' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question_categories', $criteria)); + + // Verify questions deleted or moved. + $criteria = array('category' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question', $criteria)); + + // Verify question not deleted. + $criteria = array('id' => $questions[0]->id); + $this->assertEquals(1, $DB->count_records('question', $criteria)); + } + + /** + * This function tests the question_delete_activity function. + * + * @param bool $feedback Whether to return feedback + * @dataProvider provider_feedback + */ + public function test_question_delete_activity($feedback) { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + + list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions(); + + $cm = get_coursemodule_from_instance('quiz', $quiz->id); + // Test that the feedback works. + if ($feedback) { + $this->expectOutputRegex('|'.get_string('unusedcategorydeleted', 'question').'|'); + } + question_delete_activity($cm, $feedback); + + // Verify category deleted. + $criteria = array('id' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question_categories', $criteria)); + + // Verify questions deleted or moved. + $criteria = array('category' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question', $criteria)); + } + + /** + * This function tests the question_delete_context function. + */ + public function test_question_delete_context() { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + + list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions(); + + // Get the module context id. + $result = question_delete_context($qcat->contextid); + + // Verify category deleted. + $criteria = array('id' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question_categories', $criteria)); + + // Verify questions deleted or moved. + $criteria = array('category' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question', $criteria)); + + // Test that the feedback works. + $expected[] = array($qcat->name, get_string('unusedcategorydeleted', 'question')); + $this->assertEquals($expected, $result); + } + + /** + * This function tests the question_delete_course function. + * + * @param bool $feedback Whether to return feedback + * @dataProvider provider_feedback + */ + public function test_question_delete_course($feedback) { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + + list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course'); + + // Test that the feedback works. + if ($feedback) { + $this->expectOutputRegex('|'.get_string('unusedcategorydeleted', 'question').'|'); + } + question_delete_course($course, $feedback); + + // Verify category deleted. + $criteria = array('id' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question_categories', $criteria)); + + // Verify questions deleted or moved. + $criteria = array('category' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question', $criteria)); + } + + /** + * This function tests the question_delete_course_category function. + * + * @param bool $feedback Whether to return feedback + * @dataProvider provider_feedback + */ + public function test_question_delete_course_category($feedback) { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + + list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category'); + + // Test that the feedback works. + if ($feedback) { + $this->expectOutputRegex('|'.get_string('unusedcategorydeleted', 'question').'|'); + } + question_delete_course_category($category, 0, $feedback); + + // Verify category deleted. + $criteria = array('id' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question_categories', $criteria)); + + // Verify questions deleted or moved. + $criteria = array('category' => $qcat->id); + $this->assertEquals(0, $DB->count_records('question', $criteria)); + } }