Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 311 and 400] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       1  <?php
       2  // This file is part of Moodle - http://moodle.org/
       3  //
       4  // Moodle is free software: you can redistribute it and/or modify
       5  // it under the terms of the GNU General Public License as published by
       6  // the Free Software Foundation, either version 3 of the License, or
       7  // (at your option) any later version.
       8  //
       9  // Moodle is distributed in the hope that it will be useful,
      10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
      11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12  // GNU General Public License for more details.
      13  //
      14  // You should have received a copy of the GNU General Public License
      15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
      16  
      17  /**
      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  }