Search moodle.org's
Developer Documentation

See Release Notes

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

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