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