Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 402 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace core_question; 18 19 use qubaid_list; 20 use question_bank; 21 use question_engine; 22 23 /** 24 * Tests for the {@see core_question\local\bank\random_question_loader} class. 25 * 26 * @package core_question 27 * @copyright 2015 The Open University 28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 */ 30 class random_question_loader_test extends \advanced_testcase { 31 32 public function test_empty_category_gives_null() { 33 $this->resetAfterTest(); 34 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 35 36 $cat = $generator->create_question_category(); 37 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 38 39 $this->assertNull($loader->get_next_question_id($cat->id, 0)); 40 $this->assertNull($loader->get_next_question_id($cat->id, 1)); 41 } 42 43 public function test_unknown_category_behaves_like_empty() { 44 // It is up the caller to make sure the category id is valid. 45 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 46 $this->assertNull($loader->get_next_question_id(-1, 1)); 47 } 48 49 public function test_descriptions_not_returned() { 50 $this->resetAfterTest(); 51 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 52 53 $cat = $generator->create_question_category(); 54 $info = $generator->create_question('description', null, ['category' => $cat->id]); 55 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 56 57 $this->assertNull($loader->get_next_question_id($cat->id, 0)); 58 } 59 60 public function test_hidden_questions_not_returned() { 61 global $DB; 62 $this->resetAfterTest(); 63 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 64 65 $cat = $generator->create_question_category(); 66 $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); 67 $DB->set_field('question_versions', 'status', 68 \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN, ['questionid' => $question1->id]); 69 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 70 71 $this->assertNull($loader->get_next_question_id($cat->id, 0)); 72 } 73 74 public function test_cloze_subquestions_not_returned() { 75 $this->resetAfterTest(); 76 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 77 78 $cat = $generator->create_question_category(); 79 $question1 = $generator->create_question('multianswer', null, ['category' => $cat->id]); 80 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 81 82 $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0)); 83 $this->assertNull($loader->get_next_question_id($cat->id, 0)); 84 } 85 86 public function test_random_questions_not_returned() { 87 $this->resetAfterTest(); 88 $this->setAdminUser(); 89 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 90 91 $cat = $generator->create_question_category(); 92 $course = $this->getDataGenerator()->create_course(); 93 $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course]); 94 quiz_add_random_questions($quiz, 1, $cat->id, 1, false); 95 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 96 97 $this->assertNull($loader->get_next_question_id($cat->id, 0)); 98 } 99 100 public function test_one_question_category_returns_that_q_then_null() { 101 $this->resetAfterTest(); 102 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 103 104 $cat = $generator->create_question_category(); 105 $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); 106 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 107 108 $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 1)); 109 $this->assertNull($loader->get_next_question_id($cat->id, 0)); 110 } 111 112 public function test_two_question_category_returns_both_then_null() { 113 $this->resetAfterTest(); 114 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 115 116 $cat = $generator->create_question_category(); 117 $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); 118 $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); 119 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 120 121 $questionids = []; 122 $questionids[] = $loader->get_next_question_id($cat->id, 0); 123 $questionids[] = $loader->get_next_question_id($cat->id, 0); 124 sort($questionids); 125 $this->assertEquals([$question1->id, $question2->id], $questionids); 126 127 $this->assertNull($loader->get_next_question_id($cat->id, 1)); 128 } 129 130 public function test_nested_categories() { 131 $this->resetAfterTest(); 132 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 133 134 $cat1 = $generator->create_question_category(); 135 $cat2 = $generator->create_question_category(['parent' => $cat1->id]); 136 $question1 = $generator->create_question('shortanswer', null, ['category' => $cat1->id]); 137 $question2 = $generator->create_question('shortanswer', null, ['category' => $cat2->id]); 138 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 139 140 $this->assertEquals($question2->id, $loader->get_next_question_id($cat2->id, 1)); 141 $this->assertEquals($question1->id, $loader->get_next_question_id($cat1->id, 1)); 142 143 $this->assertNull($loader->get_next_question_id($cat1->id, 0)); 144 } 145 146 public function test_used_question_not_returned_until_later() { 147 $this->resetAfterTest(); 148 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 149 150 $cat = $generator->create_question_category(); 151 $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); 152 $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); 153 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]), 154 array($question2->id => 2)); 155 156 $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0)); 157 $this->assertNull($loader->get_next_question_id($cat->id, 0)); 158 } 159 160 public function test_previously_used_question_not_returned_until_later() { 161 $this->resetAfterTest(); 162 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 163 164 $cat = $generator->create_question_category(); 165 $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); 166 $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); 167 $quba = question_engine::make_questions_usage_by_activity('test', \context_system::instance()); 168 $quba->set_preferred_behaviour('deferredfeedback'); 169 $question = question_bank::load_question($question2->id); 170 $quba->add_question($question); 171 $quba->add_question($question); 172 $quba->start_all_questions(); 173 question_engine::save_questions_usage_by_activity($quba); 174 175 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array($quba->get_id()))); 176 177 $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0)); 178 $this->assertEquals($question2->id, $loader->get_next_question_id($cat->id, 0)); 179 $this->assertNull($loader->get_next_question_id($cat->id, 0)); 180 } 181 182 public function test_empty_category_does_not_have_question_available() { 183 $this->resetAfterTest(); 184 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 185 186 $cat = $generator->create_question_category(); 187 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array())); 188 189 $this->assertFalse($loader->is_question_available($cat->id, 0, 1)); 190 $this->assertFalse($loader->is_question_available($cat->id, 1, 1)); 191 } 192 193 public function test_descriptions_not_available() { 194 $this->resetAfterTest(); 195 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 196 197 $cat = $generator->create_question_category(); 198 $info = $generator->create_question('description', null, array('category' => $cat->id)); 199 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array())); 200 201 $this->assertFalse($loader->is_question_available($cat->id, 0, $info->id)); 202 $this->assertFalse($loader->is_question_available($cat->id, 1, $info->id)); 203 } 204 205 public function test_existing_question_is_available_but_then_marked_used() { 206 $this->resetAfterTest(); 207 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 208 209 $cat = $generator->create_question_category(); 210 $question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); 211 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array())); 212 213 $this->assertTrue($loader->is_question_available($cat->id, 0, $question1->id)); 214 $this->assertFalse($loader->is_question_available($cat->id, 0, $question1->id)); 215 216 $this->assertFalse($loader->is_question_available($cat->id, 0, -1)); 217 } 218 219 /** 220 * Data provider for the get_questions test. 221 */ 222 public function get_questions_test_cases() { 223 return [ 224 'empty category' => [ 225 'categoryindex' => 'emptycat', 226 'includesubcategories' => false, 227 'usetagnames' => [], 228 'expectedquestionindexes' => [] 229 ], 230 'single category' => [ 231 'categoryindex' => 'cat1', 232 'includesubcategories' => false, 233 'usetagnames' => [], 234 'expectedquestionindexes' => ['cat1q1', 'cat1q2'] 235 ], 236 'include sub category' => [ 237 'categoryindex' => 'cat1', 238 'includesubcategories' => true, 239 'usetagnames' => [], 240 'expectedquestionindexes' => ['cat1q1', 'cat1q2', 'subcatq1', 'subcatq2'] 241 ], 242 'single category with tags' => [ 243 'categoryindex' => 'cat1', 244 'includesubcategories' => false, 245 'usetagnames' => ['cat1'], 246 'expectedquestionindexes' => ['cat1q1'] 247 ], 248 'include sub category with tag on parent' => [ 249 'categoryindex' => 'cat1', 250 'includesubcategories' => true, 251 'usetagnames' => ['cat1'], 252 'expectedquestionindexes' => ['cat1q1'] 253 ], 254 'include sub category with tag on sub' => [ 255 'categoryindex' => 'cat1', 256 'includesubcategories' => true, 257 'usetagnames' => ['subcat'], 258 'expectedquestionindexes' => ['subcatq1'] 259 ], 260 'include sub category with same tag on parent and sub' => [ 261 'categoryindex' => 'cat1', 262 'includesubcategories' => true, 263 'usetagnames' => ['foo'], 264 'expectedquestionindexes' => ['cat1q1', 'subcatq1'] 265 ], 266 'include sub category with tag not matching' => [ 267 'categoryindex' => 'cat1', 268 'includesubcategories' => true, 269 'usetagnames' => ['cat1', 'cat2'], 270 'expectedquestionindexes' => [] 271 ] 272 ]; 273 } 274 275 /** 276 * Test the get_questions function with various parameter combinations. 277 * 278 * This function creates a data set as follows: 279 * Category: cat1 280 * Question: cat1q1 281 * Tags: 'cat1', 'foo' 282 * Question: cat1q2 283 * Category: cat2 284 * Question: cat2q1 285 * Tags: 'cat2', 'foo' 286 * Question: cat2q2 287 * Category: subcat 288 * Question: subcatq1 289 * Tags: 'subcat', 'foo' 290 * Question: subcatq2 291 * Parent: cat1 292 * Category: emptycat 293 * 294 * @dataProvider get_questions_test_cases() 295 * @param string $categoryindex The named index for the category to use 296 * @param bool $includesubcategories If the search should include subcategories 297 * @param string[] $usetagnames The tag names to include in the search 298 * @param string[] $expectedquestionindexes The questions expected in the result 299 */ 300 public function test_get_questions_variations( 301 $categoryindex, 302 $includesubcategories, 303 $usetagnames, 304 $expectedquestionindexes 305 ) { 306 $this->resetAfterTest(); 307 308 $categories = []; 309 $questions = []; 310 $tagnames = [ 311 'cat1', 312 'cat2', 313 'subcat', 314 'foo' 315 ]; 316 $collid = \core_tag_collection::get_default(); 317 $tags = \core_tag_tag::create_if_missing($collid, $tagnames); 318 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 319 320 // First category and questions. 321 list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat1', 'foo']); 322 $categories['cat1'] = $category; 323 $questions['cat1q1'] = $categoryquestions[0]; 324 $questions['cat1q2'] = $categoryquestions[1]; 325 // Second category and questions. 326 list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat2', 'foo']); 327 $categories['cat2'] = $category; 328 $questions['cat2q1'] = $categoryquestions[0]; 329 $questions['cat2q2'] = $categoryquestions[1]; 330 // Sub category and questions. 331 list($category, $categoryquestions) = $this->create_category_and_questions(2, ['subcat', 'foo'], $categories['cat1']); 332 $categories['subcat'] = $category; 333 $questions['subcatq1'] = $categoryquestions[0]; 334 $questions['subcatq2'] = $categoryquestions[1]; 335 // Empty category. 336 list($category, $categoryquestions) = $this->create_category_and_questions(0); 337 $categories['emptycat'] = $category; 338 339 // Generate the arguments for the get_questions function. 340 $category = $categories[$categoryindex]; 341 $tagids = array_map(function($tagname) use ($tags) { 342 return $tags[$tagname]->id; 343 }, $usetagnames); 344 345 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 346 $result = $loader->get_questions($category->id, $includesubcategories, $tagids); 347 // Generate the expected question set. 348 $expectedquestions = array_map(function($index) use ($questions) { 349 return $questions[$index]; 350 }, $expectedquestionindexes); 351 352 // Ensure the result matches what was expected. 353 $this->assertCount(count($expectedquestions), $result); 354 foreach ($expectedquestions as $question) { 355 $this->assertEquals($result[$question->id]->id, $question->id); 356 $this->assertEquals($result[$question->id]->category, $question->category); 357 } 358 } 359 360 /** 361 * get_questions should allow limiting and offsetting of the result set. 362 */ 363 public function test_get_questions_with_limit_and_offset() { 364 $this->resetAfterTest(); 365 $numberofquestions = 5; 366 $includesubcategories = false; 367 $tagids = []; 368 $limit = 1; 369 $offset = 0; 370 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 371 list($category, $questions) = $this->create_category_and_questions($numberofquestions); 372 373 // Add questionid as key to find them easily later. 374 $questionsbyid = []; 375 array_walk($questions, function (&$value) use (&$questionsbyid) { 376 $questionsbyid[$value->id] = $value; 377 }); 378 379 for ($i = 0; $i < $numberofquestions; $i++) { 380 $result = $loader->get_questions( 381 $category->id, 382 $includesubcategories, 383 $tagids, 384 $limit, 385 $offset 386 ); 387 388 $this->assertCount($limit, $result); 389 $actual = array_shift($result); 390 $expected = $questionsbyid[$actual->id]; 391 $this->assertEquals($expected->id, $actual->id); 392 $offset++; 393 } 394 } 395 396 /** 397 * get_questions should allow retrieving questions with only a subset of 398 * fields populated. 399 */ 400 public function test_get_questions_with_restricted_fields() { 401 $this->resetAfterTest(); 402 $includesubcategories = false; 403 $tagids = []; 404 $limit = 10; 405 $offset = 0; 406 $fields = ['id', 'name']; 407 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 408 list($category, $questions) = $this->create_category_and_questions(1); 409 410 $result = $loader->get_questions( 411 $category->id, 412 $includesubcategories, 413 $tagids, 414 $limit, 415 $offset, 416 $fields 417 ); 418 419 $expectedquestion = array_shift($questions); 420 $actualquestion = array_shift($result); 421 $actualfields = get_object_vars($actualquestion); 422 $actualfields = array_keys($actualfields); 423 sort($actualfields); 424 sort($fields); 425 426 $this->assertEquals($fields, $actualfields); 427 } 428 429 /** 430 * Data provider for the count_questions test. 431 */ 432 public function count_questions_test_cases() { 433 return [ 434 'empty category' => [ 435 'categoryindex' => 'emptycat', 436 'includesubcategories' => false, 437 'usetagnames' => [], 438 'expectedcount' => 0 439 ], 440 'single category' => [ 441 'categoryindex' => 'cat1', 442 'includesubcategories' => false, 443 'usetagnames' => [], 444 'expectedcount' => 2 445 ], 446 'include sub category' => [ 447 'categoryindex' => 'cat1', 448 'includesubcategories' => true, 449 'usetagnames' => [], 450 'expectedcount' => 4 451 ], 452 'single category with tags' => [ 453 'categoryindex' => 'cat1', 454 'includesubcategories' => false, 455 'usetagnames' => ['cat1'], 456 'expectedcount' => 1 457 ], 458 'include sub category with tag on parent' => [ 459 'categoryindex' => 'cat1', 460 'includesubcategories' => true, 461 'usetagnames' => ['cat1'], 462 'expectedcount' => 1 463 ], 464 'include sub category with tag on sub' => [ 465 'categoryindex' => 'cat1', 466 'includesubcategories' => true, 467 'usetagnames' => ['subcat'], 468 'expectedcount' => 1 469 ], 470 'include sub category with same tag on parent and sub' => [ 471 'categoryindex' => 'cat1', 472 'includesubcategories' => true, 473 'usetagnames' => ['foo'], 474 'expectedcount' => 2 475 ], 476 'include sub category with tag not matching' => [ 477 'categoryindex' => 'cat1', 478 'includesubcategories' => true, 479 'usetagnames' => ['cat1', 'cat2'], 480 'expectedcount' => 0 481 ] 482 ]; 483 } 484 485 /** 486 * Test the count_questions function with various parameter combinations. 487 * 488 * This function creates a data set as follows: 489 * Category: cat1 490 * Question: cat1q1 491 * Tags: 'cat1', 'foo' 492 * Question: cat1q2 493 * Category: cat2 494 * Question: cat2q1 495 * Tags: 'cat2', 'foo' 496 * Question: cat2q2 497 * Category: subcat 498 * Question: subcatq1 499 * Tags: 'subcat', 'foo' 500 * Question: subcatq2 501 * Parent: cat1 502 * Category: emptycat 503 * 504 * @dataProvider count_questions_test_cases() 505 * @param string $categoryindex The named index for the category to use 506 * @param bool $includesubcategories If the search should include subcategories 507 * @param string[] $usetagnames The tag names to include in the search 508 * @param int $expectedcount The number of questions expected in the result 509 */ 510 public function test_count_questions_variations( 511 $categoryindex, 512 $includesubcategories, 513 $usetagnames, 514 $expectedcount 515 ) { 516 $this->resetAfterTest(); 517 518 $categories = []; 519 $questions = []; 520 $tagnames = [ 521 'cat1', 522 'cat2', 523 'subcat', 524 'foo' 525 ]; 526 $collid = \core_tag_collection::get_default(); 527 $tags = \core_tag_tag::create_if_missing($collid, $tagnames); 528 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 529 530 // First category and questions. 531 list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat1', 'foo']); 532 $categories['cat1'] = $category; 533 $questions['cat1q1'] = $categoryquestions[0]; 534 $questions['cat1q2'] = $categoryquestions[1]; 535 // Second category and questions. 536 list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat2', 'foo']); 537 $categories['cat2'] = $category; 538 $questions['cat2q1'] = $categoryquestions[0]; 539 $questions['cat2q2'] = $categoryquestions[1]; 540 // Sub category and questions. 541 list($category, $categoryquestions) = $this->create_category_and_questions(2, ['subcat', 'foo'], $categories['cat1']); 542 $categories['subcat'] = $category; 543 $questions['subcatq1'] = $categoryquestions[0]; 544 $questions['subcatq2'] = $categoryquestions[1]; 545 // Empty category. 546 list($category, $categoryquestions) = $this->create_category_and_questions(0); 547 $categories['emptycat'] = $category; 548 549 // Generate the arguments for the get_questions function. 550 $category = $categories[$categoryindex]; 551 $tagids = array_map(function($tagname) use ($tags) { 552 return $tags[$tagname]->id; 553 }, $usetagnames); 554 555 $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); 556 $result = $loader->count_questions($category->id, $includesubcategories, $tagids); 557 558 // Ensure the result matches what was expected. 559 $this->assertEquals($expectedcount, $result); 560 } 561 562 /** 563 * Create a question category and create questions in that category. Tag 564 * the first question in each category with the given tags. 565 * 566 * @param int $questioncount How many questions to create. 567 * @param string[] $tagnames The list of tags to use. 568 * @param stdClass|null $parentcategory The category to set as the parent of the created category. 569 * @return array The category and questions. 570 */ 571 protected function create_category_and_questions($questioncount, $tagnames = [], $parentcategory = null) { 572 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 573 574 if ($parentcategory) { 575 $catparams = ['parent' => $parentcategory->id]; 576 } else { 577 $catparams = []; 578 } 579 580 $category = $generator->create_question_category($catparams); 581 $questions = []; 582 583 for ($i = 0; $i < $questioncount; $i++) { 584 $questions[] = $generator->create_question('shortanswer', null, ['category' => $category->id]); 585 } 586 587 if (!empty($tagnames) && !empty($questions)) { 588 $context = \context::instance_by_id($category->contextid); 589 \core_tag_tag::set_item_tags('core_question', 'question', $questions[0]->id, $context, $tagnames); 590 } 591 592 return [$category, $questions]; 593 } 594 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body