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