Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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