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