Merge branch 'MDL-47787-master' of https://github.com/tbannister/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 30 Jun 2015 07:52:52 +0000 (09:52 +0200)
committerDan Poltawski <dan@moodle.com>
Tue, 30 Jun 2015 07:52:52 +0000 (09:52 +0200)
admin/cli/fix_orphaned_question_categories.php [new file with mode: 0644]
course/lib.php
course/tests/courselib_test.php
lib/questionlib.php
lib/tests/questionlib_test.php

diff --git a/admin/cli/fix_orphaned_question_categories.php b/admin/cli/fix_orphaned_question_categories.php
new file mode 100644 (file)
index 0000000..eeac16f
--- /dev/null
@@ -0,0 +1,103 @@
+<?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/>.
+
+/**
+ * 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();
index e49bdf1..7c89127 100644 (file)
@@ -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,
index a40bea6..27b2603 100644 (file)
@@ -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);
-
-        // Confirm the tag instances were added.
-        $this->assertEquals(3, $DB->count_records('tag_instance', array('component' => 'mod_assign', 'contextid' =>
-            $modcontext->id)));
-
-        // Verify event assignment event has been generated.
-        $eventcount = $DB->count_records('event', array('instance' => $assign->id, 'modulename' => 'assign'));
-        $this->assertEquals(1, $eventcount);
+        // 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.
+                $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' => $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);
-
-        // Verify the tag instances were deleted.
-        $this->assertEquals(0, $DB->count_records('tag_instance', array('component' => 'mod_assign', 'contextid' =>
-            $modcontext->id)));
+        // 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.
+                $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;
+        }
     }
 
     /**
index 912557c..2a2d106 100644 (file)
@@ -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.
  *
@@ -306,54 +352,52 @@ function question_delete_question($questionid) {
 }
 
 /**
- * All question categories and their questions are deleted for this course.
+ * All question categories and their questions are deleted for this context id.
  *
- * @param stdClass $course an object representing the activity
- * @param boolean $feedback to specify if the process must output a summary of its work
- * @return boolean
+ * @param object $contextid The contextid to delete question categories from
+ * @return array Feedback from deletes (if any)
  */
-function question_delete_course($course, $feedback=true) {
-    global $DB, $OUTPUT;
+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');
-    $coursecontext = context_course::instance($course->id);
-    $categoriescourse = $DB->get_records('question_categories',
-            array('contextid' => $coursecontext->id), 'parent', 'id, parent, name, contextid');
-
-    if ($categoriescourse) {
-
+    $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
-        $categoriescourse = sort_categories_by_tree($categoriescourse);
-
-        foreach ($categoriescourse as $category) {
+        $categories = sort_categories_by_tree($categories);
 
-            //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));
+        foreach ($categories as $category) {
+            question_category_delete_safe($category);
 
             //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);
-        }
+    }
+    return $feedbackdata;
+}
+
+/**
+ * All question categories and their questions are deleted for this course.
+ *
+ * @param stdClass $course an object representing the activity
+ * @param boolean $feedback to specify if the process must output a summary of its work
+ * @return boolean
+ */
+function question_delete_course($course, $feedback=true) {
+    $coursecontext = context_course::instance($course->id);
+    $feedbackdata = question_delete_context($coursecontext->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;
 }
@@ -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;
-
-    //To store feedback to be showed at the end of the process
-    $feedbackdata   = array();
+    global $DB;
 
-    //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];
index 94bb71e..6455616 100644 (file)
@@ -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));
+    }
 }