Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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