Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [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 mod_quiz;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  global $CFG;
  22  require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
  23  
  24  /**
  25   * Unit tests for quiz events.
  26   *
  27   * @package   mod_quiz
  28   * @category  test
  29   * @copyright 2013 Adrian Greeve
  30   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31   */
  32  class structure_test extends \advanced_testcase {
  33  
  34      use \quiz_question_helper_test_trait;
  35  
  36      /**
  37       * Create a course with an empty quiz.
  38       * @return array with three elements quiz, cm and course.
  39       */
  40      protected function prepare_quiz_data() {
  41  
  42          $this->resetAfterTest(true);
  43  
  44          // Create a course.
  45          $course = $this->getDataGenerator()->create_course();
  46  
  47          // Make a quiz.
  48          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
  49  
  50          $quiz = $quizgenerator->create_instance(['course' => $course->id, 'questionsperpage' => 0,
  51              'grade' => 100.0, 'sumgrades' => 2, 'preferredbehaviour' => 'immediatefeedback']);
  52  
  53          $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id);
  54  
  55          return [$quiz, $cm, $course];
  56      }
  57  
  58      /**
  59       * Creat a test quiz.
  60       *
  61       * $layout looks like this:
  62       * $layout = array(
  63       *     'Heading 1'
  64       *     array('TF1', 1, 'truefalse'),
  65       *     'Heading 2*'
  66       *     array('TF2', 2, 'truefalse'),
  67       * );
  68       * That is, either a string, which represents a section heading,
  69       * or an array that represents a question.
  70       *
  71       * If the section heading ends with *, that section is shuffled.
  72       *
  73       * The elements in the question array are name, page number, and question type.
  74       *
  75       * @param array $layout as above.
  76       * @return quiz_settings the created quiz.
  77       */
  78      protected function create_test_quiz($layout) {
  79          list($quiz, $cm, $course) = $this->prepare_quiz_data();
  80          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  81          $cat = $questiongenerator->create_question_category();
  82  
  83          $headings = [];
  84          $slot = 1;
  85          $lastpage = 0;
  86          foreach ($layout as $item) {
  87              if (is_string($item)) {
  88                  if (isset($headings[$lastpage + 1])) {
  89                      throw new \coding_exception('Sections cannot be empty.');
  90                  }
  91                  $headings[$lastpage + 1] = $item;
  92  
  93              } else {
  94                  list($name, $page, $qtype) = $item;
  95                  if ($page < 1 || !($page == $lastpage + 1 ||
  96                          (!isset($headings[$lastpage + 1]) && $page == $lastpage))) {
  97                      throw new \coding_exception('Page numbers wrong.');
  98                  }
  99                  $q = $questiongenerator->create_question($qtype, null,
 100                          ['name' => $name, 'category' => $cat->id]);
 101  
 102                  quiz_add_quiz_question($q->id, $quiz, $page);
 103                  $lastpage = $page;
 104              }
 105          }
 106  
 107          $quizobj = new quiz_settings($quiz, $cm, $course);
 108          $structure = structure::create_for_quiz($quizobj);
 109          if (isset($headings[1])) {
 110              list($heading, $shuffle) = $this->parse_section_name($headings[1]);
 111              $sections = $structure->get_sections();
 112              $firstsection = reset($sections);
 113              $structure->set_section_heading($firstsection->id, $heading);
 114              $structure->set_section_shuffle($firstsection->id, $shuffle);
 115              unset($headings[1]);
 116          }
 117  
 118          foreach ($headings as $startpage => $heading) {
 119              list($heading, $shuffle) = $this->parse_section_name($heading);
 120              $id = $structure->add_section_heading($startpage, $heading);
 121              $structure->set_section_shuffle($id, $shuffle);
 122          }
 123  
 124          return $quizobj;
 125      }
 126  
 127      /**
 128       * Verify that the given layout matches that expected.
 129       * @param array $expectedlayout as for $layout in {@link create_test_quiz()}.
 130       * @param structure $structure the structure to test.
 131       */
 132      protected function assert_quiz_layout($expectedlayout, structure $structure) {
 133          $sections = $structure->get_sections();
 134  
 135          $slot = 1;
 136          foreach ($expectedlayout as $item) {
 137              if (is_string($item)) {
 138                  list($heading, $shuffle) = $this->parse_section_name($item);
 139                  $section = array_shift($sections);
 140  
 141                  if ($slot > 1 && $section->heading == '' && $section->firstslot == 1) {
 142                      // The array $expectedlayout did not contain default first quiz section, so skip over it.
 143                      $section = array_shift($sections);
 144                  }
 145  
 146                  $this->assertEquals($slot, $section->firstslot);
 147                  $this->assertEquals($heading, $section->heading);
 148                  $this->assertEquals($shuffle, $section->shufflequestions);
 149  
 150              } else {
 151                  list($name, $page, $qtype) = $item;
 152                  $question = $structure->get_question_in_slot($slot);
 153                  $this->assertEquals($name,  $question->name);
 154                  $this->assertEquals($slot,  $question->slot,  'Slot number wrong for question ' . $name);
 155                  $this->assertEquals($qtype, $question->qtype, 'Question type wrong for question ' . $name);
 156                  $this->assertEquals($page,  $question->page,  'Page number wrong for question ' . $name);
 157  
 158                  $slot += 1;
 159              }
 160          }
 161  
 162          if ($slot - 1 != count($structure->get_slots())) {
 163              $this->fail('The quiz contains more slots than expected.');
 164          }
 165  
 166          if (!empty($sections)) {
 167              $section = array_shift($sections);
 168              if ($section->heading != '' || $section->firstslot != 1) {
 169                  $this->fail('Unexpected section (' . $section->heading .') found in the quiz.');
 170              }
 171          }
 172      }
 173  
 174      /**
 175       * Parse the section name, optionally followed by a * to mean shuffle, as
 176       * used by create_test_quiz as assert_quiz_layout.
 177       * @param string $heading the heading.
 178       * @return array with two elements, the heading and the shuffle setting.
 179       */
 180      protected function parse_section_name($heading) {
 181          if (substr($heading, -1) == '*') {
 182              return [substr($heading, 0, -1), 1];
 183          } else {
 184              return [$heading, 0];
 185          }
 186      }
 187  
 188      public function test_get_quiz_slots() {
 189          $quizobj = $this->create_test_quiz([
 190                  ['TF1', 1, 'truefalse'],
 191                  ['TF2', 1, 'truefalse'],
 192          ]);
 193          $structure = structure::create_for_quiz($quizobj);
 194  
 195          // Are the correct slots returned?
 196          $slots = $structure->get_slots();
 197          $this->assertCount(2, $structure->get_slots());
 198      }
 199  
 200      public function test_quiz_has_one_section_by_default() {
 201          $quizobj = $this->create_test_quiz([
 202                  ['TF1', 1, 'truefalse'],
 203          ]);
 204          $structure = structure::create_for_quiz($quizobj);
 205  
 206          $sections = $structure->get_sections();
 207          $this->assertCount(1, $sections);
 208  
 209          $section = array_shift($sections);
 210          $this->assertEquals(1, $section->firstslot);
 211          $this->assertEquals('', $section->heading);
 212          $this->assertEquals(0, $section->shufflequestions);
 213      }
 214  
 215      public function test_get_sections() {
 216          $quizobj = $this->create_test_quiz([
 217                  'Heading 1*',
 218                  ['TF1', 1, 'truefalse'],
 219                  'Heading 2*',
 220                  ['TF2', 2, 'truefalse'],
 221          ]);
 222          $structure = structure::create_for_quiz($quizobj);
 223  
 224          $sections = $structure->get_sections();
 225          $this->assertCount(2, $sections);
 226  
 227          $section = array_shift($sections);
 228          $this->assertEquals(1, $section->firstslot);
 229          $this->assertEquals('Heading 1', $section->heading);
 230          $this->assertEquals(1, $section->shufflequestions);
 231  
 232          $section = array_shift($sections);
 233          $this->assertEquals(2, $section->firstslot);
 234          $this->assertEquals('Heading 2', $section->heading);
 235          $this->assertEquals(1, $section->shufflequestions);
 236      }
 237  
 238      public function test_remove_section_heading() {
 239          $quizobj = $this->create_test_quiz([
 240                  'Heading 1',
 241                  ['TF1', 1, 'truefalse'],
 242                  'Heading 2',
 243                  ['TF2', 2, 'truefalse'],
 244          ]);
 245          $structure = structure::create_for_quiz($quizobj);
 246  
 247          $sections = $structure->get_sections();
 248          $section = end($sections);
 249          $structure->remove_section_heading($section->id);
 250  
 251          $structure = structure::create_for_quiz($quizobj);
 252          $this->assert_quiz_layout([
 253                  'Heading 1',
 254                  ['TF1', 1, 'truefalse'],
 255                  ['TF2', 2, 'truefalse'],
 256          ], $structure);
 257      }
 258  
 259      public function test_cannot_remove_first_section() {
 260          $quizobj = $this->create_test_quiz([
 261                  'Heading 1',
 262                  ['TF1', 1, 'truefalse'],
 263          ]);
 264          $structure = structure::create_for_quiz($quizobj);
 265  
 266          $sections = $structure->get_sections();
 267          $section = reset($sections);
 268  
 269          $this->expectException(\coding_exception::class);
 270          $structure->remove_section_heading($section->id);
 271      }
 272  
 273      public function test_move_slot_to_the_same_place_does_nothing() {
 274          $quizobj = $this->create_test_quiz([
 275                  ['TF1', 1, 'truefalse'],
 276                  ['TF2', 1, 'truefalse'],
 277                  ['TF3', 2, 'truefalse'],
 278          ]);
 279          $structure = structure::create_for_quiz($quizobj);
 280  
 281          $idtomove = $structure->get_question_in_slot(2)->slotid;
 282          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 283          $structure->move_slot($idtomove, $idmoveafter, '1');
 284  
 285          $structure = structure::create_for_quiz($quizobj);
 286          $this->assert_quiz_layout([
 287                  ['TF1', 1, 'truefalse'],
 288                  ['TF2', 1, 'truefalse'],
 289                  ['TF3', 2, 'truefalse'],
 290          ], $structure);
 291      }
 292  
 293      public function test_move_slot_end_of_one_page_to_start_of_next() {
 294          $quizobj = $this->create_test_quiz([
 295                  ['TF1', 1, 'truefalse'],
 296                  ['TF2', 1, 'truefalse'],
 297                  ['TF3', 2, 'truefalse'],
 298          ]);
 299          $structure = structure::create_for_quiz($quizobj);
 300  
 301          $idtomove = $structure->get_question_in_slot(2)->slotid;
 302          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 303          $structure->move_slot($idtomove, $idmoveafter, '2');
 304  
 305          $structure = structure::create_for_quiz($quizobj);
 306          $this->assert_quiz_layout([
 307                  ['TF1', 1, 'truefalse'],
 308                  ['TF2', 2, 'truefalse'],
 309                  ['TF3', 2, 'truefalse'],
 310          ], $structure);
 311      }
 312  
 313      public function test_move_last_slot_to_previous_page_emptying_the_last_page() {
 314          $quizobj = $this->create_test_quiz([
 315                  ['TF1', 1, 'truefalse'],
 316                  ['TF2', 2, 'truefalse'],
 317          ]);
 318          $structure = structure::create_for_quiz($quizobj);
 319  
 320          $idtomove = $structure->get_question_in_slot(2)->slotid;
 321          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 322          $structure->move_slot($idtomove, $idmoveafter, '1');
 323  
 324          $structure = structure::create_for_quiz($quizobj);
 325          $this->assert_quiz_layout([
 326                  ['TF1', 1, 'truefalse'],
 327                  ['TF2', 1, 'truefalse'],
 328          ], $structure);
 329      }
 330  
 331      public function test_end_of_one_section_to_start_of_next() {
 332          $quizobj = $this->create_test_quiz([
 333                  ['TF1', 1, 'truefalse'],
 334                  ['TF2', 1, 'truefalse'],
 335                  'Heading',
 336                  ['TF3', 2, 'truefalse'],
 337          ]);
 338          $structure = structure::create_for_quiz($quizobj);
 339  
 340          $idtomove = $structure->get_question_in_slot(2)->slotid;
 341          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 342          $structure->move_slot($idtomove, $idmoveafter, '2');
 343  
 344          $structure = structure::create_for_quiz($quizobj);
 345          $this->assert_quiz_layout([
 346                  ['TF1', 1, 'truefalse'],
 347                  'Heading',
 348                  ['TF2', 2, 'truefalse'],
 349                  ['TF3', 2, 'truefalse'],
 350          ], $structure);
 351      }
 352  
 353      public function test_start_of_one_section_to_end_of_previous() {
 354          $quizobj = $this->create_test_quiz([
 355                  ['TF1', 1, 'truefalse'],
 356                  'Heading',
 357                  ['TF2', 2, 'truefalse'],
 358                  ['TF3', 2, 'truefalse'],
 359          ]);
 360          $structure = structure::create_for_quiz($quizobj);
 361  
 362          $idtomove = $structure->get_question_in_slot(2)->slotid;
 363          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 364          $structure->move_slot($idtomove, $idmoveafter, '1');
 365  
 366          $structure = structure::create_for_quiz($quizobj);
 367          $this->assert_quiz_layout([
 368                  ['TF1', 1, 'truefalse'],
 369                  ['TF2', 1, 'truefalse'],
 370                  'Heading',
 371                  ['TF3', 2, 'truefalse'],
 372          ], $structure);
 373      }
 374      public function test_move_slot_on_same_page() {
 375          $quizobj = $this->create_test_quiz([
 376                  ['TF1', 1, 'truefalse'],
 377                  ['TF2', 1, 'truefalse'],
 378                  ['TF3', 1, 'truefalse'],
 379          ]);
 380          $structure = structure::create_for_quiz($quizobj);
 381  
 382          $idtomove = $structure->get_question_in_slot(2)->slotid;
 383          $idmoveafter = $structure->get_question_in_slot(3)->slotid;
 384          $structure->move_slot($idtomove, $idmoveafter, '1');
 385  
 386          $structure = structure::create_for_quiz($quizobj);
 387          $this->assert_quiz_layout([
 388                  ['TF1', 1, 'truefalse'],
 389                  ['TF3', 1, 'truefalse'],
 390                  ['TF2', 1, 'truefalse'],
 391          ], $structure);
 392      }
 393  
 394      public function test_move_slot_up_onto_previous_page() {
 395          $quizobj = $this->create_test_quiz([
 396                  ['TF1', 1, 'truefalse'],
 397                  ['TF2', 2, 'truefalse'],
 398                  ['TF3', 2, 'truefalse'],
 399          ]);
 400          $structure = structure::create_for_quiz($quizobj);
 401  
 402          $idtomove = $structure->get_question_in_slot(3)->slotid;
 403          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 404          $structure->move_slot($idtomove, $idmoveafter, '1');
 405  
 406          $structure = structure::create_for_quiz($quizobj);
 407          $this->assert_quiz_layout([
 408                  ['TF1', 1, 'truefalse'],
 409                  ['TF3', 1, 'truefalse'],
 410                  ['TF2', 2, 'truefalse'],
 411          ], $structure);
 412      }
 413  
 414      public function test_move_slot_emptying_a_page_renumbers_pages() {
 415          $quizobj = $this->create_test_quiz([
 416                  ['TF1', 1, 'truefalse'],
 417                  ['TF2', 2, 'truefalse'],
 418                  ['TF3', 3, 'truefalse'],
 419          ]);
 420          $structure = structure::create_for_quiz($quizobj);
 421  
 422          $idtomove = $structure->get_question_in_slot(2)->slotid;
 423          $idmoveafter = $structure->get_question_in_slot(3)->slotid;
 424          $structure->move_slot($idtomove, $idmoveafter, '3');
 425  
 426          $structure = structure::create_for_quiz($quizobj);
 427          $this->assert_quiz_layout([
 428                  ['TF1', 1, 'truefalse'],
 429                  ['TF3', 2, 'truefalse'],
 430                  ['TF2', 2, 'truefalse'],
 431          ], $structure);
 432      }
 433  
 434      public function test_move_slot_too_small_page_number_detected() {
 435          $quizobj = $this->create_test_quiz([
 436                  ['TF1', 1, 'truefalse'],
 437                  ['TF2', 2, 'truefalse'],
 438                  ['TF3', 3, 'truefalse'],
 439          ]);
 440          $structure = structure::create_for_quiz($quizobj);
 441  
 442          $idtomove = $structure->get_question_in_slot(3)->slotid;
 443          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 444          $this->expectException(\coding_exception::class);
 445          $structure->move_slot($idtomove, $idmoveafter, '1');
 446      }
 447  
 448      public function test_move_slot_too_large_page_number_detected() {
 449          $quizobj = $this->create_test_quiz([
 450                  ['TF1', 1, 'truefalse'],
 451                  ['TF2', 2, 'truefalse'],
 452                  ['TF3', 3, 'truefalse'],
 453          ]);
 454          $structure = structure::create_for_quiz($quizobj);
 455  
 456          $idtomove = $structure->get_question_in_slot(1)->slotid;
 457          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 458          $this->expectException(\coding_exception::class);
 459          $structure->move_slot($idtomove, $idmoveafter, '4');
 460      }
 461  
 462      public function test_move_slot_within_section() {
 463          $quizobj = $this->create_test_quiz([
 464                  'Heading 1',
 465                  ['TF1', 1, 'truefalse'],
 466                  ['TF2', 1, 'truefalse'],
 467                  'Heading 2',
 468                  ['TF3', 2, 'truefalse'],
 469          ]);
 470          $structure = structure::create_for_quiz($quizobj);
 471  
 472          $idtomove = $structure->get_question_in_slot(1)->slotid;
 473          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 474          $structure->move_slot($idtomove, $idmoveafter, '1');
 475  
 476          $structure = structure::create_for_quiz($quizobj);
 477          $this->assert_quiz_layout([
 478                  'Heading 1',
 479                  ['TF2', 1, 'truefalse'],
 480                  ['TF1', 1, 'truefalse'],
 481                  'Heading 2',
 482                  ['TF3', 2, 'truefalse'],
 483          ], $structure);
 484      }
 485  
 486      public function test_move_slot_to_new_section() {
 487          $quizobj = $this->create_test_quiz([
 488                  'Heading 1',
 489                  ['TF1', 1, 'truefalse'],
 490                  ['TF2', 1, 'truefalse'],
 491                  'Heading 2',
 492                  ['TF3', 2, 'truefalse'],
 493          ]);
 494          $structure = structure::create_for_quiz($quizobj);
 495  
 496          $idtomove = $structure->get_question_in_slot(2)->slotid;
 497          $idmoveafter = $structure->get_question_in_slot(3)->slotid;
 498          $structure->move_slot($idtomove, $idmoveafter, '2');
 499  
 500          $structure = structure::create_for_quiz($quizobj);
 501          $this->assert_quiz_layout([
 502                  'Heading 1',
 503                  ['TF1', 1, 'truefalse'],
 504                  'Heading 2',
 505                  ['TF3', 2, 'truefalse'],
 506                  ['TF2', 2, 'truefalse'],
 507          ], $structure);
 508      }
 509  
 510      public function test_move_slot_to_start() {
 511          $quizobj = $this->create_test_quiz([
 512                  'Heading 1',
 513                  ['TF1', 1, 'truefalse'],
 514                  'Heading 2',
 515                  ['TF2', 2, 'truefalse'],
 516                  ['TF3', 2, 'truefalse'],
 517          ]);
 518          $structure = structure::create_for_quiz($quizobj);
 519  
 520          $idtomove = $structure->get_question_in_slot(3)->slotid;
 521          $structure->move_slot($idtomove, 0, '1');
 522  
 523          $structure = structure::create_for_quiz($quizobj);
 524          $this->assert_quiz_layout([
 525                  'Heading 1',
 526                  ['TF3', 1, 'truefalse'],
 527                  ['TF1', 1, 'truefalse'],
 528                  'Heading 2',
 529                  ['TF2', 2, 'truefalse'],
 530          ], $structure);
 531      }
 532  
 533      public function test_move_slot_down_to_start_of_second_section() {
 534          $quizobj = $this->create_test_quiz([
 535                  'Heading 1',
 536                  ['TF1', 1, 'truefalse'],
 537                  ['TF2', 1, 'truefalse'],
 538                  'Heading 2',
 539                  ['TF3', 2, 'truefalse'],
 540          ]);
 541          $structure = structure::create_for_quiz($quizobj);
 542  
 543          $idtomove = $structure->get_question_in_slot(2)->slotid;
 544          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 545          $structure->move_slot($idtomove, $idmoveafter, '2');
 546  
 547          $structure = structure::create_for_quiz($quizobj);
 548          $this->assert_quiz_layout([
 549                  'Heading 1',
 550                  ['TF1', 1, 'truefalse'],
 551                  'Heading 2',
 552                  ['TF2', 2, 'truefalse'],
 553                  ['TF3', 2, 'truefalse'],
 554          ], $structure);
 555      }
 556  
 557      public function test_move_first_slot_down_to_start_of_page_2() {
 558          $quizobj = $this->create_test_quiz([
 559                  'Heading 1',
 560                  ['TF1', 1, 'truefalse'],
 561                  ['TF2', 2, 'truefalse'],
 562          ]);
 563          $structure = structure::create_for_quiz($quizobj);
 564  
 565          $idtomove = $structure->get_question_in_slot(1)->slotid;
 566          $structure->move_slot($idtomove, 0, '2');
 567  
 568          $structure = structure::create_for_quiz($quizobj);
 569          $this->assert_quiz_layout([
 570                  'Heading 1',
 571                  ['TF1', 1, 'truefalse'],
 572                  ['TF2', 1, 'truefalse'],
 573          ], $structure);
 574      }
 575  
 576      public function test_move_first_slot_to_same_place_on_page_1() {
 577          $quizobj = $this->create_test_quiz([
 578                  'Heading 1',
 579                  ['TF1', 1, 'truefalse'],
 580                  ['TF2', 2, 'truefalse'],
 581          ]);
 582          $structure = structure::create_for_quiz($quizobj);
 583  
 584          $idtomove = $structure->get_question_in_slot(1)->slotid;
 585          $structure->move_slot($idtomove, 0, '1');
 586  
 587          $structure = structure::create_for_quiz($quizobj);
 588          $this->assert_quiz_layout([
 589                  'Heading 1',
 590                  ['TF1', 1, 'truefalse'],
 591                  ['TF2', 2, 'truefalse'],
 592          ], $structure);
 593      }
 594  
 595      public function test_move_first_slot_to_before_page_1() {
 596          $quizobj = $this->create_test_quiz([
 597                  'Heading 1',
 598                  ['TF1', 1, 'truefalse'],
 599                  ['TF2', 2, 'truefalse'],
 600          ]);
 601          $structure = structure::create_for_quiz($quizobj);
 602  
 603          $idtomove = $structure->get_question_in_slot(1)->slotid;
 604          $structure->move_slot($idtomove, 0, '');
 605  
 606          $structure = structure::create_for_quiz($quizobj);
 607          $this->assert_quiz_layout([
 608                  'Heading 1',
 609                  ['TF1', 1, 'truefalse'],
 610                  ['TF2', 2, 'truefalse'],
 611          ], $structure);
 612      }
 613  
 614      public function test_move_slot_up_to_start_of_second_section() {
 615          $quizobj = $this->create_test_quiz([
 616                  'Heading 1',
 617                  ['TF1', 1, 'truefalse'],
 618                  'Heading 2',
 619                  ['TF2', 2, 'truefalse'],
 620                  'Heading 3',
 621                  ['TF3', 3, 'truefalse'],
 622                  ['TF4', 3, 'truefalse'],
 623          ]);
 624          $structure = structure::create_for_quiz($quizobj);
 625  
 626          $idtomove = $structure->get_question_in_slot(3)->slotid;
 627          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 628          $structure->move_slot($idtomove, $idmoveafter, '2');
 629  
 630          $structure = structure::create_for_quiz($quizobj);
 631          $this->assert_quiz_layout([
 632                  'Heading 1',
 633                  ['TF1', 1, 'truefalse'],
 634                  'Heading 2',
 635                  ['TF3', 2, 'truefalse'],
 636                  ['TF2', 2, 'truefalse'],
 637                  'Heading 3',
 638                  ['TF4', 3, 'truefalse'],
 639          ], $structure);
 640      }
 641  
 642      public function test_move_slot_does_not_violate_heading_unique_key() {
 643          $quizobj = $this->create_test_quiz([
 644                  'Heading 1',
 645                  ['TF1', 1, 'truefalse'],
 646                  'Heading 2',
 647                  ['TF2', 2, 'truefalse'],
 648                  'Heading 3',
 649                  ['TF3', 3, 'truefalse'],
 650                  ['TF4', 3, 'truefalse'],
 651          ]);
 652          $structure = structure::create_for_quiz($quizobj);
 653  
 654          $idtomove = $structure->get_question_in_slot(4)->slotid;
 655          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 656          $structure->move_slot($idtomove, $idmoveafter, 1);
 657  
 658          $structure = structure::create_for_quiz($quizobj);
 659          $this->assert_quiz_layout([
 660                  'Heading 1',
 661                  ['TF1', 1, 'truefalse'],
 662                  ['TF4', 1, 'truefalse'],
 663                  'Heading 2',
 664                  ['TF2', 2, 'truefalse'],
 665                  'Heading 3',
 666                  ['TF3', 3, 'truefalse'],
 667          ], $structure);
 668      }
 669  
 670      public function test_quiz_remove_slot() {
 671          $quizobj = $this->create_test_quiz([
 672                  ['TF1', 1, 'truefalse'],
 673                  ['TF2', 1, 'truefalse'],
 674                  'Heading 2',
 675                  ['TF3', 2, 'truefalse'],
 676          ]);
 677          $structure = structure::create_for_quiz($quizobj);
 678  
 679          $structure->remove_slot(2);
 680  
 681          $structure = structure::create_for_quiz($quizobj);
 682          $this->assert_quiz_layout([
 683                  ['TF1', 1, 'truefalse'],
 684                  'Heading 2',
 685                  ['TF3', 2, 'truefalse'],
 686          ], $structure);
 687      }
 688  
 689      public function test_quiz_removing_a_random_question_deletes_the_question() {
 690          global $DB;
 691  
 692          $this->resetAfterTest(true);
 693          $this->setAdminUser();
 694  
 695          $quizobj = $this->create_test_quiz([
 696                  ['TF1', 1, 'truefalse'],
 697          ]);
 698  
 699          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 700          $cat = $questiongenerator->create_question_category();
 701          $this->add_random_questions($quizobj->get_quizid(), 1, $cat->id, 1);
 702          $structure = structure::create_for_quiz($quizobj);
 703          $sql = 'SELECT qsr.*
 704                   FROM {question_set_references} qsr
 705                   JOIN {quiz_slots} qs ON qs.id = qsr.itemid
 706                   WHERE qs.quizid = ?
 707                     AND qsr.component = ?
 708                     AND qsr.questionarea = ?';
 709          $randomq = $DB->get_record_sql($sql, [$quizobj->get_quizid(), 'mod_quiz', 'slot']);
 710  
 711          $structure->remove_slot(2);
 712  
 713          $structure = structure::create_for_quiz($quizobj);
 714          $this->assert_quiz_layout([
 715                  ['TF1', 1, 'truefalse'],
 716          ], $structure);
 717          $this->assertFalse($DB->record_exists('question_set_references',
 718              ['id' => $randomq->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']));
 719      }
 720  
 721      /**
 722       * Unit test to make sue it is not possible to remove all slots in a section at once.
 723       */
 724      public function test_cannot_remove_all_slots_in_a_section() {
 725          $quizobj = $this->create_test_quiz([
 726              ['TF1', 1, 'truefalse'],
 727              ['TF2', 1, 'truefalse'],
 728              'Heading 2',
 729              ['TF3', 2, 'truefalse'],
 730          ]);
 731          $structure = structure::create_for_quiz($quizobj);
 732  
 733          $structure->remove_slot(1);
 734          $this->expectException(\coding_exception::class);
 735          $structure->remove_slot(2);
 736      }
 737  
 738      public function test_cannot_remove_last_slot_in_a_section() {
 739          $quizobj = $this->create_test_quiz([
 740                  ['TF1', 1, 'truefalse'],
 741                  ['TF2', 1, 'truefalse'],
 742                  'Heading 2',
 743                  ['TF3', 2, 'truefalse'],
 744          ]);
 745          $structure = structure::create_for_quiz($quizobj);
 746  
 747          $this->expectException(\coding_exception::class);
 748          $structure->remove_slot(3);
 749      }
 750  
 751      public function test_can_remove_last_question_in_a_quiz() {
 752          $quizobj = $this->create_test_quiz([
 753                  'Heading 1',
 754                  ['TF1', 1, 'truefalse'],
 755          ]);
 756          $structure = structure::create_for_quiz($quizobj);
 757  
 758          $structure->remove_slot(1);
 759  
 760          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 761          $cat = $questiongenerator->create_question_category();
 762          $q = $questiongenerator->create_question('truefalse', null,
 763                  ['name' => 'TF2', 'category' => $cat->id]);
 764  
 765          quiz_add_quiz_question($q->id, $quizobj->get_quiz(), 0);
 766          $structure = structure::create_for_quiz($quizobj);
 767  
 768          $this->assert_quiz_layout([
 769                  'Heading 1',
 770                  ['TF2', 1, 'truefalse'],
 771          ], $structure);
 772      }
 773  
 774      public function test_add_question_updates_headings() {
 775          $quizobj = $this->create_test_quiz([
 776                  ['TF1', 1, 'truefalse'],
 777                  'Heading 2',
 778                  ['TF2', 2, 'truefalse'],
 779          ]);
 780  
 781          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 782          $cat = $questiongenerator->create_question_category();
 783          $q = $questiongenerator->create_question('truefalse', null,
 784                  ['name' => 'TF3', 'category' => $cat->id]);
 785  
 786          quiz_add_quiz_question($q->id, $quizobj->get_quiz(), 1);
 787  
 788          $structure = structure::create_for_quiz($quizobj);
 789          $this->assert_quiz_layout([
 790                  ['TF1', 1, 'truefalse'],
 791                  ['TF3', 1, 'truefalse'],
 792                  'Heading 2',
 793                  ['TF2', 2, 'truefalse'],
 794          ], $structure);
 795      }
 796  
 797      public function test_add_question_updates_headings_even_with_one_question_sections() {
 798          $quizobj = $this->create_test_quiz([
 799                  'Heading 1',
 800                  ['TF1', 1, 'truefalse'],
 801                  'Heading 2',
 802                  ['TF2', 2, 'truefalse'],
 803                  'Heading 3',
 804                  ['TF3', 3, 'truefalse'],
 805          ]);
 806  
 807          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 808          $cat = $questiongenerator->create_question_category();
 809          $q = $questiongenerator->create_question('truefalse', null,
 810                  ['name' => 'TF4', 'category' => $cat->id]);
 811  
 812          quiz_add_quiz_question($q->id, $quizobj->get_quiz(), 1);
 813  
 814          $structure = structure::create_for_quiz($quizobj);
 815          $this->assert_quiz_layout([
 816                  'Heading 1',
 817                  ['TF1', 1, 'truefalse'],
 818                  ['TF4', 1, 'truefalse'],
 819                  'Heading 2',
 820                  ['TF2', 2, 'truefalse'],
 821                  'Heading 3',
 822                  ['TF3', 3, 'truefalse'],
 823          ], $structure);
 824      }
 825  
 826      public function test_add_question_at_end_does_not_update_headings() {
 827          $quizobj = $this->create_test_quiz([
 828                  ['TF1', 1, 'truefalse'],
 829                  'Heading 2',
 830                  ['TF2', 2, 'truefalse'],
 831          ]);
 832  
 833          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 834          $cat = $questiongenerator->create_question_category();
 835          $q = $questiongenerator->create_question('truefalse', null,
 836                  ['name' => 'TF3', 'category' => $cat->id]);
 837  
 838          quiz_add_quiz_question($q->id, $quizobj->get_quiz(), 0);
 839  
 840          $structure = structure::create_for_quiz($quizobj);
 841          $this->assert_quiz_layout([
 842                  ['TF1', 1, 'truefalse'],
 843                  'Heading 2',
 844                  ['TF2', 2, 'truefalse'],
 845                  ['TF3', 2, 'truefalse'],
 846          ], $structure);
 847      }
 848  
 849      public function test_remove_page_break() {
 850          $quizobj = $this->create_test_quiz([
 851                  ['TF1', 1, 'truefalse'],
 852                  ['TF2', 2, 'truefalse'],
 853          ]);
 854          $structure = structure::create_for_quiz($quizobj);
 855  
 856          $slotid = $structure->get_question_in_slot(2)->slotid;
 857          $slots = $structure->update_page_break($slotid, repaginate::LINK);
 858  
 859          $structure = structure::create_for_quiz($quizobj);
 860          $this->assert_quiz_layout([
 861                  ['TF1', 1, 'truefalse'],
 862                  ['TF2', 1, 'truefalse'],
 863          ], $structure);
 864      }
 865  
 866      public function test_add_page_break() {
 867          $quizobj = $this->create_test_quiz([
 868                  ['TF1', 1, 'truefalse'],
 869                  ['TF2', 1, 'truefalse'],
 870          ]);
 871          $structure = structure::create_for_quiz($quizobj);
 872  
 873          $slotid = $structure->get_question_in_slot(2)->slotid;
 874          $slots = $structure->update_page_break($slotid, repaginate::UNLINK);
 875  
 876          $structure = structure::create_for_quiz($quizobj);
 877          $this->assert_quiz_layout([
 878                  ['TF1', 1, 'truefalse'],
 879                  ['TF2', 2, 'truefalse'],
 880          ], $structure);
 881      }
 882  
 883      public function test_update_question_dependency() {
 884          $quizobj = $this->create_test_quiz([
 885                  ['TF1', 1, 'truefalse'],
 886                  ['TF2', 1, 'truefalse'],
 887          ]);
 888          $structure = structure::create_for_quiz($quizobj);
 889  
 890          // Test adding a dependency.
 891          $slotid = $structure->get_slot_id_for_slot(2);
 892          $structure->update_question_dependency($slotid, true);
 893  
 894          // Having called update page break, we need to reload $structure.
 895          $structure = structure::create_for_quiz($quizobj);
 896          $this->assertEquals(1, $structure->is_question_dependent_on_previous_slot(2));
 897  
 898          // Test removing a dependency.
 899          $structure->update_question_dependency($slotid, false);
 900  
 901          // Having called update page break, we need to reload $structure.
 902          $structure = structure::create_for_quiz($quizobj);
 903          $this->assertEquals(0, $structure->is_question_dependent_on_previous_slot(2));
 904      }
 905  
 906      /**
 907       * Test for can_add_random_questions.
 908       */
 909      public function test_can_add_random_questions() {
 910          $this->resetAfterTest();
 911  
 912          $quiz = $this->create_test_quiz([]);
 913          $course = $quiz->get_course();
 914  
 915          $generator = $this->getDataGenerator();
 916          $teacher = $generator->create_and_enrol($course, 'editingteacher');
 917          $noneditingteacher = $generator->create_and_enrol($course, 'teacher');
 918  
 919          $this->setUser($teacher);
 920          $structure = structure::create_for_quiz($quiz);
 921          $this->assertTrue($structure->can_add_random_questions());
 922  
 923          $this->setUser($noneditingteacher);
 924          $structure = structure::create_for_quiz($quiz);
 925          $this->assertFalse($structure->can_add_random_questions());
 926      }
 927  
 928      /**
 929       * Test to get the version information for a question to show in the version selection dropdown.
 930       *
 931       * @covers ::get_question_version_info
 932       */
 933      public function test_get_version_choices_for_slot() {
 934          $this->resetAfterTest();
 935  
 936          $quizobj = $this->create_test_quiz([]);
 937  
 938          // Create a question with two versions.
 939          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 940          $cat = $questiongenerator->create_question_category(['contextid' => $quizobj->get_context()->id]);
 941          $q = $questiongenerator->create_question('essay', null,
 942                  ['category' => $cat->id, 'name' => 'This is the first version']);
 943          $questiongenerator->update_question($q, null, ['name' => 'This is the second version']);
 944          $questiongenerator->update_question($q, null, ['name' => 'This is the third version']);
 945          quiz_add_quiz_question($q->id, $quizobj->get_quiz());
 946  
 947          // Create the quiz object.
 948          $structure = structure::create_for_quiz($quizobj);
 949          $versiondata = $structure->get_version_choices_for_slot(1);
 950          $this->assertEquals(4, count($versiondata));
 951          $this->assertEquals('Always latest', $versiondata[0]->versionvalue);
 952          $this->assertEquals('v3 (latest)', $versiondata[1]->versionvalue);
 953          $this->assertEquals('v2', $versiondata[2]->versionvalue);
 954          $this->assertEquals('v1', $versiondata[3]->versionvalue);
 955          $this->assertTrue($versiondata[0]->selected);
 956          $this->assertFalse($versiondata[1]->selected);
 957          $this->assertFalse($versiondata[2]->selected);
 958          $this->assertFalse($versiondata[3]->selected);
 959      }
 960  
 961      /**
 962       * Test the current user have '...use' capability over the question(s) in a given slot.
 963       *
 964       * @covers ::has_use_capability
 965       */
 966      public function test_has_use_capability() {
 967          $this->resetAfterTest();
 968  
 969          // Create a quiz with question.
 970          $quizobj = $this->create_test_quiz([]);
 971          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 972          $cat = $questiongenerator->create_question_category(['contextid' => $quizobj->get_context()->id]);
 973          $q = $questiongenerator->create_question('essay', null,
 974              ['category' => $cat->id, 'name' => 'This is essay question']);
 975          quiz_add_quiz_question($q->id, $quizobj->get_quiz());
 976  
 977          // Create the quiz object.
 978          $structure = structure::create_for_quiz($quizobj);
 979          $slots = $structure->get_slots();
 980  
 981          // Get slot.
 982          $slotid = array_pop($slots)->slot;
 983  
 984          $course = $quizobj->get_course();
 985          $generator = $this->getDataGenerator();
 986          $teacher = $generator->create_and_enrol($course, 'editingteacher');
 987          $student = $generator->create_and_enrol($course);
 988  
 989          $this->setUser($teacher);
 990          $this->assertTrue($structure->has_use_capability($slotid));
 991  
 992          $this->setUser($student);
 993          $this->assertFalse($structure->has_use_capability($slotid));
 994      }
 995  }