Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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