Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  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      /**
 263       * @expectedException coding_exception
 264       */
 265      public function test_cannot_remove_first_section() {
 266          $quizobj = $this->create_test_quiz(array(
 267                  'Heading 1',
 268                  array('TF1', 1, 'truefalse'),
 269          ));
 270          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 271  
 272          $sections = $structure->get_sections();
 273          $section = reset($sections);
 274  
 275          $structure->remove_section_heading($section->id);
 276      }
 277  
 278      public function test_move_slot_to_the_same_place_does_nothing() {
 279          $quizobj = $this->create_test_quiz(array(
 280                  array('TF1', 1, 'truefalse'),
 281                  array('TF2', 1, 'truefalse'),
 282                  array('TF3', 2, 'truefalse'),
 283              ));
 284          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 285  
 286          $idtomove = $structure->get_question_in_slot(2)->slotid;
 287          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 288          $structure->move_slot($idtomove, $idmoveafter, '1');
 289  
 290          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 291          $this->assert_quiz_layout(array(
 292                  array('TF1', 1, 'truefalse'),
 293                  array('TF2', 1, 'truefalse'),
 294                  array('TF3', 2, 'truefalse'),
 295              ), $structure);
 296      }
 297  
 298      public function test_move_slot_end_of_one_page_to_start_of_next() {
 299          $quizobj = $this->create_test_quiz(array(
 300                  array('TF1', 1, 'truefalse'),
 301                  array('TF2', 1, 'truefalse'),
 302                  array('TF3', 2, 'truefalse'),
 303              ));
 304          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 305  
 306          $idtomove = $structure->get_question_in_slot(2)->slotid;
 307          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 308          $structure->move_slot($idtomove, $idmoveafter, '2');
 309  
 310          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 311          $this->assert_quiz_layout(array(
 312                  array('TF1', 1, 'truefalse'),
 313                  array('TF2', 2, 'truefalse'),
 314                  array('TF3', 2, 'truefalse'),
 315              ), $structure);
 316      }
 317  
 318      public function test_move_last_slot_to_previous_page_emptying_the_last_page() {
 319          $quizobj = $this->create_test_quiz(array(
 320                  array('TF1', 1, 'truefalse'),
 321                  array('TF2', 2, 'truefalse'),
 322              ));
 323          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 324  
 325          $idtomove = $structure->get_question_in_slot(2)->slotid;
 326          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 327          $structure->move_slot($idtomove, $idmoveafter, '1');
 328  
 329          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 330          $this->assert_quiz_layout(array(
 331                  array('TF1', 1, 'truefalse'),
 332                  array('TF2', 1, 'truefalse'),
 333              ), $structure);
 334      }
 335  
 336      public function test_end_of_one_section_to_start_of_next() {
 337          $quizobj = $this->create_test_quiz(array(
 338                  array('TF1', 1, 'truefalse'),
 339                  array('TF2', 1, 'truefalse'),
 340                  'Heading',
 341                  array('TF3', 2, 'truefalse'),
 342              ));
 343          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 344  
 345          $idtomove = $structure->get_question_in_slot(2)->slotid;
 346          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 347          $structure->move_slot($idtomove, $idmoveafter, '2');
 348  
 349          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 350          $this->assert_quiz_layout(array(
 351                  array('TF1', 1, 'truefalse'),
 352                  'Heading',
 353                  array('TF2', 2, 'truefalse'),
 354                  array('TF3', 2, 'truefalse'),
 355              ), $structure);
 356      }
 357  
 358      public function test_start_of_one_section_to_end_of_previous() {
 359          $quizobj = $this->create_test_quiz(array(
 360                  array('TF1', 1, 'truefalse'),
 361                  'Heading',
 362                  array('TF2', 2, 'truefalse'),
 363                  array('TF3', 2, 'truefalse'),
 364              ));
 365          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 366  
 367          $idtomove = $structure->get_question_in_slot(2)->slotid;
 368          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 369          $structure->move_slot($idtomove, $idmoveafter, '1');
 370  
 371          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 372          $this->assert_quiz_layout(array(
 373                  array('TF1', 1, 'truefalse'),
 374                  array('TF2', 1, 'truefalse'),
 375                  'Heading',
 376                  array('TF3', 2, 'truefalse'),
 377              ), $structure);
 378      }
 379      public function test_move_slot_on_same_page() {
 380          $quizobj = $this->create_test_quiz(array(
 381                  array('TF1', 1, 'truefalse'),
 382                  array('TF2', 1, 'truefalse'),
 383                  array('TF3', 1, 'truefalse'),
 384              ));
 385          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 386  
 387          $idtomove = $structure->get_question_in_slot(2)->slotid;
 388          $idmoveafter = $structure->get_question_in_slot(3)->slotid;
 389          $structure->move_slot($idtomove, $idmoveafter, '1');
 390  
 391          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 392          $this->assert_quiz_layout(array(
 393                  array('TF1', 1, 'truefalse'),
 394                  array('TF3', 1, 'truefalse'),
 395                  array('TF2', 1, 'truefalse'),
 396          ), $structure);
 397      }
 398  
 399      public function test_move_slot_up_onto_previous_page() {
 400          $quizobj = $this->create_test_quiz(array(
 401                  array('TF1', 1, 'truefalse'),
 402                  array('TF2', 2, 'truefalse'),
 403                  array('TF3', 2, 'truefalse'),
 404          ));
 405          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 406  
 407          $idtomove = $structure->get_question_in_slot(3)->slotid;
 408          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 409          $structure->move_slot($idtomove, $idmoveafter, '1');
 410  
 411          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 412          $this->assert_quiz_layout(array(
 413                  array('TF1', 1, 'truefalse'),
 414                  array('TF3', 1, 'truefalse'),
 415                  array('TF2', 2, 'truefalse'),
 416          ), $structure);
 417      }
 418  
 419      public function test_move_slot_emptying_a_page_renumbers_pages() {
 420          $quizobj = $this->create_test_quiz(array(
 421                  array('TF1', 1, 'truefalse'),
 422                  array('TF2', 2, 'truefalse'),
 423                  array('TF3', 3, 'truefalse'),
 424          ));
 425          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 426  
 427          $idtomove = $structure->get_question_in_slot(2)->slotid;
 428          $idmoveafter = $structure->get_question_in_slot(3)->slotid;
 429          $structure->move_slot($idtomove, $idmoveafter, '3');
 430  
 431          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 432          $this->assert_quiz_layout(array(
 433                  array('TF1', 1, 'truefalse'),
 434                  array('TF3', 2, 'truefalse'),
 435                  array('TF2', 2, 'truefalse'),
 436          ), $structure);
 437      }
 438  
 439      /**
 440       * @expectedException coding_exception
 441       */
 442      public function test_move_slot_too_small_page_number_detected() {
 443          $quizobj = $this->create_test_quiz(array(
 444                  array('TF1', 1, 'truefalse'),
 445                  array('TF2', 2, 'truefalse'),
 446                  array('TF3', 3, 'truefalse'),
 447          ));
 448          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 449  
 450          $idtomove = $structure->get_question_in_slot(3)->slotid;
 451          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 452          $structure->move_slot($idtomove, $idmoveafter, '1');
 453      }
 454  
 455      /**
 456       * @expectedException coding_exception
 457       */
 458      public function test_move_slot_too_large_page_number_detected() {
 459          $quizobj = $this->create_test_quiz(array(
 460                  array('TF1', 1, 'truefalse'),
 461                  array('TF2', 2, 'truefalse'),
 462                  array('TF3', 3, 'truefalse'),
 463          ));
 464          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 465  
 466          $idtomove = $structure->get_question_in_slot(1)->slotid;
 467          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 468          $structure->move_slot($idtomove, $idmoveafter, '4');
 469      }
 470  
 471      public function test_move_slot_within_section() {
 472          $quizobj = $this->create_test_quiz(array(
 473                  'Heading 1',
 474                  array('TF1', 1, 'truefalse'),
 475                  array('TF2', 1, 'truefalse'),
 476                  'Heading 2',
 477                  array('TF3', 2, 'truefalse'),
 478              ));
 479          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 480  
 481          $idtomove = $structure->get_question_in_slot(1)->slotid;
 482          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 483          $structure->move_slot($idtomove, $idmoveafter, '1');
 484  
 485          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 486          $this->assert_quiz_layout(array(
 487                  'Heading 1',
 488                  array('TF2', 1, 'truefalse'),
 489                  array('TF1', 1, 'truefalse'),
 490                  'Heading 2',
 491                  array('TF3', 2, 'truefalse'),
 492              ), $structure);
 493      }
 494  
 495      public function test_move_slot_to_new_section() {
 496          $quizobj = $this->create_test_quiz(array(
 497                  'Heading 1',
 498                  array('TF1', 1, 'truefalse'),
 499                  array('TF2', 1, 'truefalse'),
 500                  'Heading 2',
 501                  array('TF3', 2, 'truefalse'),
 502              ));
 503          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 504  
 505          $idtomove = $structure->get_question_in_slot(2)->slotid;
 506          $idmoveafter = $structure->get_question_in_slot(3)->slotid;
 507          $structure->move_slot($idtomove, $idmoveafter, '2');
 508  
 509          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 510          $this->assert_quiz_layout(array(
 511                  'Heading 1',
 512                  array('TF1', 1, 'truefalse'),
 513                  'Heading 2',
 514                  array('TF3', 2, 'truefalse'),
 515                  array('TF2', 2, 'truefalse'),
 516              ), $structure);
 517      }
 518  
 519      public function test_move_slot_to_start() {
 520          $quizobj = $this->create_test_quiz(array(
 521                  'Heading 1',
 522                  array('TF1', 1, 'truefalse'),
 523                  'Heading 2',
 524                  array('TF2', 2, 'truefalse'),
 525                  array('TF3', 2, 'truefalse'),
 526              ));
 527          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 528  
 529          $idtomove = $structure->get_question_in_slot(3)->slotid;
 530          $structure->move_slot($idtomove, 0, '1');
 531  
 532          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 533          $this->assert_quiz_layout(array(
 534                  'Heading 1',
 535                  array('TF3', 1, 'truefalse'),
 536                  array('TF1', 1, 'truefalse'),
 537                  'Heading 2',
 538                  array('TF2', 2, 'truefalse'),
 539              ), $structure);
 540      }
 541  
 542      public function test_move_slot_down_to_start_of_second_section() {
 543          $quizobj = $this->create_test_quiz(array(
 544                  'Heading 1',
 545                  array('TF1', 1, 'truefalse'),
 546                  array('TF2', 1, 'truefalse'),
 547                  'Heading 2',
 548                  array('TF3', 2, 'truefalse'),
 549              ));
 550          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 551  
 552          $idtomove = $structure->get_question_in_slot(2)->slotid;
 553          $idmoveafter = $structure->get_question_in_slot(2)->slotid;
 554          $structure->move_slot($idtomove, $idmoveafter, '2');
 555  
 556          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 557          $this->assert_quiz_layout(array(
 558                  'Heading 1',
 559                  array('TF1', 1, 'truefalse'),
 560                  'Heading 2',
 561                  array('TF2', 2, 'truefalse'),
 562                  array('TF3', 2, 'truefalse'),
 563              ), $structure);
 564      }
 565  
 566      public function test_move_first_slot_down_to_start_of_page_2() {
 567          $quizobj = $this->create_test_quiz(array(
 568                  'Heading 1',
 569                  array('TF1', 1, 'truefalse'),
 570                  array('TF2', 2, 'truefalse'),
 571              ));
 572          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 573  
 574          $idtomove = $structure->get_question_in_slot(1)->slotid;
 575          $structure->move_slot($idtomove, 0, '2');
 576  
 577          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 578          $this->assert_quiz_layout(array(
 579                  'Heading 1',
 580                  array('TF1', 1, 'truefalse'),
 581                  array('TF2', 1, 'truefalse'),
 582              ), $structure);
 583      }
 584  
 585      public function test_move_first_slot_to_same_place_on_page_1() {
 586          $quizobj = $this->create_test_quiz(array(
 587                  'Heading 1',
 588                  array('TF1', 1, 'truefalse'),
 589                  array('TF2', 2, 'truefalse'),
 590              ));
 591          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 592  
 593          $idtomove = $structure->get_question_in_slot(1)->slotid;
 594          $structure->move_slot($idtomove, 0, '1');
 595  
 596          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 597          $this->assert_quiz_layout(array(
 598                  'Heading 1',
 599                  array('TF1', 1, 'truefalse'),
 600                  array('TF2', 2, 'truefalse'),
 601              ), $structure);
 602      }
 603  
 604      public function test_move_first_slot_to_before_page_1() {
 605          $quizobj = $this->create_test_quiz(array(
 606                  'Heading 1',
 607                  array('TF1', 1, 'truefalse'),
 608                  array('TF2', 2, 'truefalse'),
 609              ));
 610          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 611  
 612          $idtomove = $structure->get_question_in_slot(1)->slotid;
 613          $structure->move_slot($idtomove, 0, '');
 614  
 615          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 616          $this->assert_quiz_layout(array(
 617                  'Heading 1',
 618                  array('TF1', 1, 'truefalse'),
 619                  array('TF2', 2, 'truefalse'),
 620              ), $structure);
 621      }
 622  
 623      public function test_move_slot_up_to_start_of_second_section() {
 624          $quizobj = $this->create_test_quiz(array(
 625                  'Heading 1',
 626                  array('TF1', 1, 'truefalse'),
 627                  'Heading 2',
 628                  array('TF2', 2, 'truefalse'),
 629                  'Heading 3',
 630                  array('TF3', 3, 'truefalse'),
 631                  array('TF4', 3, 'truefalse'),
 632              ));
 633          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 634  
 635          $idtomove = $structure->get_question_in_slot(3)->slotid;
 636          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 637          $structure->move_slot($idtomove, $idmoveafter, '2');
 638  
 639          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 640          $this->assert_quiz_layout(array(
 641                  'Heading 1',
 642                  array('TF1', 1, 'truefalse'),
 643                  'Heading 2',
 644                  array('TF3', 2, 'truefalse'),
 645                  array('TF2', 2, 'truefalse'),
 646                  'Heading 3',
 647                  array('TF4', 3, 'truefalse'),
 648              ), $structure);
 649      }
 650  
 651      public function test_move_slot_does_not_violate_heading_unique_key() {
 652          $quizobj = $this->create_test_quiz(array(
 653                  'Heading 1',
 654                  array('TF1', 1, 'truefalse'),
 655                  'Heading 2',
 656                  array('TF2', 2, 'truefalse'),
 657                  'Heading 3',
 658                  array('TF3', 3, 'truefalse'),
 659                  array('TF4', 3, 'truefalse'),
 660          ));
 661          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 662  
 663          $idtomove = $structure->get_question_in_slot(4)->slotid;
 664          $idmoveafter = $structure->get_question_in_slot(1)->slotid;
 665          $structure->move_slot($idtomove, $idmoveafter, 1);
 666  
 667          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 668          $this->assert_quiz_layout(array(
 669                  'Heading 1',
 670                  array('TF1', 1, 'truefalse'),
 671                  array('TF4', 1, 'truefalse'),
 672                  'Heading 2',
 673                  array('TF2', 2, 'truefalse'),
 674                  'Heading 3',
 675                  array('TF3', 3, 'truefalse'),
 676          ), $structure);
 677      }
 678  
 679      public function test_quiz_remove_slot() {
 680          $quizobj = $this->create_test_quiz(array(
 681                  array('TF1', 1, 'truefalse'),
 682                  array('TF2', 1, 'truefalse'),
 683                  'Heading 2',
 684                  array('TF3', 2, 'truefalse'),
 685              ));
 686          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 687  
 688          $structure->remove_slot(2);
 689  
 690          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 691          $this->assert_quiz_layout(array(
 692                  array('TF1', 1, 'truefalse'),
 693                  'Heading 2',
 694                  array('TF3', 2, 'truefalse'),
 695              ), $structure);
 696      }
 697  
 698      public function test_quiz_removing_a_random_question_deletes_the_question() {
 699          global $DB;
 700  
 701          $this->resetAfterTest(true);
 702          $this->setAdminUser();
 703  
 704          $quizobj = $this->create_test_quiz(array(
 705                  array('TF1', 1, 'truefalse'),
 706              ));
 707  
 708          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 709          $cat = $questiongenerator->create_question_category();
 710          quiz_add_random_questions($quizobj->get_quiz(), 1, $cat->id, 1, false);
 711          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 712          $randomq = $DB->get_record('question', array('qtype' => 'random'));
 713  
 714          $structure->remove_slot(2);
 715  
 716          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 717          $this->assert_quiz_layout(array(
 718                  array('TF1', 1, 'truefalse'),
 719              ), $structure);
 720          $this->assertFalse($DB->record_exists('question', array('id' => $randomq->id)));
 721      }
 722  
 723      /**
 724       * Unit test to make sue it is not possible to remove all slots in a section at once.
 725       *
 726       * @expectedException coding_exception
 727       */
 728      public function test_cannot_remove_all_slots_in_a_section() {
 729          $quizobj = $this->create_test_quiz(array(
 730              array('TF1', 1, 'truefalse'),
 731              array('TF2', 1, 'truefalse'),
 732              'Heading 2',
 733              array('TF3', 2, 'truefalse'),
 734          ));
 735          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 736  
 737          $structure->remove_slot(1);
 738          $structure->remove_slot(2);
 739      }
 740  
 741      /**
 742       * @expectedException coding_exception
 743       */
 744      public function test_cannot_remove_last_slot_in_a_section() {
 745          $quizobj = $this->create_test_quiz(array(
 746                  array('TF1', 1, 'truefalse'),
 747                  array('TF2', 1, 'truefalse'),
 748                  'Heading 2',
 749                  array('TF3', 2, 'truefalse'),
 750              ));
 751          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 752  
 753          $structure->remove_slot(3);
 754      }
 755  
 756      public function test_can_remove_last_question_in_a_quiz() {
 757          $quizobj = $this->create_test_quiz(array(
 758                  'Heading 1',
 759                  array('TF1', 1, 'truefalse'),
 760              ));
 761          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 762  
 763          $structure->remove_slot(1);
 764  
 765          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 766          $cat = $questiongenerator->create_question_category();
 767          $q = $questiongenerator->create_question('truefalse', null,
 768                  array('name' => 'TF2', 'category' => $cat->id));
 769  
 770          quiz_add_quiz_question($q->id, $quizobj->get_quiz(), 0);
 771          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 772  
 773          $this->assert_quiz_layout(array(
 774                  'Heading 1',
 775                  array('TF2', 1, 'truefalse'),
 776          ), $structure);
 777      }
 778  
 779      public function test_add_question_updates_headings() {
 780          $quizobj = $this->create_test_quiz(array(
 781                  array('TF1', 1, 'truefalse'),
 782                  'Heading 2',
 783                  array('TF2', 2, 'truefalse'),
 784          ));
 785  
 786          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 787          $cat = $questiongenerator->create_question_category();
 788          $q = $questiongenerator->create_question('truefalse', null,
 789                  array('name' => 'TF3', 'category' => $cat->id));
 790  
 791          quiz_add_quiz_question($q->id, $quizobj->get_quiz(), 1);
 792  
 793          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 794          $this->assert_quiz_layout(array(
 795                  array('TF1', 1, 'truefalse'),
 796                  array('TF3', 1, 'truefalse'),
 797                  'Heading 2',
 798                  array('TF2', 2, 'truefalse'),
 799          ), $structure);
 800      }
 801  
 802      public function test_add_question_updates_headings_even_with_one_question_sections() {
 803          $quizobj = $this->create_test_quiz(array(
 804                  'Heading 1',
 805                  array('TF1', 1, 'truefalse'),
 806                  'Heading 2',
 807                  array('TF2', 2, 'truefalse'),
 808                  'Heading 3',
 809                  array('TF3', 3, 'truefalse'),
 810          ));
 811  
 812          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 813          $cat = $questiongenerator->create_question_category();
 814          $q = $questiongenerator->create_question('truefalse', null,
 815                  array('name' => 'TF4', 'category' => $cat->id));
 816  
 817          quiz_add_quiz_question($q->id, $quizobj->get_quiz(), 1);
 818  
 819          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 820          $this->assert_quiz_layout(array(
 821                  'Heading 1',
 822                  array('TF1', 1, 'truefalse'),
 823                  array('TF4', 1, 'truefalse'),
 824                  'Heading 2',
 825                  array('TF2', 2, 'truefalse'),
 826                  'Heading 3',
 827                  array('TF3', 3, 'truefalse'),
 828          ), $structure);
 829      }
 830  
 831      public function test_add_question_at_end_does_not_update_headings() {
 832          $quizobj = $this->create_test_quiz(array(
 833                  array('TF1', 1, 'truefalse'),
 834                  'Heading 2',
 835                  array('TF2', 2, 'truefalse'),
 836          ));
 837  
 838          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 839          $cat = $questiongenerator->create_question_category();
 840          $q = $questiongenerator->create_question('truefalse', null,
 841                  array('name' => 'TF3', 'category' => $cat->id));
 842  
 843          quiz_add_quiz_question($q->id, $quizobj->get_quiz(), 0);
 844  
 845          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 846          $this->assert_quiz_layout(array(
 847                  array('TF1', 1, 'truefalse'),
 848                  'Heading 2',
 849                  array('TF2', 2, 'truefalse'),
 850                  array('TF3', 2, 'truefalse'),
 851          ), $structure);
 852      }
 853  
 854      public function test_remove_page_break() {
 855          $quizobj = $this->create_test_quiz(array(
 856                  array('TF1', 1, 'truefalse'),
 857                  array('TF2', 2, 'truefalse'),
 858              ));
 859          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 860  
 861          $slotid = $structure->get_question_in_slot(2)->slotid;
 862          $slots = $structure->update_page_break($slotid, \mod_quiz\repaginate::LINK);
 863  
 864          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 865          $this->assert_quiz_layout(array(
 866                  array('TF1', 1, 'truefalse'),
 867                  array('TF2', 1, 'truefalse'),
 868              ), $structure);
 869      }
 870  
 871      public function test_add_page_break() {
 872          $quizobj = $this->create_test_quiz(array(
 873                  array('TF1', 1, 'truefalse'),
 874                  array('TF2', 1, 'truefalse'),
 875          ));
 876          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 877  
 878          $slotid = $structure->get_question_in_slot(2)->slotid;
 879          $slots = $structure->update_page_break($slotid, \mod_quiz\repaginate::UNLINK);
 880  
 881          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 882          $this->assert_quiz_layout(array(
 883                  array('TF1', 1, 'truefalse'),
 884                  array('TF2', 2, 'truefalse'),
 885          ), $structure);
 886      }
 887  
 888      public function test_update_question_dependency() {
 889          $quizobj = $this->create_test_quiz(array(
 890                  array('TF1', 1, 'truefalse'),
 891                  array('TF2', 1, 'truefalse'),
 892          ));
 893          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 894  
 895          // Test adding a dependency.
 896          $slotid = $structure->get_slot_id_for_slot(2);
 897          $structure->update_question_dependency($slotid, true);
 898  
 899          // Having called update page break, we need to reload $structure.
 900          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 901          $this->assertEquals(1, $structure->is_question_dependent_on_previous_slot(2));
 902  
 903          // Test removing a dependency.
 904          $structure->update_question_dependency($slotid, false);
 905  
 906          // Having called update page break, we need to reload $structure.
 907          $structure = \mod_quiz\structure::create_for_quiz($quizobj);
 908          $this->assertEquals(0, $structure->is_question_dependent_on_previous_slot(2));
 909      }
 910  
 911      /**
 912       * Data provider for the get_slot_tags_for_slot test.
 913       */
 914      public function get_slot_tags_for_slot_test_cases() {
 915          return [
 916              'incorrect slot id' => [
 917                  'layout' => [
 918                      ['TF1', 1, 'truefalse'],
 919                      ['TF2', 1, 'truefalse'],
 920                      ['TF3', 1, 'truefalse']
 921                  ],
 922                  'tagnames' => [
 923                      ['foo'],
 924                      ['bar'],
 925                      ['baz']
 926                  ],
 927                  'slotnumber' => null,
 928                  'expected' => []
 929              ],
 930              'no tags' => [
 931                  'layout' => [
 932                      ['TF1', 1, 'truefalse'],
 933                      ['TF2', 1, 'truefalse'],
 934                      ['TF3', 1, 'truefalse']
 935                  ],
 936                  'tagnames' => [
 937                      ['foo'],
 938                      [],
 939                      ['baz']
 940                  ],
 941                  'slotnumber' => 2,
 942                  'expected' => []
 943              ],
 944              'one tag 1' => [
 945                  'layout' => [
 946                      ['TF1', 1, 'truefalse'],
 947                      ['TF2', 1, 'truefalse'],
 948                      ['TF3', 1, 'truefalse']
 949                  ],
 950                  'tagnames' => [
 951                      ['foo'],
 952                      ['bar'],
 953                      ['baz']
 954                  ],
 955                  'slotnumber' => 1,
 956                  'expected' => ['foo']
 957              ],
 958              'one tag 2' => [
 959                  'layout' => [
 960                      ['TF1', 1, 'truefalse'],
 961                      ['TF2', 1, 'truefalse'],
 962                      ['TF3', 1, 'truefalse']
 963                  ],
 964                  'tagnames' => [
 965                      ['foo'],
 966                      ['bar'],
 967                      ['baz']
 968                  ],
 969                  'slotnumber' => 2,
 970                  'expected' => ['bar']
 971              ],
 972              'multiple tags 1' => [
 973                  'layout' => [
 974                      ['TF1', 1, 'truefalse'],
 975                      ['TF2', 1, 'truefalse'],
 976                      ['TF3', 1, 'truefalse']
 977                  ],
 978                  'tagnames' => [
 979                      ['foo', 'bar'],
 980                      ['bar'],
 981                      ['baz']
 982                  ],
 983                  'slotnumber' => 1,
 984                  'expected' => ['foo', 'bar']
 985              ],
 986              'multiple tags 2' => [
 987                  'layout' => [
 988                      ['TF1', 1, 'truefalse'],
 989                      ['TF2', 1, 'truefalse'],
 990                      ['TF3', 1, 'truefalse']
 991                  ],
 992                  'tagnames' => [
 993                      ['foo', 'bar'],
 994                      ['bar', 'baz'],
 995                      ['baz']
 996                  ],
 997                  'slotnumber' => 2,
 998                  'expected' => ['bar', 'baz']
 999              ]
1000          ];
1001      }
1002  
1003      /**
1004       * @dataProvider get_slot_tags_for_slot_test_cases()
1005       * @param  array $layout Quiz layout for create_test_quiz function
1006       * @param  array $tagnames Tags to create for each question slot
1007       * @param  int $slotnumber The slot number to select tags from
1008       * @param  string[] $expected The tags expected for the given $slotnumber
1009       */
1010      public function test_get_slot_tags_for_slot($layout, $tagnames, $slotnumber, $expected) {
1011          global $DB;
1012          $this->resetAfterTest();
1013  
1014          $quiz = $this->create_test_quiz($layout);
1015          $structure = \mod_quiz\structure::create_for_quiz($quiz);
1016          $collid = core_tag_area::get_collection('core', 'question');
1017          $slottagrecords = [];
1018  
1019          if (is_null($slotnumber)) {
1020              // Null slot number means to create a non-existent slot id.
1021              $slot = $structure->get_last_slot();
1022              $slotid = $slot->id + 100;
1023          } else {
1024              $slot = $structure->get_slot_by_number($slotnumber);
1025              $slotid = $slot->id;
1026          }
1027  
1028          foreach ($tagnames as $index => $slottagnames) {
1029              $tagslotnumber = $index + 1;
1030              $tagslotid = $structure->get_slot_id_for_slot($tagslotnumber);
1031              $tags = core_tag_tag::create_if_missing($collid, $slottagnames);
1032              $records = array_map(function($tag) use ($tagslotid) {
1033                  return (object) [
1034                      'slotid' => $tagslotid,
1035                      'tagid' => $tag->id,
1036                      'tagname' => $tag->name
1037                  ];
1038              }, array_values($tags));
1039              $slottagrecords = array_merge($slottagrecords, $records);
1040          }
1041  
1042          $DB->insert_records('quiz_slot_tags', $slottagrecords);
1043  
1044          $actualslottags = $structure->get_slot_tags_for_slot_id($slotid);
1045          $actual = array_map(function($slottag) {
1046              return $slottag->tagname;
1047          }, $actualslottags);
1048  
1049          sort($expected);
1050          sort($actual);
1051  
1052          $this->assertEquals($expected, $actual);
1053      }
1054  
1055      /**
1056       * Test for can_add_random_questions.
1057       */
1058      public function test_can_add_random_questions() {
1059          $this->resetAfterTest();
1060  
1061          $quiz = $this->create_test_quiz([]);
1062          $course = $quiz->get_course();
1063  
1064          $generator = $this->getDataGenerator();
1065          $teacher = $generator->create_and_enrol($course, 'editingteacher');
1066          $noneditingteacher = $generator->create_and_enrol($course, 'teacher');
1067  
1068          $this->setUser($teacher);
1069          $structure = \mod_quiz\structure::create_for_quiz($quiz);
1070          $this->assertTrue($structure->can_add_random_questions());
1071  
1072          $this->setUser($noneditingteacher);
1073          $structure = \mod_quiz\structure::create_for_quiz($quiz);
1074          $this->assertFalse($structure->can_add_random_questions());
1075      }
1076  }