Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core;
  18  
  19  use question_bank;
  20  
  21  defined('MOODLE_INTERNAL') || die();
  22  
  23  global $CFG;
  24  
  25  require_once($CFG->libdir . '/questionlib.php');
  26  require_once($CFG->dirroot . '/mod/quiz/locallib.php');
  27  
  28  // Get the necessary files to perform backup and restore.
  29  require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
  30  require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
  31  
  32  /**
  33   * Unit tests for (some of) ../questionlib.php.
  34   *
  35   * @package    core_question
  36   * @category   test
  37   * @copyright  2006 The Open University
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class questionlib_test extends \advanced_testcase {
  41  
  42      /**
  43       * Test set up.
  44       *
  45       * This is executed before running any test in this file.
  46       */
  47      public function setUp(): void {
  48          $this->resetAfterTest();
  49      }
  50  
  51      /**
  52       * Setup a course, a quiz, a question category and a question for testing.
  53       *
  54       * @param string $type The type of question category to create.
  55       * @return array The created data objects
  56       */
  57      public function setup_quiz_and_questions($type = 'module') {
  58          // Create course category.
  59          $category = $this->getDataGenerator()->create_category();
  60  
  61          // Create course.
  62          $course = $this->getDataGenerator()->create_course(array(
  63              'numsections' => 5,
  64              'category' => $category->id
  65          ));
  66  
  67          $options = array(
  68              'course' => $course->id,
  69              'duedate' => time(),
  70          );
  71  
  72          // Generate an assignment with due date (will generate a course event).
  73          $quiz = $this->getDataGenerator()->create_module('quiz', $options);
  74  
  75          $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
  76  
  77          switch ($type) {
  78              case 'course':
  79                  $context = \context_course::instance($course->id);
  80                  break;
  81  
  82              case 'category':
  83                  $context = \context_coursecat::instance($category->id);
  84                  break;
  85  
  86              case 'system':
  87                  $context = \context_system::instance();
  88                  break;
  89  
  90              default:
  91                  $context = \context_module::instance($quiz->cmid);
  92                  break;
  93          }
  94  
  95          $qcat = $qgen->create_question_category(array('contextid' => $context->id));
  96  
  97          $questions = array(
  98                  $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
  99                  $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
 100          );
 101  
 102          quiz_add_quiz_question($questions[0]->id, $quiz);
 103  
 104          return array($category, $course, $quiz, $qcat, $questions);
 105      }
 106  
 107      public function test_question_reorder_qtypes() {
 108          $this->assertEquals(
 109              array(0 => 't2', 1 => 't1', 2 => 't3'),
 110              question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't1', +1));
 111          $this->assertEquals(
 112              array(0 => 't1', 1 => 't2', 2 => 't3'),
 113              question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't1', -1));
 114          $this->assertEquals(
 115              array(0 => 't2', 1 => 't1', 2 => 't3'),
 116              question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't2', -1));
 117          $this->assertEquals(
 118              array(0 => 't1', 1 => 't2', 2 => 't3'),
 119              question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't3', +1));
 120          $this->assertEquals(
 121              array(0 => 't1', 1 => 't2', 2 => 't3'),
 122              question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 'missing', +1));
 123      }
 124  
 125      public function test_match_grade_options() {
 126          $gradeoptions = question_bank::fraction_options_full();
 127  
 128          $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.3333333, 'error'));
 129          $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.333333, 'error'));
 130          $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33333, 'error'));
 131          $this->assertFalse(match_grade_options($gradeoptions, 0.3333, 'error'));
 132  
 133          $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.3333333, 'nearest'));
 134          $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.333333, 'nearest'));
 135          $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33333, 'nearest'));
 136          $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33, 'nearest'));
 137  
 138          $this->assertEquals(-0.1428571, match_grade_options($gradeoptions, -0.15, 'nearest'));
 139      }
 140  
 141      /**
 142       * This function tests that the functions responsible for moving questions to
 143       * different contexts also updates the tag instances associated with the questions.
 144       */
 145      public function test_altering_tag_instance_context() {
 146          global $CFG, $DB;
 147  
 148          // Set to admin user.
 149          $this->setAdminUser();
 150  
 151          // Create two course categories - we are going to delete one of these later and will expect
 152          // all the questions belonging to the course in the deleted category to be moved.
 153          $coursecat1 = $this->getDataGenerator()->create_category();
 154          $coursecat2 = $this->getDataGenerator()->create_category();
 155  
 156          // Create a couple of categories and questions.
 157          $context1 = \context_coursecat::instance($coursecat1->id);
 158          $context2 = \context_coursecat::instance($coursecat2->id);
 159          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 160          $questioncat1 = $questiongenerator->create_question_category(array('contextid' =>
 161              $context1->id));
 162          $questioncat2 = $questiongenerator->create_question_category(array('contextid' =>
 163              $context2->id));
 164          $question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat1->id));
 165          $question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat1->id));
 166          $question3 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat2->id));
 167          $question4 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat2->id));
 168  
 169          // Now lets tag these questions.
 170          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $context1, array('tag 1', 'tag 2'));
 171          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $context1, array('tag 3', 'tag 4'));
 172          \core_tag_tag::set_item_tags('core_question', 'question', $question3->id, $context2, array('tag 5', 'tag 6'));
 173          \core_tag_tag::set_item_tags('core_question', 'question', $question4->id, $context2, array('tag 7', 'tag 8'));
 174  
 175          // Test moving the questions to another category.
 176          question_move_questions_to_category(array($question1->id, $question2->id), $questioncat2->id);
 177  
 178          // Test that all tag_instances belong to one context.
 179          $this->assertEquals(8, $DB->count_records('tag_instance', array('component' => 'core_question',
 180              'contextid' => $questioncat2->contextid)));
 181  
 182          // Test moving them back.
 183          question_move_questions_to_category(array($question1->id, $question2->id), $questioncat1->id);
 184  
 185          // Test that all tag_instances are now reset to how they were initially.
 186          $this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
 187              'contextid' => $questioncat1->contextid)));
 188          $this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
 189              'contextid' => $questioncat2->contextid)));
 190  
 191          // Now test moving a whole question category to another context.
 192          question_move_category_to_context($questioncat1->id, $questioncat1->contextid, $questioncat2->contextid);
 193  
 194          // Test that all tag_instances belong to one context.
 195          $this->assertEquals(8, $DB->count_records('tag_instance', array('component' => 'core_question',
 196              'contextid' => $questioncat2->contextid)));
 197  
 198          // Now test moving them back.
 199          question_move_category_to_context($questioncat1->id, $questioncat2->contextid,
 200              \context_coursecat::instance($coursecat1->id)->id);
 201  
 202          // Test that all tag_instances are now reset to how they were initially.
 203          $this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
 204              'contextid' => $questioncat1->contextid)));
 205          $this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
 206              'contextid' => $questioncat2->contextid)));
 207  
 208          // Now we want to test deleting the course category and moving the questions to another category.
 209          question_delete_course_category($coursecat1, $coursecat2);
 210  
 211          // Test that all tag_instances belong to one context.
 212          $this->assertEquals(8, $DB->count_records('tag_instance', array('component' => 'core_question',
 213              'contextid' => $questioncat2->contextid)));
 214  
 215          // Create a course.
 216          $course = $this->getDataGenerator()->create_course();
 217  
 218          // Create some question categories and questions in this course.
 219          $coursecontext = \context_course::instance($course->id);
 220          $questioncat = $questiongenerator->create_question_category(array('contextid' =>
 221              $coursecontext->id));
 222          $question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
 223          $question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
 224  
 225          // Add some tags to these questions.
 226          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, array('tag 1', 'tag 2'));
 227          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, array('tag 1', 'tag 2'));
 228  
 229          // Create a course that we are going to restore the other course to.
 230          $course2 = $this->getDataGenerator()->create_course();
 231  
 232          // Create backup file and save it to the backup location.
 233          $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
 234              \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, 2);
 235          $bc->execute_plan();
 236          $results = $bc->get_results();
 237          $file = $results['backup_destination'];
 238          $fp = get_file_packer('application/vnd.moodle.backup');
 239          $filepath = $CFG->dataroot . '/temp/backup/test-restore-course';
 240          $file->extract_to_pathname($fp, $filepath);
 241          $bc->destroy();
 242  
 243          // Now restore the course.
 244          $rc = new \restore_controller('test-restore-course', $course2->id, \backup::INTERACTIVE_NO,
 245              \backup::MODE_GENERAL, 2, \backup::TARGET_NEW_COURSE);
 246          $rc->execute_precheck();
 247          $rc->execute_plan();
 248  
 249          // Get the created question category.
 250          $restoredcategory = $DB->get_record_select('question_categories', 'contextid = ? AND parent <> 0',
 251                  array(\context_course::instance($course2->id)->id), '*', MUST_EXIST);
 252  
 253          // Check that there are two questions in the restored to course's context.
 254          $this->assertEquals(2, $DB->count_records('question', array('category' => $restoredcategory->id)));
 255  
 256          $rc->destroy();
 257      }
 258  
 259      /**
 260       * Test that deleting a question from the question bank works in the normal case.
 261       */
 262      public function test_question_delete_question() {
 263          global $DB;
 264  
 265          // Setup.
 266          $context = \context_system::instance();
 267          $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
 268          $qcat = $qgen->create_question_category(array('contextid' => $context->id));
 269          $q1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
 270          $q2 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
 271  
 272          // Do.
 273          question_delete_question($q1->id);
 274  
 275          // Verify.
 276          $this->assertFalse($DB->record_exists('question', ['id' => $q1->id]));
 277          // Check that we did not delete too much.
 278          $this->assertTrue($DB->record_exists('question', ['id' => $q2->id]));
 279      }
 280  
 281      /**
 282       * Test that deleting a broken question from the question bank does not cause fatal errors.
 283       */
 284      public function test_question_delete_question_broken_data() {
 285          global $DB;
 286  
 287          // Setup.
 288          $context = \context_system::instance();
 289          $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
 290          $qcat = $qgen->create_question_category(array('contextid' => $context->id));
 291          $q1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
 292  
 293          // Now delete the category, to simulate what happens in old sites where
 294          // referential integrity has failed.
 295          $DB->delete_records('question_categories', ['id' => $qcat->id]);
 296  
 297          // Do.
 298          question_delete_question($q1->id);
 299  
 300          // Verify.
 301          $this->assertDebuggingCalled('Deleting question ' . $q1->id .
 302                  ' which is no longer linked to a context. Assuming system context ' .
 303                  'to avoid errors, but this may mean that some data like ' .
 304                  'files, tags, are not cleaned up.');
 305          $this->assertFalse($DB->record_exists('question', ['id' => $q1->id]));
 306      }
 307  
 308      /**
 309       * This function tests the question_category_delete_safe function.
 310       */
 311      public function test_question_category_delete_safe() {
 312          global $DB;
 313          $this->resetAfterTest(true);
 314          $this->setAdminUser();
 315  
 316          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
 317  
 318          question_category_delete_safe($qcat);
 319  
 320          // Verify category deleted.
 321          $criteria = array('id' => $qcat->id);
 322          $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
 323  
 324          // Verify questions deleted or moved.
 325          $criteria = array('category' => $qcat->id);
 326          $this->assertEquals(0, $DB->count_records('question', $criteria));
 327  
 328          // Verify question not deleted.
 329          $criteria = array('id' => $questions[0]->id);
 330          $this->assertEquals(1, $DB->count_records('question', $criteria));
 331      }
 332  
 333      /**
 334       * This function tests the question_delete_activity function.
 335       */
 336      public function test_question_delete_activity() {
 337          global $DB;
 338          $this->resetAfterTest(true);
 339          $this->setAdminUser();
 340  
 341          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
 342  
 343          $cm = get_coursemodule_from_instance('quiz', $quiz->id);
 344  
 345          // Test the deletion.
 346          question_delete_activity($cm);
 347  
 348          // Verify category deleted.
 349          $criteria = array('id' => $qcat->id);
 350          $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
 351  
 352          // Verify questions deleted or moved.
 353          $criteria = array('category' => $qcat->id);
 354          $this->assertEquals(0, $DB->count_records('question', $criteria));
 355      }
 356  
 357      /**
 358       * This function tests the question_delete_context function.
 359       */
 360      public function test_question_delete_context() {
 361          global $DB;
 362          $this->resetAfterTest(true);
 363          $this->setAdminUser();
 364  
 365          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
 366  
 367          // Get the module context id.
 368          $result = question_delete_context($qcat->contextid);
 369  
 370          // Verify category deleted.
 371          $criteria = array('id' => $qcat->id);
 372          $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
 373  
 374          // Verify questions deleted or moved.
 375          $criteria = array('category' => $qcat->id);
 376          $this->assertEquals(0, $DB->count_records('question', $criteria));
 377      }
 378  
 379      /**
 380       * This function tests the question_delete_course function.
 381       */
 382      public function test_question_delete_course() {
 383          global $DB;
 384          $this->resetAfterTest(true);
 385          $this->setAdminUser();
 386  
 387          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
 388  
 389          // Test the deletion.
 390          question_delete_course($course);
 391  
 392          // Verify category deleted.
 393          $criteria = array('id' => $qcat->id);
 394          $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
 395  
 396          // Verify questions deleted or moved.
 397          $criteria = array('category' => $qcat->id);
 398          $this->assertEquals(0, $DB->count_records('question', $criteria));
 399      }
 400  
 401      /**
 402       * This function tests the question_delete_course_category function.
 403       */
 404      public function test_question_delete_course_category() {
 405          global $DB;
 406          $this->resetAfterTest(true);
 407          $this->setAdminUser();
 408  
 409          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
 410  
 411          // Test that the feedback works.
 412          question_delete_course_category($category, null);
 413  
 414          // Verify category deleted.
 415          $criteria = array('id' => $qcat->id);
 416          $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
 417  
 418          // Verify questions deleted or moved.
 419          $criteria = array('category' => $qcat->id);
 420          $this->assertEquals(0, $DB->count_records('question', $criteria));
 421      }
 422  
 423      /**
 424       * This function tests the question_delete_course_category function when it is supposed to move question categories.
 425       */
 426      public function test_question_delete_course_category_move_qcats() {
 427          global $DB;
 428          $this->resetAfterTest(true);
 429          $this->setAdminUser();
 430  
 431          list($category1, $course1, $quiz1, $qcat1, $questions1) = $this->setup_quiz_and_questions('category');
 432          list($category2, $course2, $quiz2, $qcat2, $questions2) = $this->setup_quiz_and_questions('category');
 433  
 434          $questionsinqcat1 = count($questions1);
 435          $questionsinqcat2 = count($questions2);
 436  
 437          // Test the delete.
 438          question_delete_course_category($category1, $category2);
 439  
 440          // Verify category not deleted.
 441          $criteria = array('id' => $qcat1->id);
 442          $this->assertEquals(1, $DB->count_records('question_categories', $criteria));
 443  
 444          // Verify questions are moved.
 445          $params = array($qcat2->contextid);
 446          $actualquestionscount = $DB->count_records_sql("SELECT COUNT(*)
 447                                                            FROM {question} q
 448                                                            JOIN {question_categories} qc ON q.category = qc.id
 449                                                           WHERE qc.contextid = ?", $params);
 450          $this->assertEquals($questionsinqcat1 + $questionsinqcat2, $actualquestionscount);
 451  
 452          // Verify there is just a single top-level category.
 453          $criteria = array('contextid' => $qcat2->contextid, 'parent' => 0);
 454          $this->assertEquals(1, $DB->count_records('question_categories', $criteria));
 455  
 456          // Verify there is no question category in previous context.
 457          $criteria = array('contextid' => $qcat1->contextid);
 458          $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
 459      }
 460  
 461      /**
 462       * This function tests the question_save_from_deletion function when it is supposed to make a new category and
 463       * move question categories to that new category.
 464       */
 465      public function test_question_save_from_deletion() {
 466          global $DB;
 467          $this->resetAfterTest(true);
 468          $this->setAdminUser();
 469  
 470          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
 471  
 472          $context = \context::instance_by_id($qcat->contextid);
 473  
 474          $newcat = question_save_from_deletion(array_column($questions, 'id'),
 475                  $context->get_parent_context()->id, $context->get_context_name());
 476  
 477          // Verify that the newcat itself is not a tep level category.
 478          $this->assertNotEquals(0, $newcat->parent);
 479  
 480          // Verify there is just a single top-level category.
 481          $this->assertEquals(1, $DB->count_records('question_categories', ['contextid' => $qcat->contextid, 'parent' => 0]));
 482      }
 483  
 484      /**
 485       * This function tests the question_save_from_deletion function when it is supposed to make a new category and
 486       * move question categories to that new category when quiz name is very long but less than 256 characters.
 487       */
 488      public function test_question_save_from_deletion_quiz_with_long_name() {
 489          global $DB;
 490          $this->resetAfterTest(true);
 491          $this->setAdminUser();
 492  
 493          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
 494  
 495          // Moodle doesn't allow you to enter a name longer than 255 characters.
 496          $quiz->name = shorten_text(str_repeat('123456789 ', 26), 255);
 497  
 498          $DB->update_record('quiz', $quiz);
 499  
 500          $context = \context::instance_by_id($qcat->contextid);
 501  
 502          $newcat = question_save_from_deletion(array_column($questions, 'id'),
 503                  $context->get_parent_context()->id, $context->get_context_name());
 504  
 505          // Verifying that the inserted record's name is expected or not.
 506          $this->assertEquals($DB->get_record('question_categories', ['id' => $newcat->id])->name, $newcat->name);
 507  
 508          // Verify that the newcat itself is not a top level category.
 509          $this->assertNotEquals(0, $newcat->parent);
 510  
 511          // Verify there is just a single top-level category.
 512          $this->assertEquals(1, $DB->count_records('question_categories', ['contextid' => $qcat->contextid, 'parent' => 0]));
 513      }
 514  
 515      public function test_question_remove_stale_questions_from_category() {
 516          global $DB;
 517          $this->resetAfterTest(true);
 518          $this->setAdminUser();
 519  
 520          $dg = $this->getDataGenerator();
 521          $course = $dg->create_course();
 522          $quiz = $dg->create_module('quiz', ['course' => $course->id]);
 523  
 524          $qgen = $dg->get_plugin_generator('core_question');
 525          $context = \context_system::instance();
 526  
 527          $qcat1 = $qgen->create_question_category(['contextid' => $context->id]);
 528          $q1a = $qgen->create_question('shortanswer', null, ['category' => $qcat1->id]);     // Will be hidden.
 529          $DB->set_field('question', 'hidden', 1, ['id' => $q1a->id]);
 530  
 531          $qcat2 = $qgen->create_question_category(['contextid' => $context->id]);
 532          $q2a = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]);     // Will be hidden.
 533          $q2b = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]);     // Will be hidden but used.
 534          $DB->set_field('question', 'hidden', 1, ['id' => $q2a->id]);
 535          $DB->set_field('question', 'hidden', 1, ['id' => $q2b->id]);
 536          quiz_add_quiz_question($q2b->id, $quiz);
 537          quiz_add_random_questions($quiz, 0, $qcat2->id, 1, false);
 538  
 539          // We added one random question to the quiz and we expect the quiz to have only one random question.
 540          $q2d = $DB->get_record_sql("SELECT q.*
 541                                        FROM {question} q
 542                                        JOIN {quiz_slots} s ON s.questionid = q.id
 543                                       WHERE q.qtype = :qtype
 544                                             AND s.quizid = :quizid",
 545                  array('qtype' => 'random', 'quizid' => $quiz->id), MUST_EXIST);
 546  
 547          // The following 2 lines have to be after the quiz_add_random_questions() call above.
 548          // Otherwise, quiz_add_random_questions() will to be "smart" and use them instead of creating a new "random" question.
 549          $q1b = $qgen->create_question('random', null, ['category' => $qcat1->id]);          // Will not be used.
 550          $q2c = $qgen->create_question('random', null, ['category' => $qcat2->id]);          // Will not be used.
 551  
 552          $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
 553          $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
 554  
 555          // Non-existing category, nothing will happen.
 556          question_remove_stale_questions_from_category(0);
 557          $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
 558          $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
 559  
 560          // First category, should be empty afterwards.
 561          question_remove_stale_questions_from_category($qcat1->id);
 562          $this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
 563          $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
 564          $this->assertFalse($DB->record_exists('question', ['id' => $q1a->id]));
 565          $this->assertFalse($DB->record_exists('question', ['id' => $q1b->id]));
 566  
 567          // Second category, used questions should be left untouched.
 568          question_remove_stale_questions_from_category($qcat2->id);
 569          $this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
 570          $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat2->id]));
 571          $this->assertFalse($DB->record_exists('question', ['id' => $q2a->id]));
 572          $this->assertTrue($DB->record_exists('question', ['id' => $q2b->id]));
 573          $this->assertFalse($DB->record_exists('question', ['id' => $q2c->id]));
 574          $this->assertTrue($DB->record_exists('question', ['id' => $q2d->id]));
 575      }
 576  
 577      /**
 578       * get_question_options should add the category object to the given question.
 579       */
 580      public function test_get_question_options_includes_category_object_single_question() {
 581          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
 582          $question = array_shift($questions);
 583  
 584          get_question_options($question);
 585  
 586          $this->assertEquals($qcat, $question->categoryobject);
 587      }
 588  
 589      /**
 590       * get_question_options should add the category object to all of the questions in
 591       * the given list.
 592       */
 593      public function test_get_question_options_includes_category_object_multiple_questions() {
 594          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
 595  
 596          get_question_options($questions);
 597  
 598          foreach ($questions as $question) {
 599              $this->assertEquals($qcat, $question->categoryobject);
 600          }
 601      }
 602  
 603      /**
 604       * get_question_options includes the tags for all questions in the list.
 605       */
 606      public function test_get_question_options_includes_question_tags() {
 607          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
 608          $question1 = $questions[0];
 609          $question2 = $questions[1];
 610          $qcontext = \context::instance_by_id($qcat->contextid);
 611  
 612          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
 613          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
 614  
 615          get_question_options($questions, true);
 616  
 617          foreach ($questions as $question) {
 618              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 619              $expectedtags = [];
 620              $actualtags = $question->tags;
 621              foreach ($tags as $tag) {
 622                  $expectedtags[$tag->id] = $tag->get_display_name();
 623              }
 624  
 625              // The question should have a tags property populated with each tag id
 626              // and display name as a key vale pair.
 627              $this->assertEquals($expectedtags, $actualtags);
 628  
 629              $actualtagobjects = $question->tagobjects;
 630              sort($tags);
 631              sort($actualtagobjects);
 632  
 633              // The question should have a full set of each tag object.
 634              $this->assertEquals($tags, $actualtagobjects);
 635              // The question should not have any course tags.
 636              $this->assertEmpty($question->coursetagobjects);
 637          }
 638      }
 639  
 640      /**
 641       * get_question_options includes the course tags for all questions in the list.
 642       */
 643      public function test_get_question_options_includes_course_tags() {
 644          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
 645          $question1 = $questions[0];
 646          $question2 = $questions[1];
 647          $coursecontext = \context_course::instance($course->id);
 648  
 649          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
 650          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
 651  
 652          get_question_options($questions, true);
 653  
 654          foreach ($questions as $question) {
 655              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 656              $expectedcoursetags = [];
 657              $actualcoursetags = $question->coursetags;
 658              foreach ($tags as $tag) {
 659                  $expectedcoursetags[$tag->id] = $tag->get_display_name();
 660              }
 661  
 662              // The question should have a coursetags property populated with each tag id
 663              // and display name as a key vale pair.
 664              $this->assertEquals($expectedcoursetags, $actualcoursetags);
 665  
 666              $actualcoursetagobjects = $question->coursetagobjects;
 667              sort($tags);
 668              sort($actualcoursetagobjects);
 669  
 670              // The question should have a full set of the course tag objects.
 671              $this->assertEquals($tags, $actualcoursetagobjects);
 672              // The question should not have any other tags.
 673              $this->assertEmpty($question->tagobjects);
 674              $this->assertEmpty($question->tags);
 675          }
 676      }
 677  
 678      /**
 679       * get_question_options only categorises a tag as a course tag if it is in a
 680       * course context that is different from the question context.
 681       */
 682      public function test_get_question_options_course_tags_in_course_question_context() {
 683          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
 684          $question1 = $questions[0];
 685          $question2 = $questions[1];
 686          $coursecontext = \context_course::instance($course->id);
 687  
 688          // Create course level tags in the course context that matches the question
 689          // course context.
 690          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
 691          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
 692  
 693          get_question_options($questions, true);
 694  
 695          foreach ($questions as $question) {
 696              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 697  
 698              $actualtagobjects = $question->tagobjects;
 699              sort($tags);
 700              sort($actualtagobjects);
 701  
 702              // The tags should not be considered course tags because they are in
 703              // the same context as the question. That makes them question tags.
 704              $this->assertEmpty($question->coursetagobjects);
 705              // The course context tags should be returned in the regular tag object
 706              // list.
 707              $this->assertEquals($tags, $actualtagobjects);
 708          }
 709      }
 710  
 711      /**
 712       * get_question_options includes the tags and course tags for all questions in the list
 713       * if each question has course and question level tags.
 714       */
 715      public function test_get_question_options_includes_question_and_course_tags() {
 716          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
 717          $question1 = $questions[0];
 718          $question2 = $questions[1];
 719          $qcontext = \context::instance_by_id($qcat->contextid);
 720          $coursecontext = \context_course::instance($course->id);
 721  
 722          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
 723          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['cfoo', 'cbar']);
 724          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
 725          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['cbaz', 'cbop']);
 726  
 727          get_question_options($questions, true);
 728  
 729          foreach ($questions as $question) {
 730              $alltags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 731              $tags = array_filter($alltags, function($tag) use ($qcontext) {
 732                  return $tag->taginstancecontextid == $qcontext->id;
 733              });
 734              $coursetags = array_filter($alltags, function($tag) use ($coursecontext) {
 735                  return $tag->taginstancecontextid == $coursecontext->id;
 736              });
 737  
 738              $expectedtags = [];
 739              $actualtags = $question->tags;
 740              foreach ($tags as $tag) {
 741                  $expectedtags[$tag->id] = $tag->get_display_name();
 742              }
 743  
 744              // The question should have a tags property populated with each tag id
 745              // and display name as a key vale pair.
 746              $this->assertEquals($expectedtags, $actualtags);
 747  
 748              $actualtagobjects = $question->tagobjects;
 749              sort($tags);
 750              sort($actualtagobjects);
 751              // The question should have a full set of each tag object.
 752              $this->assertEquals($tags, $actualtagobjects);
 753  
 754              $actualcoursetagobjects = $question->coursetagobjects;
 755              sort($coursetags);
 756              sort($actualcoursetagobjects);
 757              // The question should have a full set of course tag objects.
 758              $this->assertEquals($coursetags, $actualcoursetagobjects);
 759          }
 760      }
 761  
 762      /**
 763       * get_question_options should update the context id to the question category
 764       * context id for any non-course context tag that isn't in the question category
 765       * context.
 766       */
 767      public function test_get_question_options_normalises_question_tags() {
 768          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
 769          $question1 = $questions[0];
 770          $question2 = $questions[1];
 771          $qcontext = \context::instance_by_id($qcat->contextid);
 772          $systemcontext = \context_system::instance();
 773  
 774          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
 775          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
 776  
 777          $q1tags = \core_tag_tag::get_item_tags('core_question', 'question', $question1->id);
 778          $q2tags = \core_tag_tag::get_item_tags('core_question', 'question', $question2->id);
 779          $q1tag = array_shift($q1tags);
 780          $q2tag = array_shift($q2tags);
 781  
 782          // Change two of the tag instances to be a different (non-course) context to the
 783          // question tag context. These tags should then be normalised back to the question
 784          // tag context.
 785          \core_tag_tag::change_instances_context([$q1tag->taginstanceid, $q2tag->taginstanceid], $systemcontext);
 786  
 787          get_question_options($questions, true);
 788  
 789          foreach ($questions as $question) {
 790              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 791  
 792              // The database should have been updated with the correct context id.
 793              foreach ($tags as $tag) {
 794                  $this->assertEquals($qcontext->id, $tag->taginstancecontextid);
 795              }
 796  
 797              // The tag objects on the question should have been updated with the
 798              // correct context id.
 799              foreach ($question->tagobjects as $tag) {
 800                  $this->assertEquals($qcontext->id, $tag->taginstancecontextid);
 801              }
 802          }
 803      }
 804  
 805      /**
 806       * get_question_options if the question is a course level question then tags
 807       * in that context should not be consdered course tags, they are question tags.
 808       */
 809      public function test_get_question_options_includes_course_context_question_tags() {
 810          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
 811          $question1 = $questions[0];
 812          $question2 = $questions[1];
 813          $coursecontext = \context_course::instance($course->id);
 814  
 815          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
 816          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
 817  
 818          get_question_options($questions, true);
 819  
 820          foreach ($questions as $question) {
 821              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 822              // Tags in a course context that matches the question context should
 823              // not be considered course tags.
 824              $this->assertEmpty($question->coursetagobjects);
 825              $this->assertEmpty($question->coursetags);
 826  
 827              $actualtagobjects = $question->tagobjects;
 828              sort($tags);
 829              sort($actualtagobjects);
 830              // The tags should be considered question tags not course tags.
 831              $this->assertEquals($tags, $actualtagobjects);
 832          }
 833      }
 834  
 835      /**
 836       * get_question_options should return tags from all course contexts by default.
 837       */
 838      public function test_get_question_options_includes_multiple_courses_tags() {
 839          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
 840          $question1 = $questions[0];
 841          $question2 = $questions[1];
 842          $coursecontext = \context_course::instance($course->id);
 843          // Create a sibling course.
 844          $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
 845          $siblingcoursecontext = \context_course::instance($siblingcourse->id);
 846  
 847          // Create course tags.
 848          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['c1']);
 849          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['c1']);
 850          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['c2']);
 851          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['c2']);
 852  
 853          get_question_options($questions, true);
 854  
 855          foreach ($questions as $question) {
 856              $this->assertCount(2, $question->coursetagobjects);
 857  
 858              foreach ($question->coursetagobjects as $tag) {
 859                  if ($tag->name == 'c1') {
 860                      $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
 861                  } else {
 862                      $this->assertEquals($siblingcoursecontext->id, $tag->taginstancecontextid);
 863                  }
 864              }
 865          }
 866      }
 867  
 868      /**
 869       * get_question_options should filter the course tags by the given list of courses.
 870       */
 871      public function test_get_question_options_includes_filter_course_tags() {
 872          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
 873          $question1 = $questions[0];
 874          $question2 = $questions[1];
 875          $coursecontext = \context_course::instance($course->id);
 876          // Create a sibling course.
 877          $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
 878          $siblingcoursecontext = \context_course::instance($siblingcourse->id);
 879  
 880          // Create course tags.
 881          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo']);
 882          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
 883          // Create sibling course tags. These should be filtered out.
 884          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['filtered1']);
 885          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['filtered2']);
 886  
 887          // Ask to only receive course tags from $course (ignoring $siblingcourse tags).
 888          get_question_options($questions, true, [$course]);
 889  
 890          foreach ($questions as $question) {
 891              foreach ($question->coursetagobjects as $tag) {
 892                  // We should only be seeing course tags from $course. The tags from
 893                  // $siblingcourse should have been filtered out.
 894                  $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
 895              }
 896          }
 897      }
 898  
 899      /**
 900       * question_move_question_tags_to_new_context should update all of the
 901       * question tags contexts when they are moving down (from system to course
 902       * category context).
 903       */
 904      public function test_question_move_question_tags_to_new_context_system_to_course_cat_qtags() {
 905          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
 906          $question1 = $questions[0];
 907          $question2 = $questions[1];
 908          $qcontext = \context::instance_by_id($qcat->contextid);
 909          $newcontext = \context_coursecat::instance($category->id);
 910  
 911          foreach ($questions as $question) {
 912              $question->contextid = $qcat->contextid;
 913          }
 914  
 915          // Create tags in the system context.
 916          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
 917          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo', 'bar']);
 918  
 919          question_move_question_tags_to_new_context($questions, $newcontext);
 920  
 921          foreach ($questions as $question) {
 922              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 923  
 924              // All of the tags should have their context id set to the new context.
 925              foreach ($tags as $tag) {
 926                  $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
 927              }
 928          }
 929      }
 930  
 931      /**
 932       * question_move_question_tags_to_new_context should update all of the question tags
 933       * contexts when they are moving down (from system to course category context)
 934       * but leave any tags in the course context where they are.
 935       */
 936      public function test_question_move_question_tags_to_new_context_system_to_course_cat_qtags_and_course_tags() {
 937          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
 938          $question1 = $questions[0];
 939          $question2 = $questions[1];
 940          $qcontext = \context::instance_by_id($qcat->contextid);
 941          $coursecontext = \context_course::instance($course->id);
 942          $newcontext = \context_coursecat::instance($category->id);
 943  
 944          foreach ($questions as $question) {
 945              $question->contextid = $qcat->contextid;
 946          }
 947  
 948          // Create tags in the system context.
 949          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
 950          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
 951          // Create tags in the course context.
 952          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
 953          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
 954  
 955          question_move_question_tags_to_new_context($questions, $newcontext);
 956  
 957          foreach ($questions as $question) {
 958              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 959  
 960              foreach ($tags as $tag) {
 961                  if ($tag->name == 'ctag') {
 962                      // Course tags should remain in the course context.
 963                      $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
 964                  } else {
 965                      // Other tags should be updated.
 966                      $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
 967                  }
 968              }
 969          }
 970      }
 971  
 972      /**
 973       * question_move_question_tags_to_new_context should update all of the question
 974       * contexts tags when they are moving up (from course category to system context).
 975       */
 976      public function test_question_move_question_tags_to_new_context_course_cat_to_system_qtags() {
 977          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
 978          $question1 = $questions[0];
 979          $question2 = $questions[1];
 980          $qcontext = \context::instance_by_id($qcat->contextid);
 981          $newcontext = \context_system::instance();
 982  
 983          foreach ($questions as $question) {
 984              $question->contextid = $qcat->contextid;
 985          }
 986  
 987          // Create tags in the course category context.
 988          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
 989          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo', 'bar']);
 990  
 991          question_move_question_tags_to_new_context($questions, $newcontext);
 992  
 993          foreach ($questions as $question) {
 994              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 995  
 996              // All of the tags should have their context id set to the new context.
 997              foreach ($tags as $tag) {
 998                  $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
 999              }
1000          }
1001      }
1002  
1003      /**
1004       * question_move_question_tags_to_new_context should update all of the question
1005       * tags contexts when they are moving up (from course category context to system
1006       * context) but leave any tags in the course context where they are.
1007       */
1008      public function test_question_move_question_tags_to_new_context_course_cat_to_system_qtags_and_course_tags() {
1009          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1010          $question1 = $questions[0];
1011          $question2 = $questions[1];
1012          $qcontext = \context::instance_by_id($qcat->contextid);
1013          $coursecontext = \context_course::instance($course->id);
1014          $newcontext = \context_system::instance();
1015  
1016          foreach ($questions as $question) {
1017              $question->contextid = $qcat->contextid;
1018          }
1019  
1020          // Create tags in the system context.
1021          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1022          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1023          // Create tags in the course context.
1024          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1025          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1026  
1027          question_move_question_tags_to_new_context($questions, $newcontext);
1028  
1029          foreach ($questions as $question) {
1030              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1031  
1032              foreach ($tags as $tag) {
1033                  if ($tag->name == 'ctag') {
1034                      // Course tags should remain in the course context.
1035                      $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1036                  } else {
1037                      // Other tags should be updated.
1038                      $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1039                  }
1040              }
1041          }
1042      }
1043  
1044      /**
1045       * question_move_question_tags_to_new_context should merge all tags into the course
1046       * context when moving down from course category context into course context.
1047       */
1048      public function test_question_move_question_tags_to_new_context_course_cat_to_coures_qtags_and_course_tags() {
1049          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1050          $question1 = $questions[0];
1051          $question2 = $questions[1];
1052          $qcontext = \context::instance_by_id($qcat->contextid);
1053          $coursecontext = \context_course::instance($course->id);
1054          $newcontext = $coursecontext;
1055  
1056          foreach ($questions as $question) {
1057              $question->contextid = $qcat->contextid;
1058          }
1059  
1060          // Create tags in the system context.
1061          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1062          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1063          // Create tags in the course context.
1064          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1065          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1066  
1067          question_move_question_tags_to_new_context($questions, $newcontext);
1068  
1069          foreach ($questions as $question) {
1070              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1071              // Each question should have 2 tags.
1072              $this->assertCount(2, $tags);
1073  
1074              foreach ($tags as $tag) {
1075                  // All tags should be updated to the course context and merged in.
1076                  $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1077              }
1078          }
1079      }
1080  
1081      /**
1082       * question_move_question_tags_to_new_context should delete all of the tag
1083       * instances from sibling courses when moving the context of a question down
1084       * from a course category into a course context because the other courses will
1085       * no longer have access to the question.
1086       */
1087      public function test_question_move_question_tags_to_new_context_remove_other_course_tags() {
1088          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1089          // Create a sibling course.
1090          $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
1091          $question1 = $questions[0];
1092          $question2 = $questions[1];
1093          $qcontext = \context::instance_by_id($qcat->contextid);
1094          $coursecontext = \context_course::instance($course->id);
1095          $siblingcoursecontext = \context_course::instance($siblingcourse->id);
1096          $newcontext = $coursecontext;
1097  
1098          foreach ($questions as $question) {
1099              $question->contextid = $qcat->contextid;
1100          }
1101  
1102          // Create tags in the system context.
1103          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1104          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1105          // Create tags in the target course context.
1106          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1107          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1108          // Create tags in the sibling course context. These should be deleted as
1109          // part of the move.
1110          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['stag']);
1111          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['stag']);
1112  
1113          question_move_question_tags_to_new_context($questions, $newcontext);
1114  
1115          foreach ($questions as $question) {
1116              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1117              // Each question should have 2 tags, 'foo' and 'ctag'.
1118              $this->assertCount(2, $tags);
1119  
1120              foreach ($tags as $tag) {
1121                  $tagname = $tag->name;
1122                  // The 'stag' should have been deleted because it's in a sibling
1123                  // course context.
1124                  $this->assertContains($tagname, ['foo', 'ctag']);
1125                  // All tags should be in the course context now.
1126                  $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1127              }
1128          }
1129      }
1130  
1131      /**
1132       * question_move_question_tags_to_new_context should update all of the question
1133       * tags to be the course category context when moving the tags from a course
1134       * context to a course category context.
1135       */
1136      public function test_question_move_question_tags_to_new_context_course_to_course_cat() {
1137          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
1138          $question1 = $questions[0];
1139          $question2 = $questions[1];
1140          $qcontext = \context::instance_by_id($qcat->contextid);
1141          // Moving up into the course category context.
1142          $newcontext = \context_coursecat::instance($category->id);
1143  
1144          foreach ($questions as $question) {
1145              $question->contextid = $qcat->contextid;
1146          }
1147  
1148          // Create tags in the course context.
1149          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1150          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1151  
1152          question_move_question_tags_to_new_context($questions, $newcontext);
1153  
1154          foreach ($questions as $question) {
1155              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1156  
1157              // All of the tags should have their context id set to the new context.
1158              foreach ($tags as $tag) {
1159                  $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1160              }
1161          }
1162      }
1163  
1164      /**
1165       * question_move_question_tags_to_new_context should update all of the
1166       * question tags contexts when they are moving down (from system to course
1167       * category context).
1168       */
1169      public function test_question_move_question_tags_to_new_context_orphaned_tag_contexts() {
1170          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
1171          $question1 = $questions[0];
1172          $question2 = $questions[1];
1173          $othercategory = $this->getDataGenerator()->create_category();
1174          $qcontext = \context::instance_by_id($qcat->contextid);
1175          $newcontext = \context_coursecat::instance($category->id);
1176          $othercategorycontext = \context_coursecat::instance($othercategory->id);
1177  
1178          foreach ($questions as $question) {
1179              $question->contextid = $qcat->contextid;
1180          }
1181  
1182          // Create tags in the system context.
1183          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1184          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1185          // Create tags in the other course category context. These should be
1186          // update to the next context id because they represent erroneous data
1187          // from a time before context id was mandatory in the tag API.
1188          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercategorycontext, ['bar']);
1189          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercategorycontext, ['bar']);
1190  
1191          question_move_question_tags_to_new_context($questions, $newcontext);
1192  
1193          foreach ($questions as $question) {
1194              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1195              // Each question should have two tags, 'foo' and 'bar'.
1196              $this->assertCount(2, $tags);
1197  
1198              // All of the tags should have their context id set to the new context
1199              // (course category context).
1200              foreach ($tags as $tag) {
1201                  $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1202              }
1203          }
1204      }
1205  
1206      /**
1207       * When moving from a course category context down into an activity context
1208       * all question context tags and course tags (where the course is a parent of
1209       * the activity) should move into the new context.
1210       */
1211      public function test_question_move_question_tags_to_new_context_course_cat_to_activity_qtags_and_course_tags() {
1212          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1213          $question1 = $questions[0];
1214          $question2 = $questions[1];
1215          $qcontext = \context::instance_by_id($qcat->contextid);
1216          $coursecontext = \context_course::instance($course->id);
1217          $newcontext = \context_module::instance($quiz->cmid);
1218  
1219          foreach ($questions as $question) {
1220              $question->contextid = $qcat->contextid;
1221          }
1222  
1223          // Create tags in the course category context.
1224          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1225          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1226          // Move the questions to the activity context which is a child context of
1227          // $coursecontext.
1228          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1229          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1230  
1231          question_move_question_tags_to_new_context($questions, $newcontext);
1232  
1233          foreach ($questions as $question) {
1234              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1235              // Each question should have 2 tags.
1236              $this->assertCount(2, $tags);
1237  
1238              foreach ($tags as $tag) {
1239                  $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1240              }
1241          }
1242      }
1243  
1244      /**
1245       * When moving from a course category context down into an activity context
1246       * all question context tags and course tags (where the course is a parent of
1247       * the activity) should move into the new context. Tags in course contexts
1248       * that are not a parent of the activity context should be deleted.
1249       */
1250      public function test_question_move_question_tags_to_new_context_course_cat_to_activity_orphaned_tags() {
1251          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1252          $question1 = $questions[0];
1253          $question2 = $questions[1];
1254          $qcontext = \context::instance_by_id($qcat->contextid);
1255          $coursecontext = \context_course::instance($course->id);
1256          $newcontext = \context_module::instance($quiz->cmid);
1257          $othercourse = $this->getDataGenerator()->create_course();
1258          $othercoursecontext = \context_course::instance($othercourse->id);
1259  
1260          foreach ($questions as $question) {
1261              $question->contextid = $qcat->contextid;
1262          }
1263  
1264          // Create tags in the course category context.
1265          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1266          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1267          // Create tags in the course context.
1268          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1269          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1270          // Create tags in the other course context. These should be deleted.
1271          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercoursecontext, ['delete']);
1272          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercoursecontext, ['delete']);
1273  
1274          // Move the questions to the activity context which is a child context of
1275          // $coursecontext.
1276          question_move_question_tags_to_new_context($questions, $newcontext);
1277  
1278          foreach ($questions as $question) {
1279              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1280              // Each question should have 2 tags.
1281              $this->assertCount(2, $tags);
1282  
1283              foreach ($tags as $tag) {
1284                  // Make sure we don't have any 'delete' tags.
1285                  $this->assertContains($tag->name, ['foo', 'ctag']);
1286                  $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1287              }
1288          }
1289      }
1290  
1291      /**
1292       * When moving from a course context down into an activity context all of the
1293       * course tags should move into the activity context.
1294       */
1295      public function test_question_move_question_tags_to_new_context_course_to_activity_qtags() {
1296          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
1297          $question1 = $questions[0];
1298          $question2 = $questions[1];
1299          $qcontext = \context::instance_by_id($qcat->contextid);
1300          $newcontext = \context_module::instance($quiz->cmid);
1301  
1302          foreach ($questions as $question) {
1303              $question->contextid = $qcat->contextid;
1304          }
1305  
1306          // Create tags in the course context.
1307          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1308          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1309  
1310          question_move_question_tags_to_new_context($questions, $newcontext);
1311  
1312          foreach ($questions as $question) {
1313              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1314  
1315              foreach ($tags as $tag) {
1316                  $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1317              }
1318          }
1319      }
1320  
1321      /**
1322       * When moving from a course context down into an activity context all of the
1323       * course tags should move into the activity context.
1324       */
1325      public function test_question_move_question_tags_to_new_context_activity_to_course_qtags() {
1326          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
1327          $question1 = $questions[0];
1328          $question2 = $questions[1];
1329          $qcontext = \context::instance_by_id($qcat->contextid);
1330          $newcontext = \context_course::instance($course->id);
1331  
1332          foreach ($questions as $question) {
1333              $question->contextid = $qcat->contextid;
1334          }
1335  
1336          // Create tags in the activity context.
1337          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1338          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1339  
1340          question_move_question_tags_to_new_context($questions, $newcontext);
1341  
1342          foreach ($questions as $question) {
1343              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1344  
1345              foreach ($tags as $tag) {
1346                  $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1347              }
1348          }
1349      }
1350  
1351      /**
1352       * question_move_question_tags_to_new_context should update all of the
1353       * question tags contexts when they are moving down (from system to course
1354       * category context).
1355       *
1356       * Course tags within the new category context should remain while any course
1357       * tags in course contexts that can no longer access the question should be
1358       * deleted.
1359       */
1360      public function test_question_move_question_tags_to_new_context_system_to_course_cat_with_orphaned_tags() {
1361          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
1362          $question1 = $questions[0];
1363          $question2 = $questions[1];
1364          $othercategory = $this->getDataGenerator()->create_category();
1365          $othercourse = $this->getDataGenerator()->create_course(['category' => $othercategory->id]);
1366          $qcontext = \context::instance_by_id($qcat->contextid);
1367          $newcontext = \context_coursecat::instance($category->id);
1368          $othercategorycontext = \context_coursecat::instance($othercategory->id);
1369          $coursecontext = \context_course::instance($course->id);
1370          $othercoursecontext = \context_course::instance($othercourse->id);
1371  
1372          foreach ($questions as $question) {
1373              $question->contextid = $qcat->contextid;
1374          }
1375  
1376          // Create tags in the system context.
1377          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1378          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1379          // Create tags in the child course context of the new context.
1380          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['bar']);
1381          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
1382          // Create tags in the other course context. These should be deleted when
1383          // the question moves to the new course category context because this
1384          // course belongs to a different category, which means it will no longer
1385          // have access to the question.
1386          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercoursecontext, ['delete']);
1387          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercoursecontext, ['delete']);
1388  
1389          question_move_question_tags_to_new_context($questions, $newcontext);
1390  
1391          foreach ($questions as $question) {
1392              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1393              // Each question should have two tags, 'foo' and 'bar'.
1394              $this->assertCount(2, $tags);
1395  
1396              // All of the tags should have their context id set to the new context
1397              // (course category context).
1398              foreach ($tags as $tag) {
1399                  $this->assertContains($tag->name, ['foo', 'bar']);
1400  
1401                  if ($tag->name == 'foo') {
1402                      $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1403                  } else {
1404                      $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1405                  }
1406              }
1407          }
1408      }
1409  
1410      /**
1411       * question_sort_tags() includes the tags for all questions in the list.
1412       */
1413      public function test_question_sort_tags_includes_question_tags() {
1414  
1415          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1416          $question1 = $questions[0];
1417          $question2 = $questions[1];
1418          $qcontext = \context::instance_by_id($qcat->contextid);
1419  
1420          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
1421          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
1422  
1423          foreach ($questions as $question) {
1424              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1425              $categorycontext = \context::instance_by_id($qcat->contextid);
1426              $tagobjects = question_sort_tags($tags, $categorycontext);
1427              $expectedtags = [];
1428              $actualtags = $tagobjects->tags;
1429              foreach ($tagobjects->tagobjects as $tag) {
1430                  $expectedtags[$tag->id] = $tag->name;
1431              }
1432  
1433              // The question should have a tags property populated with each tag id
1434              // and display name as a key vale pair.
1435              $this->assertEquals($expectedtags, $actualtags);
1436  
1437              $actualtagobjects = $tagobjects->tagobjects;
1438              sort($tags);
1439              sort($actualtagobjects);
1440  
1441              // The question should have a full set of each tag object.
1442              $this->assertEquals($tags, $actualtagobjects);
1443              // The question should not have any course tags.
1444              $this->assertEmpty($tagobjects->coursetagobjects);
1445          }
1446      }
1447  
1448      /**
1449       * question_sort_tags() includes course tags for all questions in the list.
1450       */
1451      public function test_question_sort_tags_includes_question_course_tags() {
1452          global $DB;
1453  
1454          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1455          $question1 = $questions[0];
1456          $question2 = $questions[1];
1457          $coursecontext = \context_course::instance($course->id);
1458  
1459          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
1460          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
1461  
1462          foreach ($questions as $question) {
1463              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1464              $tagobjects = question_sort_tags($tags, $qcat);
1465  
1466              $expectedtags = [];
1467              $actualtags = $tagobjects->coursetags;
1468              foreach ($actualtags as $coursetagid => $coursetagname) {
1469                  $expectedtags[$coursetagid] = $coursetagname;
1470              }
1471  
1472              // The question should have a tags property populated with each tag id
1473              // and display name as a key vale pair.
1474              $this->assertEquals($expectedtags, $actualtags);
1475  
1476              $actualtagobjects = $tagobjects->coursetagobjects;
1477              sort($tags);
1478              sort($actualtagobjects);
1479  
1480              // The question should have a full set of each tag object.
1481              $this->assertEquals($tags, $actualtagobjects);
1482              // The question should not have any course tags.
1483              $this->assertEmpty($tagobjects->tagobjects);
1484          }
1485      }
1486  
1487      /**
1488       * question_sort_tags() should return tags from all course contexts by default.
1489       */
1490      public function test_question_sort_tags_includes_multiple_courses_tags() {
1491          global $DB;
1492  
1493          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1494          $question1 = $questions[0];
1495          $question2 = $questions[1];
1496          $coursecontext = \context_course::instance($course->id);
1497          // Create a sibling course.
1498          $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
1499          $siblingcoursecontext = \context_course::instance($siblingcourse->id);
1500  
1501          // Create course tags.
1502          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['c1']);
1503          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['c1']);
1504          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['c2']);
1505          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['c2']);
1506  
1507          foreach ($questions as $question) {
1508              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1509              $tagobjects = question_sort_tags($tags, $qcat);
1510              $this->assertCount(2, $tagobjects->coursetagobjects);
1511  
1512              foreach ($tagobjects->coursetagobjects as $tag) {
1513                  if ($tag->name == 'c1') {
1514                      $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1515                  } else {
1516                      $this->assertEquals($siblingcoursecontext->id, $tag->taginstancecontextid);
1517                  }
1518              }
1519          }
1520      }
1521  
1522      /**
1523       * question_sort_tags() should filter the course tags by the given list of courses.
1524       */
1525      public function test_question_sort_tags_includes_filter_course_tags() {
1526          global $DB;
1527  
1528          list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1529          $question1 = $questions[0];
1530          $question2 = $questions[1];
1531          $coursecontext = \context_course::instance($course->id);
1532          // Create a sibling course.
1533          $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
1534          $siblingcoursecontext = \context_course::instance($siblingcourse->id);
1535  
1536          // Create course tags.
1537          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo']);
1538          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
1539          // Create sibling course tags. These should be filtered out.
1540          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['filtered1']);
1541          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['filtered2']);
1542  
1543          foreach ($questions as $question) {
1544              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1545              $tagobjects = question_sort_tags($tags, $qcat, [$course]);
1546              foreach ($tagobjects->coursetagobjects as $tag) {
1547  
1548                  // We should only be seeing course tags from $course. The tags from
1549                  // $siblingcourse should have been filtered out.
1550                  $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1551              }
1552          }
1553      }
1554  
1555      /**
1556       * Data provider for tests of question_has_capability_on_context and question_require_capability_on_context.
1557       *
1558       * @return  array
1559       */
1560      public function question_capability_on_question_provider() {
1561          return [
1562              'Unrelated capability which is present' => [
1563                  'capabilities' => [
1564                      'moodle/question:config' => CAP_ALLOW,
1565                  ],
1566                  'testcapability' => 'config',
1567                  'isowner' => true,
1568                  'expect' => true,
1569              ],
1570              'Unrelated capability which is present (not owner)' => [
1571                  'capabilities' => [
1572                      'moodle/question:config' => CAP_ALLOW,
1573                  ],
1574                  'testcapability' => 'config',
1575                  'isowner' => false,
1576                  'expect' => true,
1577              ],
1578              'Unrelated capability which is not set' => [
1579                  'capabilities' => [
1580                  ],
1581                  'testcapability' => 'config',
1582                  'isowner' => true,
1583                  'expect' => false,
1584              ],
1585              'Unrelated capability which is not set (not owner)' => [
1586                  'capabilities' => [
1587                  ],
1588                  'testcapability' => 'config',
1589                  'isowner' => false,
1590                  'expect' => false,
1591              ],
1592              'Unrelated capability which is prevented' => [
1593                  'capabilities' => [
1594                      'moodle/question:config' => CAP_PREVENT,
1595                  ],
1596                  'testcapability' => 'config',
1597                  'isowner' => true,
1598                  'expect' => false,
1599              ],
1600              'Unrelated capability which is prevented (not owner)' => [
1601                  'capabilities' => [
1602                      'moodle/question:config' => CAP_PREVENT,
1603                  ],
1604                  'testcapability' => 'config',
1605                  'isowner' => false,
1606                  'expect' => false,
1607              ],
1608              'Related capability which is not set' => [
1609                  'capabilities' => [
1610                  ],
1611                  'testcapability' => 'edit',
1612                  'isowner' => true,
1613                  'expect' => false,
1614              ],
1615              'Related capability which is not set (not owner)' => [
1616                  'capabilities' => [
1617                  ],
1618                  'testcapability' => 'edit',
1619                  'isowner' => false,
1620                  'expect' => false,
1621              ],
1622              'Related capability which is allowed at all, unset at mine' => [
1623                  'capabilities' => [
1624                      'moodle/question:editall' => CAP_ALLOW,
1625                  ],
1626                  'testcapability' => 'edit',
1627                  'isowner' => true,
1628                  'expect' => true,
1629              ],
1630              'Related capability which is allowed at all, unset at mine (not owner)' => [
1631                  'capabilities' => [
1632                      'moodle/question:editall' => CAP_ALLOW,
1633                  ],
1634                  'testcapability' => 'edit',
1635                  'isowner' => false,
1636                  'expect' => true,
1637              ],
1638              'Related capability which is allowed at all, prevented at mine' => [
1639                  'capabilities' => [
1640                      'moodle/question:editall' => CAP_ALLOW,
1641                      'moodle/question:editmine' => CAP_PREVENT,
1642                  ],
1643                  'testcapability' => 'edit',
1644                  'isowner' => true,
1645                  'expect' => true,
1646              ],
1647              'Related capability which is allowed at all, prevented at mine (not owner)' => [
1648                  'capabilities' => [
1649                      'moodle/question:editall' => CAP_ALLOW,
1650                      'moodle/question:editmine' => CAP_PREVENT,
1651                  ],
1652                  'testcapability' => 'edit',
1653                  'isowner' => false,
1654                  'expect' => true,
1655              ],
1656              'Related capability which is unset all, allowed at mine' => [
1657                  'capabilities' => [
1658                      'moodle/question:editall' => CAP_PREVENT,
1659                      'moodle/question:editmine' => CAP_ALLOW,
1660                  ],
1661                  'testcapability' => 'edit',
1662                  'isowner' => true,
1663                  'expect' => true,
1664              ],
1665              'Related capability which is unset all, allowed at mine (not owner)' => [
1666                  'capabilities' => [
1667                      'moodle/question:editall' => CAP_PREVENT,
1668                      'moodle/question:editmine' => CAP_ALLOW,
1669                  ],
1670                  'testcapability' => 'edit',
1671                  'isowner' => false,
1672                  'expect' => false,
1673              ],
1674          ];
1675      }
1676  
1677      /**
1678       * Tests that question_has_capability_on does not throw exception on broken questions.
1679       */
1680      public function test_question_has_capability_on_broken_question() {
1681          global $DB;
1682  
1683          // Create the test data.
1684          $generator = $this->getDataGenerator();
1685          $questiongenerator = $generator->get_plugin_generator('core_question');
1686  
1687          $category = $generator->create_category();
1688          $context = \context_coursecat::instance($category->id);
1689          $questioncat = $questiongenerator->create_question_category([
1690              'contextid' => $context->id,
1691          ]);
1692  
1693          // Create a cloze question.
1694          $question = $questiongenerator->create_question('multianswer', null, [
1695              'category' => $questioncat->id,
1696          ]);
1697          // Now, break the question.
1698          $DB->delete_records('question_multianswer', ['question' => $question->id]);
1699  
1700          $this->setAdminUser();
1701  
1702          $result = question_has_capability_on($question->id, 'tag');
1703          $this->assertTrue($result);
1704  
1705          $this->assertDebuggingCalled();
1706      }
1707  
1708      /**
1709       * Tests for the deprecated question_has_capability_on function when passing a stdClass as parameter.
1710       *
1711       * @dataProvider question_capability_on_question_provider
1712       * @param   array   $capabilities The capability assignments to set.
1713       * @param   string  $capability The capability to test
1714       * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1715       * @param   bool    $expect The expected result.
1716       */
1717      public function test_question_has_capability_on_using_stdClass($capabilities, $capability, $isowner, $expect) {
1718          $this->resetAfterTest();
1719  
1720          // Create the test data.
1721          $user = $this->getDataGenerator()->create_user();
1722          $otheruser = $this->getDataGenerator()->create_user();
1723          $roleid = $this->getDataGenerator()->create_role();
1724          $category = $this->getDataGenerator()->create_category();
1725          $context = \context_coursecat::instance($category->id);
1726  
1727          // Assign the user to the role.
1728          role_assign($roleid, $user->id, $context->id);
1729  
1730          // Assign the capabilities to the role.
1731          foreach ($capabilities as $capname => $capvalue) {
1732              assign_capability($capname, $capvalue, $roleid, $context->id);
1733          }
1734  
1735          $this->setUser($user);
1736  
1737          // The current fake question we make use of is always a stdClass and typically has no ID.
1738          $fakequestion = (object) [
1739              'contextid' => $context->id,
1740          ];
1741  
1742          if ($isowner) {
1743              $fakequestion->createdby = $user->id;
1744          } else {
1745              $fakequestion->createdby = $otheruser->id;
1746          }
1747  
1748          $result = question_has_capability_on($fakequestion, $capability);
1749          $this->assertEquals($expect, $result);
1750      }
1751  
1752      /**
1753       * Tests for the deprecated question_has_capability_on function when using question definition.
1754       *
1755       * @dataProvider question_capability_on_question_provider
1756       * @param   array   $capabilities The capability assignments to set.
1757       * @param   string  $capability The capability to test
1758       * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1759       * @param   bool    $expect The expected result.
1760       */
1761      public function test_question_has_capability_on_using_question_definition($capabilities, $capability, $isowner, $expect) {
1762          $this->resetAfterTest();
1763  
1764          // Create the test data.
1765          $generator = $this->getDataGenerator();
1766          $questiongenerator = $generator->get_plugin_generator('core_question');
1767          $user = $generator->create_user();
1768          $otheruser = $generator->create_user();
1769          $roleid = $generator->create_role();
1770          $category = $generator->create_category();
1771          $context = \context_coursecat::instance($category->id);
1772          $questioncat = $questiongenerator->create_question_category([
1773              'contextid' => $context->id,
1774          ]);
1775  
1776          // Assign the user to the role.
1777          role_assign($roleid, $user->id, $context->id);
1778  
1779          // Assign the capabilities to the role.
1780          foreach ($capabilities as $capname => $capvalue) {
1781              assign_capability($capname, $capvalue, $roleid, $context->id);
1782          }
1783  
1784          // Create the question.
1785          $qtype = 'truefalse';
1786          $overrides = [
1787              'category' => $questioncat->id,
1788          ];
1789  
1790          $question = $questiongenerator->create_question($qtype, null, $overrides);
1791  
1792          // The question generator does not support setting of the createdby for some reason.
1793          $question->createdby = ($isowner) ? $user->id : $otheruser->id;
1794          $fromform = \test_question_maker::get_question_form_data($qtype, null);
1795          $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
1796          question_bank::get_qtype($qtype)->save_question($question, $fromform);
1797  
1798          $this->setUser($user);
1799          $result = question_has_capability_on($question, $capability);
1800          $this->assertEquals($expect, $result);
1801      }
1802  
1803      /**
1804       * Tests for the deprecated question_has_capability_on function when using a real question id.
1805       *
1806       * @dataProvider question_capability_on_question_provider
1807       * @param   array   $capabilities The capability assignments to set.
1808       * @param   string  $capability The capability to test
1809       * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1810       * @param   bool    $expect The expected result.
1811       */
1812      public function test_question_has_capability_on_using_question_id($capabilities, $capability, $isowner, $expect) {
1813          $this->resetAfterTest();
1814  
1815          // Create the test data.
1816          $generator = $this->getDataGenerator();
1817          $questiongenerator = $generator->get_plugin_generator('core_question');
1818          $user = $generator->create_user();
1819          $otheruser = $generator->create_user();
1820          $roleid = $generator->create_role();
1821          $category = $generator->create_category();
1822          $context = \context_coursecat::instance($category->id);
1823          $questioncat = $questiongenerator->create_question_category([
1824              'contextid' => $context->id,
1825          ]);
1826  
1827          // Assign the user to the role.
1828          role_assign($roleid, $user->id, $context->id);
1829  
1830          // Assign the capabilities to the role.
1831          foreach ($capabilities as $capname => $capvalue) {
1832              assign_capability($capname, $capvalue, $roleid, $context->id);
1833          }
1834  
1835          // Create the question.
1836          $qtype = 'truefalse';
1837          $overrides = [
1838              'category' => $questioncat->id,
1839          ];
1840  
1841          $question = $questiongenerator->create_question($qtype, null, $overrides);
1842  
1843          // The question generator does not support setting of the createdby for some reason.
1844          $question->createdby = ($isowner) ? $user->id : $otheruser->id;
1845          $fromform = \test_question_maker::get_question_form_data($qtype, null);
1846          $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
1847          question_bank::get_qtype($qtype)->save_question($question, $fromform);
1848  
1849          $this->setUser($user);
1850          $result = question_has_capability_on($question->id, $capability);
1851          $this->assertEquals($expect, $result);
1852      }
1853  
1854      /**
1855       * Tests for the deprecated question_has_capability_on function when using a string as question id.
1856       *
1857       * @dataProvider question_capability_on_question_provider
1858       * @param   array   $capabilities The capability assignments to set.
1859       * @param   string  $capability The capability to test
1860       * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1861       * @param   bool    $expect The expected result.
1862       */
1863      public function test_question_has_capability_on_using_question_string_id($capabilities, $capability, $isowner, $expect) {
1864          $this->resetAfterTest();
1865  
1866          // Create the test data.
1867          $generator = $this->getDataGenerator();
1868          $questiongenerator = $generator->get_plugin_generator('core_question');
1869          $user = $generator->create_user();
1870          $otheruser = $generator->create_user();
1871          $roleid = $generator->create_role();
1872          $category = $generator->create_category();
1873          $context = \context_coursecat::instance($category->id);
1874          $questioncat = $questiongenerator->create_question_category([
1875              'contextid' => $context->id,
1876          ]);
1877  
1878          // Assign the user to the role.
1879          role_assign($roleid, $user->id, $context->id);
1880  
1881          // Assign the capabilities to the role.
1882          foreach ($capabilities as $capname => $capvalue) {
1883              assign_capability($capname, $capvalue, $roleid, $context->id);
1884          }
1885  
1886          // Create the question.
1887          $qtype = 'truefalse';
1888          $overrides = [
1889              'category' => $questioncat->id,
1890          ];
1891  
1892          $question = $questiongenerator->create_question($qtype, null, $overrides);
1893  
1894          // The question generator does not support setting of the createdby for some reason.
1895          $question->createdby = ($isowner) ? $user->id : $otheruser->id;
1896          $fromform = \test_question_maker::get_question_form_data($qtype, null);
1897          $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
1898          question_bank::get_qtype($qtype)->save_question($question, $fromform);
1899  
1900          $this->setUser($user);
1901          $result = question_has_capability_on((string) $question->id, $capability);
1902          $this->assertEquals($expect, $result);
1903      }
1904  
1905      /**
1906       * Tests for the question_has_capability_on function when using a moved question.
1907       *
1908       * @dataProvider question_capability_on_question_provider
1909       * @param   array   $capabilities The capability assignments to set.
1910       * @param   string  $capability The capability to test
1911       * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1912       * @param   bool    $expect The expected result.
1913       */
1914      public function test_question_has_capability_on_using_moved_question($capabilities, $capability, $isowner, $expect) {
1915          $this->resetAfterTest();
1916  
1917          // Create the test data.
1918          $generator = $this->getDataGenerator();
1919          $questiongenerator = $generator->get_plugin_generator('core_question');
1920          $user = $generator->create_user();
1921          $otheruser = $generator->create_user();
1922          $roleid = $generator->create_role();
1923          $category = $generator->create_category();
1924          $context = \context_coursecat::instance($category->id);
1925          $questioncat = $questiongenerator->create_question_category([
1926              'contextid' => $context->id,
1927          ]);
1928  
1929          $newcategory = $generator->create_category();
1930          $newcontext = \context_coursecat::instance($newcategory->id);
1931          $newquestioncat = $questiongenerator->create_question_category([
1932              'contextid' => $newcontext->id,
1933          ]);
1934  
1935          // Assign the user to the role in the _new_ context..
1936          role_assign($roleid, $user->id, $newcontext->id);
1937  
1938          // Assign the capabilities to the role in the _new_ context.
1939          foreach ($capabilities as $capname => $capvalue) {
1940              assign_capability($capname, $capvalue, $roleid, $newcontext->id);
1941          }
1942  
1943          // Create the question.
1944          $qtype = 'truefalse';
1945          $overrides = [
1946              'category' => $questioncat->id,
1947          ];
1948  
1949          $question = $questiongenerator->create_question($qtype, null, $overrides);
1950  
1951          // The question generator does not support setting of the createdby for some reason.
1952          $question->createdby = ($isowner) ? $user->id : $otheruser->id;
1953          $fromform = \test_question_maker::get_question_form_data($qtype, null);
1954          $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
1955          question_bank::get_qtype($qtype)->save_question($question, $fromform);
1956  
1957          // Move the question.
1958          question_move_questions_to_category([$question->id], $newquestioncat->id);
1959  
1960          // Test that the capability is correct after the question has been moved.
1961          $this->setUser($user);
1962          $result = question_has_capability_on($question->id, $capability);
1963          $this->assertEquals($expect, $result);
1964      }
1965  
1966      /**
1967       * Tests for the question_has_capability_on function when using a real question.
1968       *
1969       * @dataProvider question_capability_on_question_provider
1970       * @param   array   $capabilities The capability assignments to set.
1971       * @param   string  $capability The capability to test
1972       * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1973       * @param   bool    $expect The expected result.
1974       */
1975      public function test_question_has_capability_on_using_question($capabilities, $capability, $isowner, $expect) {
1976          $this->resetAfterTest();
1977  
1978          // Create the test data.
1979          $generator = $this->getDataGenerator();
1980          $questiongenerator = $generator->get_plugin_generator('core_question');
1981          $user = $generator->create_user();
1982          $otheruser = $generator->create_user();
1983          $roleid = $generator->create_role();
1984          $category = $generator->create_category();
1985          $context = \context_coursecat::instance($category->id);
1986          $questioncat = $questiongenerator->create_question_category([
1987              'contextid' => $context->id,
1988          ]);
1989  
1990          // Assign the user to the role.
1991          role_assign($roleid, $user->id, $context->id);
1992  
1993          // Assign the capabilities to the role.
1994          foreach ($capabilities as $capname => $capvalue) {
1995              assign_capability($capname, $capvalue, $roleid, $context->id);
1996          }
1997  
1998          // Create the question.
1999          $question = $questiongenerator->create_question('truefalse', null, [
2000              'category' => $questioncat->id,
2001          ]);
2002          $question = question_bank::load_question_data($question->id);
2003  
2004          // The question generator does not support setting of the createdby for some reason.
2005          $question->createdby = ($isowner) ? $user->id : $otheruser->id;
2006  
2007          $this->setUser($user);
2008          $result = question_has_capability_on($question, $capability);
2009          $this->assertEquals($expect, $result);
2010      }
2011  
2012      /**
2013       * Tests that question_has_capability_on throws an exception for wrong parameter types.
2014       */
2015      public function test_question_has_capability_on_wrong_param_type() {
2016          // Create the test data.
2017          $generator = $this->getDataGenerator();
2018          $questiongenerator = $generator->get_plugin_generator('core_question');
2019          $user = $generator->create_user();
2020  
2021          $category = $generator->create_category();
2022          $context = \context_coursecat::instance($category->id);
2023          $questioncat = $questiongenerator->create_question_category([
2024              'contextid' => $context->id,
2025          ]);
2026  
2027          // Create the question.
2028          $question = $questiongenerator->create_question('truefalse', null, [
2029              'category' => $questioncat->id,
2030          ]);
2031          $question = question_bank::load_question_data($question->id);
2032  
2033          // The question generator does not support setting of the createdby for some reason.
2034          $question->createdby = $user->id;
2035  
2036          $this->setUser($user);
2037          $result = question_has_capability_on((string)$question->id, 'tag');
2038          $this->assertFalse($result);
2039  
2040          $this->expectException('coding_exception');
2041          $this->expectExceptionMessage('$questionorid parameter needs to be an integer or an object.');
2042          question_has_capability_on('one', 'tag');
2043      }
2044  
2045      /**
2046       * Test of question_categorylist_parents function.
2047       */
2048      public function test_question_categorylist_parents() {
2049          $this->resetAfterTest();
2050          $generator = $this->getDataGenerator();
2051          $questiongenerator = $generator->get_plugin_generator('core_question');
2052          $category = $generator->create_category();
2053          $context = \context_coursecat::instance($category->id);
2054          // Create a top category.
2055          $cat0 = question_get_top_category($context->id, true);
2056          // Add sub-categories.
2057          $cat1 = $questiongenerator->create_question_category(['parent' => $cat0->id]);
2058          $cat2 = $questiongenerator->create_question_category(['parent' => $cat1->id]);
2059          // Test the 'get parents' function.
2060          $parentcategories = question_categorylist_parents($cat2->id);
2061          $this->assertEquals($cat0->id, $parentcategories[0]);
2062          $this->assertEquals($cat1->id, $parentcategories[1]);
2063          $this->assertCount(2, $parentcategories);
2064      }
2065  
2066      public function test_question_get_export_single_question_url() {
2067          $generator = $this->getDataGenerator();
2068  
2069          // Create a course and an activity.
2070          $course = $generator->create_course();
2071          $quiz = $generator->create_module('quiz', ['course' => $course->id]);
2072  
2073          // Create a question in each place.
2074          $questiongenerator = $generator->get_plugin_generator('core_question');
2075          $courseqcat = $questiongenerator->create_question_category(['contextid' => \context_course::instance($course->id)->id]);
2076          $courseq = $questiongenerator->create_question('truefalse', null, ['category' => $courseqcat->id]);
2077          $quizqcat = $questiongenerator->create_question_category(['contextid' => \context_module::instance($quiz->cmid)->id]);
2078          $quizq = $questiongenerator->create_question('truefalse', null, ['category' => $quizqcat->id]);
2079          $systemqcat = $questiongenerator->create_question_category();
2080          $systemq = $questiongenerator->create_question('truefalse', null, ['category' => $systemqcat->id]);
2081  
2082          // Verify some URLs.
2083          $this->assertEquals(new \moodle_url('/question/exportone.php',
2084                  ['id' => $courseq->id, 'courseid' => $course->id, 'sesskey' => sesskey()]),
2085                  question_get_export_single_question_url(question_bank::load_question_data($courseq->id)));
2086  
2087          $this->assertEquals(new \moodle_url('/question/exportone.php',
2088                  ['id' => $quizq->id, 'cmid' => $quiz->cmid, 'sesskey' => sesskey()]),
2089                  question_get_export_single_question_url(question_bank::load_question($quizq->id)));
2090  
2091          $this->assertEquals(new \moodle_url('/question/exportone.php',
2092                  ['id' => $systemq->id, 'courseid' => SITEID, 'sesskey' => sesskey()]),
2093                  question_get_export_single_question_url(question_bank::load_question($systemq->id)));
2094      }
2095  
2096      /**
2097       * Get test cases for test_core_question_find_next_unused_idnumber.
2098       *
2099       * @return array test cases.
2100       */
2101      public function find_next_unused_idnumber_cases(): array {
2102          return [
2103              ['id', null],
2104              ['id1a', null],
2105              ['id001', 'id002'],
2106              ['id9', 'id10'],
2107              ['id009', 'id010'],
2108              ['id999', 'id1000'],
2109              ['0', '1'],
2110              ['-1', '-2'],
2111              ['01', '02'],
2112              ['09', '10'],
2113              ['1.0E+29', '1.0E+30'], // Idnumbers are strings, not floats.
2114              ['1.0E-29', '1.0E-30'], // By the way, this is not a sensible idnumber!
2115              ['10.1', '10.2'],
2116              ['10.9', '10.10'],
2117  
2118          ];
2119      }
2120  
2121      /**
2122       * Test core_question_find_next_unused_idnumber in the case when there are no other questions.
2123       *
2124       * @dataProvider find_next_unused_idnumber_cases
2125       * @param string $oldidnumber value to pass to core_question_find_next_unused_idnumber.
2126       * @param string|null $expectednewidnumber expected result.
2127       */
2128      public function test_core_question_find_next_unused_idnumber(string $oldidnumber, ?string $expectednewidnumber) {
2129          $this->assertSame($expectednewidnumber, core_question_find_next_unused_idnumber($oldidnumber, 0));
2130      }
2131  
2132      public function test_core_question_find_next_unused_idnumber_skips_used() {
2133          $this->resetAfterTest();
2134  
2135          /** @var core_question_generator $generator */
2136          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
2137          $category = $generator->create_question_category();
2138          $othercategory = $generator->create_question_category();
2139          $generator->create_question('truefalse', null, ['category' => $category->id, 'idnumber' => 'id9']);
2140          $generator->create_question('truefalse', null, ['category' => $category->id, 'idnumber' => 'id10']);
2141          // Next one to make sure only idnumbers from the right category are ruled out.
2142          $generator->create_question('truefalse', null, ['category' => $othercategory->id, 'idnumber' => 'id11']);
2143  
2144          $this->assertSame('id11', core_question_find_next_unused_idnumber('id9', $category->id));
2145          $this->assertSame('id11', core_question_find_next_unused_idnumber('id8', $category->id));
2146      }
2147  }