Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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