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