Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace mod_quiz;
  18  
  19  use context_module;
  20  use core\output\inplace_editable;
  21  use mod_quiz\question\bank\qbank_helper;
  22  use mod_quiz\question\qubaids_for_quiz;
  23  use stdClass;
  24  
  25  /**
  26   * Quiz structure class.
  27   *
  28   * The structure of the quiz. That is, which questions it is built up
  29   * from. This is used on the Edit quiz page (edit.php) and also when
  30   * starting an attempt at the quiz (startattempt.php). Once an attempt
  31   * has been started, then the attempt holds the specific set of questions
  32   * that that student should answer, and we no longer use this class.
  33   *
  34   * @package   mod_quiz
  35   * @copyright 2014 The Open University
  36   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class structure {
  39      /** @var quiz_settings the quiz this is the structure of. */
  40      protected $quizobj = null;
  41  
  42      /**
  43       * @var stdClass[] the questions in this quiz. Contains the row from the questions
  44       * table, with the data from the quiz_slots table added, and also question_categories.contextid.
  45       */
  46      protected $questions = [];
  47  
  48      /** @var stdClass[] quiz_slots.slot => the quiz_slots rows for this quiz, augmented by sectionid. */
  49      protected $slotsinorder = [];
  50  
  51      /**
  52       * @var stdClass[] currently a dummy. Holds data that will match the
  53       * quiz_sections, once it exists.
  54       */
  55      protected $sections = [];
  56  
  57      /** @var bool caches the results of can_be_edited. */
  58      protected $canbeedited = null;
  59  
  60      /** @var bool caches the results of can_add_random_question. */
  61      protected $canaddrandom = null;
  62  
  63      /**
  64       * Create an instance of this class representing an empty quiz.
  65       *
  66       * @return structure
  67       */
  68      public static function create() {
  69          return new self();
  70      }
  71  
  72      /**
  73       * Create an instance of this class representing the structure of a given quiz.
  74       *
  75       * @param quiz_settings $quizobj the quiz.
  76       * @return structure
  77       */
  78      public static function create_for_quiz($quizobj) {
  79          $structure = self::create();
  80          $structure->quizobj = $quizobj;
  81          $structure->populate_structure();
  82          return $structure;
  83      }
  84  
  85      /**
  86       * Whether there are any questions in the quiz.
  87       *
  88       * @return bool true if there is at least one question in the quiz.
  89       */
  90      public function has_questions() {
  91          return !empty($this->questions);
  92      }
  93  
  94      /**
  95       * Get the number of questions in the quiz.
  96       *
  97       * @return int the number of questions in the quiz.
  98       */
  99      public function get_question_count() {
 100          return count($this->questions);
 101      }
 102  
 103      /**
 104       * Get the information about the question with this id.
 105       *
 106       * @param int $questionid The question id.
 107       * @return stdClass the data from the questions table, augmented with
 108       * question_category.contextid, and the quiz_slots data for the question in this quiz.
 109       */
 110      public function get_question_by_id($questionid) {
 111          return $this->questions[$questionid];
 112      }
 113  
 114      /**
 115       * Get the information about the question in a given slot.
 116       *
 117       * @param int $slotnumber the index of the slot in question.
 118       * @return stdClass the data from the questions table, augmented with
 119       * question_category.contextid, and the quiz_slots data for the question in this quiz.
 120       */
 121      public function get_question_in_slot($slotnumber) {
 122          return $this->questions[$this->slotsinorder[$slotnumber]->questionid];
 123      }
 124  
 125      /**
 126       * Get the name of the question in a given slot.
 127       *
 128       * @param int $slotnumber the index of the slot in question.
 129       * @return stdClass the data from the questions table, augmented with
 130       */
 131      public function get_question_name_in_slot($slotnumber) {
 132          return $this->questions[$this->slotsinorder[$slotnumber]->name];
 133      }
 134  
 135      /**
 136       * Get the displayed question number (or 'i') for a given slot.
 137       *
 138       * @param int $slotnumber the index of the slot in question.
 139       * @return string the question number ot display for this slot.
 140       */
 141      public function get_displayed_number_for_slot($slotnumber) {
 142          $slot = $this->slotsinorder[$slotnumber];
 143          return $slot->displaynumber ?? $slot->defaultnumber;
 144      }
 145  
 146      /**
 147       * Check the question has a number that could be customised.
 148       *
 149       * @param int $slotnumber
 150       * @return bool
 151       */
 152      public function can_display_number_be_customised(int $slotnumber): bool {
 153          return $this->is_real_question($slotnumber) && !quiz_has_attempts($this->quizobj->get_quizid());
 154      }
 155  
 156      /**
 157       * Check whether the question number is customised.
 158       *
 159       * @param int $slotid
 160       * @return bool
 161       * @todo MDL-76612 Final deprecation in Moodle 4.6
 162       * @deprecated since 4.2. $slot->displayednumber is no longer used. If you need this,
 163       *      use isset(...->displaynumber), but this method was not used.
 164       */
 165      public function is_display_number_customised(int $slotid): bool {
 166          $slotobj = $this->get_slot_by_id($slotid);
 167          return isset($slotobj->displaynumber);
 168      }
 169  
 170      /**
 171       * Make slot display number in place editable api call.
 172  
 173       * @param int $slotid
 174       * @param \context $context
 175       * @return \core\output\inplace_editable
 176       */
 177      public function make_slot_display_number_in_place_editable(int $slotid, \context $context): \core\output\inplace_editable {
 178          $slot = $this->get_slot_by_id($slotid);
 179          $editable = has_capability('mod/quiz:manage', $context);
 180  
 181          // Get the current value.
 182          $value = $slot->displaynumber ?? $slot->defaultnumber;
 183          $displayvalue = s($value);
 184  
 185          return new inplace_editable('mod_quiz', 'slotdisplaynumber', $slotid,
 186                  $editable, $displayvalue, $value,
 187                  get_string('edit_slotdisplaynumber_hint', 'mod_quiz'),
 188                  get_string('edit_slotdisplaynumber_label', 'mod_quiz', $displayvalue));
 189      }
 190  
 191      /**
 192       * Get the page a given slot is on.
 193       *
 194       * @param int $slotnumber the index of the slot in question.
 195       * @return int the page number of the page that slot is on.
 196       */
 197      public function get_page_number_for_slot($slotnumber) {
 198          return $this->slotsinorder[$slotnumber]->page;
 199      }
 200  
 201      /**
 202       * Get the slot id of a given slot slot.
 203       *
 204       * @param int $slotnumber the index of the slot in question.
 205       * @return int the page number of the page that slot is on.
 206       */
 207      public function get_slot_id_for_slot($slotnumber) {
 208          return $this->slotsinorder[$slotnumber]->id;
 209      }
 210  
 211      /**
 212       * Get the question type in a given slot.
 213       *
 214       * @param int $slotnumber the index of the slot in question.
 215       * @return string the question type (e.g. multichoice).
 216       */
 217      public function get_question_type_for_slot($slotnumber) {
 218          return $this->questions[$this->slotsinorder[$slotnumber]->questionid]->qtype;
 219      }
 220  
 221      /**
 222       * Whether it would be possible, given the question types, etc. for the
 223       * question in the given slot to require that the previous question had been
 224       * answered before this one is displayed.
 225       *
 226       * @param int $slotnumber the index of the slot in question.
 227       * @return bool can this question require the previous one.
 228       */
 229      public function can_question_depend_on_previous_slot($slotnumber) {
 230          return $slotnumber > 1 && $this->can_finish_during_the_attempt($slotnumber - 1);
 231      }
 232  
 233      /**
 234       * Whether it is possible for another question to depend on this one finishing.
 235       * Note that the answer is not exact, because of random questions, and sometimes
 236       * questions cannot be depended upon because of quiz options.
 237       *
 238       * @param int $slotnumber the index of the slot in question.
 239       * @return bool can this question finish naturally during the attempt?
 240       */
 241      public function can_finish_during_the_attempt($slotnumber) {
 242          if ($this->quizobj->get_navigation_method() == QUIZ_NAVMETHOD_SEQ) {
 243              return false;
 244          }
 245  
 246          if ($this->slotsinorder[$slotnumber]->section->shufflequestions) {
 247              return false;
 248          }
 249  
 250          if (in_array($this->get_question_type_for_slot($slotnumber), ['random', 'missingtype'])) {
 251              return \question_engine::can_questions_finish_during_the_attempt(
 252                      $this->quizobj->get_quiz()->preferredbehaviour);
 253          }
 254  
 255          if (isset($this->slotsinorder[$slotnumber]->canfinish)) {
 256              return $this->slotsinorder[$slotnumber]->canfinish;
 257          }
 258  
 259          try {
 260              $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context());
 261              $tempslot = $quba->add_question(\question_bank::load_question(
 262                      $this->slotsinorder[$slotnumber]->questionid));
 263              $quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour);
 264              $quba->start_all_questions();
 265  
 266              $this->slotsinorder[$slotnumber]->canfinish = $quba->can_question_finish_during_attempt($tempslot);
 267              return $this->slotsinorder[$slotnumber]->canfinish;
 268          } catch (\Exception $e) {
 269              // If the question fails to start, this should not block editing.
 270              return false;
 271          }
 272      }
 273  
 274      /**
 275       * Whether it would be possible, given the question types, etc. for the
 276       * question in the given slot to require that the previous question had been
 277       * answered before this one is displayed.
 278       *
 279       * @param int $slotnumber the index of the slot in question.
 280       * @return bool can this question require the previous one.
 281       */
 282      public function is_question_dependent_on_previous_slot($slotnumber) {
 283          return $this->slotsinorder[$slotnumber]->requireprevious;
 284      }
 285  
 286      /**
 287       * Is a particular question in this attempt a real question, or something like a description.
 288       *
 289       * @param int $slotnumber the index of the slot in question.
 290       * @return bool whether that question is a real question.
 291       */
 292      public function is_real_question($slotnumber) {
 293          return $this->get_question_in_slot($slotnumber)->length != 0;
 294      }
 295  
 296      /**
 297       * Does the current user have '...use' capability over the question(s) in a given slot?
 298       *
 299       *
 300       * @param int $slotnumber the index of the slot in question.
 301       * @return bool true if they have the required capability.
 302       */
 303      public function has_use_capability(int $slotnumber): bool {
 304          $slot = $this->slotsinorder[$slotnumber];
 305          if (is_numeric($slot->questionid)) {
 306              // Non-random question.
 307              return question_has_capability_on($this->get_question_by_id($slot->questionid), 'use');
 308          } else {
 309              // Random question.
 310              $context = \context::instance_by_id($slot->contextid);
 311              return has_capability('moodle/question:useall', $context);
 312          }
 313      }
 314  
 315      /**
 316       * Get the course id that the quiz belongs to.
 317       *
 318       * @return int the course.id for the quiz.
 319       */
 320      public function get_courseid() {
 321          return $this->quizobj->get_courseid();
 322      }
 323  
 324      /**
 325       * Get the course module id of the quiz.
 326       *
 327       * @return int the course_modules.id for the quiz.
 328       */
 329      public function get_cmid() {
 330          return $this->quizobj->get_cmid();
 331      }
 332  
 333      /**
 334       * Get the quiz context.
 335       *
 336       * @return context_module the context of the quiz that this is the structure of.
 337       */
 338      public function get_context(): context_module {
 339          return $this->quizobj->get_context();
 340      }
 341  
 342      /**
 343       * Get id of the quiz.
 344       *
 345       * @return int the quiz.id for the quiz.
 346       */
 347      public function get_quizid() {
 348          return $this->quizobj->get_quizid();
 349      }
 350  
 351      /**
 352       * Get the quiz object.
 353       *
 354       * @return stdClass the quiz settings row from the database.
 355       */
 356      public function get_quiz() {
 357          return $this->quizobj->get_quiz();
 358      }
 359  
 360      /**
 361       * Quizzes can only be repaginated if they have not been attempted, the
 362       * questions are not shuffled, and there are two or more questions.
 363       *
 364       * @return bool whether this quiz can be repaginated.
 365       */
 366      public function can_be_repaginated() {
 367          return $this->can_be_edited() && $this->get_question_count() >= 2;
 368      }
 369  
 370      /**
 371       * Quizzes can only be edited if they have not been attempted.
 372       *
 373       * @return bool whether the quiz can be edited.
 374       */
 375      public function can_be_edited() {
 376          if ($this->canbeedited === null) {
 377              $this->canbeedited = !quiz_has_attempts($this->quizobj->get_quizid());
 378          }
 379          return $this->canbeedited;
 380      }
 381  
 382      /**
 383       * This quiz can only be edited if they have not been attempted.
 384       * Throw an exception if this is not the case.
 385       */
 386      public function check_can_be_edited() {
 387          if (!$this->can_be_edited()) {
 388              $reportlink = quiz_attempt_summary_link_to_reports($this->get_quiz(),
 389                      $this->quizobj->get_cm(), $this->quizobj->get_context());
 390              throw new \moodle_exception('cannoteditafterattempts', 'quiz',
 391                      new \moodle_url('/mod/quiz/edit.php', ['cmid' => $this->get_cmid()]), $reportlink);
 392          }
 393      }
 394  
 395      /**
 396       * How many questions are allowed per page in the quiz.
 397       * This setting controls how frequently extra page-breaks should be inserted
 398       * automatically when questions are added to the quiz.
 399       *
 400       * @return int the number of questions that should be on each page of the
 401       * quiz by default.
 402       */
 403      public function get_questions_per_page() {
 404          return $this->quizobj->get_quiz()->questionsperpage;
 405      }
 406  
 407      /**
 408       * Get quiz slots.
 409       *
 410       * @return stdClass[] the slots in this quiz.
 411       */
 412      public function get_slots() {
 413          return array_column($this->slotsinorder, null, 'id');
 414      }
 415  
 416      /**
 417       * Is this slot the first one on its page?
 418       *
 419       * @param int $slotnumber the index of the slot in question.
 420       * @return bool whether this slot the first one on its page.
 421       */
 422      public function is_first_slot_on_page($slotnumber) {
 423          if ($slotnumber == 1) {
 424              return true;
 425          }
 426          return $this->slotsinorder[$slotnumber]->page != $this->slotsinorder[$slotnumber - 1]->page;
 427      }
 428  
 429      /**
 430       * Is this slot the last one on its page?
 431       *
 432       * @param int $slotnumber the index of the slot in question.
 433       * @return bool whether this slot the last one on its page.
 434       */
 435      public function is_last_slot_on_page($slotnumber) {
 436          if (!isset($this->slotsinorder[$slotnumber + 1])) {
 437              return true;
 438          }
 439          return $this->slotsinorder[$slotnumber]->page != $this->slotsinorder[$slotnumber + 1]->page;
 440      }
 441  
 442      /**
 443       * Is this slot the last one in its section?
 444       *
 445       * @param int $slotnumber the index of the slot in question.
 446       * @return bool whether this slot the last one on its section.
 447       */
 448      public function is_last_slot_in_section($slotnumber) {
 449          return $slotnumber == $this->slotsinorder[$slotnumber]->section->lastslot;
 450      }
 451  
 452      /**
 453       * Is this slot the only one in its section?
 454       *
 455       * @param int $slotnumber the index of the slot in question.
 456       * @return bool whether this slot the only one on its section.
 457       */
 458      public function is_only_slot_in_section($slotnumber) {
 459          return $this->slotsinorder[$slotnumber]->section->firstslot ==
 460                  $this->slotsinorder[$slotnumber]->section->lastslot;
 461      }
 462  
 463      /**
 464       * Is this slot the last one in the quiz?
 465       *
 466       * @param int $slotnumber the index of the slot in question.
 467       * @return bool whether this slot the last one in the quiz.
 468       */
 469      public function is_last_slot_in_quiz($slotnumber) {
 470          end($this->slotsinorder);
 471          return $slotnumber == key($this->slotsinorder);
 472      }
 473  
 474      /**
 475       * Is this the first section in the quiz?
 476       *
 477       * @param stdClass $section the quiz_sections row.
 478       * @return bool whether this is first section in the quiz.
 479       */
 480      public function is_first_section($section) {
 481          return $section->firstslot == 1;
 482      }
 483  
 484      /**
 485       * Is this the last section in the quiz?
 486       *
 487       * @param stdClass $section the quiz_sections row.
 488       * @return bool whether this is first section in the quiz.
 489       */
 490      public function is_last_section($section) {
 491          return $section->id == end($this->sections)->id;
 492      }
 493  
 494      /**
 495       * Does this section only contain one slot?
 496       *
 497       * @param stdClass $section the quiz_sections row.
 498       * @return bool whether this section contains only one slot.
 499       */
 500      public function is_only_one_slot_in_section($section) {
 501          return $section->firstslot == $section->lastslot;
 502      }
 503  
 504      /**
 505       * Get the final slot in the quiz.
 506       *
 507       * @return stdClass the quiz_slots for the final slot in the quiz.
 508       */
 509      public function get_last_slot() {
 510          return end($this->slotsinorder);
 511      }
 512  
 513      /**
 514       * Get a slot by its id. Throws an exception if it is missing.
 515       *
 516       * @param int $slotid the slot id.
 517       * @return stdClass the requested quiz_slots row.
 518       */
 519      public function get_slot_by_id($slotid) {
 520          foreach ($this->slotsinorder as $slot) {
 521              if ($slot->id == $slotid) {
 522                  return $slot;
 523              }
 524          }
 525  
 526          throw new \coding_exception('The \'slotid\' could not be found.');
 527      }
 528  
 529      /**
 530       * Get a slot by its slot number. Throws an exception if it is missing.
 531       *
 532       * @param int $slotnumber The slot number
 533       * @return stdClass
 534       * @throws \coding_exception
 535       */
 536      public function get_slot_by_number($slotnumber) {
 537          if (!array_key_exists($slotnumber, $this->slotsinorder)) {
 538              throw new \coding_exception('The \'slotnumber\' could not be found.');
 539          }
 540          return $this->slotsinorder[$slotnumber];
 541      }
 542  
 543      /**
 544       * Check whether adding a section heading is possible
 545       *
 546       * @param int $pagenumber the number of the page.
 547       * @return boolean
 548       */
 549      public function can_add_section_heading($pagenumber) {
 550          // There is a default section heading on this page,
 551          // do not show adding new section heading in the Add menu.
 552          if ($pagenumber == 1) {
 553              return false;
 554          }
 555          // Get an array of firstslots.
 556          $firstslots = [];
 557          foreach ($this->sections as $section) {
 558              $firstslots[] = $section->firstslot;
 559          }
 560          foreach ($this->slotsinorder as $slot) {
 561              if ($slot->page == $pagenumber) {
 562                  if (in_array($slot->slot, $firstslots)) {
 563                      return false;
 564                  }
 565              }
 566          }
 567          // Do not show the adding section heading on the last add menu.
 568          if ($pagenumber == 0) {
 569              return false;
 570          }
 571          return true;
 572      }
 573  
 574      /**
 575       * Get all the slots in a section of the quiz.
 576       *
 577       * @param int $sectionid the section id.
 578       * @return int[] slot numbers.
 579       */
 580      public function get_slots_in_section($sectionid) {
 581          $slots = [];
 582          foreach ($this->slotsinorder as $slot) {
 583              if ($slot->section->id == $sectionid) {
 584                  $slots[] = $slot->slot;
 585              }
 586          }
 587          return $slots;
 588      }
 589  
 590      /**
 591       * Get all the sections of the quiz.
 592       *
 593       * @return stdClass[] the sections in this quiz.
 594       */
 595      public function get_sections() {
 596          return $this->sections;
 597      }
 598  
 599      /**
 600       * Get a particular section by id.
 601       *
 602       * @return stdClass the section.
 603       */
 604      public function get_section_by_id($sectionid) {
 605          return $this->sections[$sectionid];
 606      }
 607  
 608      /**
 609       * Get the number of questions in the quiz.
 610       *
 611       * @return int the number of questions in the quiz.
 612       */
 613      public function get_section_count() {
 614          return count($this->sections);
 615      }
 616  
 617      /**
 618       * Get the overall quiz grade formatted for display.
 619       *
 620       * @return string the maximum grade for this quiz.
 621       */
 622      public function formatted_quiz_grade() {
 623          return quiz_format_grade($this->get_quiz(), $this->get_quiz()->grade);
 624      }
 625  
 626      /**
 627       * Get the maximum mark for a question, formatted for display.
 628       *
 629       * @param int $slotnumber the index of the slot in question.
 630       * @return string the maximum mark for the question in this slot.
 631       */
 632      public function formatted_question_grade($slotnumber) {
 633          return quiz_format_question_grade($this->get_quiz(), $this->slotsinorder[$slotnumber]->maxmark);
 634      }
 635  
 636      /**
 637       * Get the number of decimal places for displaying overall quiz grades or marks.
 638       *
 639       * @return int the number of decimal places.
 640       */
 641      public function get_decimal_places_for_grades() {
 642          return $this->get_quiz()->decimalpoints;
 643      }
 644  
 645      /**
 646       * Get the number of decimal places for displaying question marks.
 647       *
 648       * @return int the number of decimal places.
 649       */
 650      public function get_decimal_places_for_question_marks() {
 651          return quiz_get_grade_format($this->get_quiz());
 652      }
 653  
 654      /**
 655       * Get any warnings to show at the top of the edit page.
 656       * @return string[] array of strings.
 657       */
 658      public function get_edit_page_warnings() {
 659          $warnings = [];
 660  
 661          if (quiz_has_attempts($this->quizobj->get_quizid())) {
 662              $reviewlink = quiz_attempt_summary_link_to_reports($this->quizobj->get_quiz(),
 663                      $this->quizobj->get_cm(), $this->quizobj->get_context());
 664              $warnings[] = get_string('cannoteditafterattempts', 'quiz', $reviewlink);
 665          }
 666  
 667          return $warnings;
 668      }
 669  
 670      /**
 671       * Get the date information about the current state of the quiz.
 672       * @return string[] array of two strings. First a short summary, then a longer
 673       * explanation of the current state, e.g. for a tool-tip.
 674       */
 675      public function get_dates_summary() {
 676          $timenow = time();
 677          $quiz = $this->quizobj->get_quiz();
 678  
 679          // Exact open and close dates for the tool-tip.
 680          $dates = [];
 681          if ($quiz->timeopen > 0) {
 682              if ($timenow > $quiz->timeopen) {
 683                  $dates[] = get_string('quizopenedon', 'quiz', userdate($quiz->timeopen));
 684              } else {
 685                  $dates[] = get_string('quizwillopen', 'quiz', userdate($quiz->timeopen));
 686              }
 687          }
 688          if ($quiz->timeclose > 0) {
 689              if ($timenow > $quiz->timeclose) {
 690                  $dates[] = get_string('quizclosed', 'quiz', userdate($quiz->timeclose));
 691              } else {
 692                  $dates[] = get_string('quizcloseson', 'quiz', userdate($quiz->timeclose));
 693              }
 694          }
 695          if (empty($dates)) {
 696              $dates[] = get_string('alwaysavailable', 'quiz');
 697          }
 698          $explanation = implode(', ', $dates);
 699  
 700          // Brief summary on the page.
 701          if ($timenow < $quiz->timeopen) {
 702              $currentstatus = get_string('quizisclosedwillopen', 'quiz',
 703                      userdate($quiz->timeopen, get_string('strftimedatetimeshort', 'langconfig')));
 704          } else if ($quiz->timeclose && $timenow <= $quiz->timeclose) {
 705              $currentstatus = get_string('quizisopenwillclose', 'quiz',
 706                      userdate($quiz->timeclose, get_string('strftimedatetimeshort', 'langconfig')));
 707          } else if ($quiz->timeclose && $timenow > $quiz->timeclose) {
 708              $currentstatus = get_string('quizisclosed', 'quiz');
 709          } else {
 710              $currentstatus = get_string('quizisopen', 'quiz');
 711          }
 712  
 713          return [$currentstatus, $explanation];
 714      }
 715  
 716      /**
 717       * Set up this class with the structure for a given quiz.
 718       */
 719      protected function populate_structure() {
 720          global $DB;
 721  
 722          $slots = qbank_helper::get_question_structure($this->quizobj->get_quizid(), $this->quizobj->get_context());
 723          $this->questions = [];
 724          $this->slotsinorder = [];
 725          foreach ($slots as $slotdata) {
 726              $this->questions[$slotdata->questionid] = $slotdata;
 727  
 728              $slot = clone($slotdata);
 729              $slot->quizid = $this->quizobj->get_quizid();
 730              $this->slotsinorder[$slot->slot] = $slot;
 731          }
 732  
 733          // Get quiz sections in ascending order of the firstslot.
 734          $this->sections = $DB->get_records('quiz_sections', ['quizid' => $this->quizobj->get_quizid()], 'firstslot');
 735          $this->populate_slots_with_sections();
 736          $this->populate_question_numbers();
 737      }
 738  
 739      /**
 740       * Fill in the section ids for each slot.
 741       */
 742      public function populate_slots_with_sections() {
 743          $sections = array_values($this->sections);
 744          foreach ($sections as $i => $section) {
 745              if (isset($sections[$i + 1])) {
 746                  $section->lastslot = $sections[$i + 1]->firstslot - 1;
 747              } else {
 748                  $section->lastslot = count($this->slotsinorder);
 749              }
 750              for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) {
 751                  $this->slotsinorder[$slot]->section = $section;
 752              }
 753          }
 754      }
 755  
 756      /**
 757       * Number the questions.
 758       */
 759      protected function populate_question_numbers() {
 760          $number = 1;
 761          foreach ($this->slotsinorder as $slot) {
 762              $question = $this->questions[$slot->questionid];
 763              if ($question->length == 0) {
 764                  $slot->displaynumber = null;
 765                  $slot->defaultnumber = get_string('infoshort', 'quiz');
 766              } else {
 767                  $slot->defaultnumber = $number;
 768              }
 769              if ($slot->displaynumber === '') {
 770                  $slot->displaynumber = null;
 771              }
 772              $number += $question->length;
 773          }
 774      }
 775  
 776      /**
 777       * Get the version options to show on the 'Questions' page for a particular question.
 778       *
 779       * @param int $slotnumber which slot to get the choices for.
 780       * @return stdClass[] other versions of this question. Each object has fields versionid,
 781       *       version and selected. Array is returned most recent version first.
 782       */
 783      public function get_version_choices_for_slot(int $slotnumber): array {
 784          $slot = $this->get_slot_by_number($slotnumber);
 785  
 786          // Get all the versions which exist.
 787          $versions = qbank_helper::get_version_options($slot->questionid);
 788          $latestversion = reset($versions);
 789  
 790          // Format the choices for display.
 791          $versionoptions = [];
 792          foreach ($versions as $version) {
 793              $version->selected = $version->version === $slot->requestedversion;
 794  
 795              if ($version->version === $latestversion->version) {
 796                  $version->versionvalue = get_string('questionversionlatest', 'quiz', $version->version);
 797              } else {
 798                  $version->versionvalue = get_string('questionversion', 'quiz', $version->version);
 799              }
 800  
 801              $versionoptions[] = $version;
 802          }
 803  
 804          // Make a choice for 'Always latest'.
 805          $alwaysuselatest = new stdClass();
 806          $alwaysuselatest->versionid = 0;
 807          $alwaysuselatest->version = 0;
 808          $alwaysuselatest->versionvalue = get_string('alwayslatest', 'quiz');
 809          $alwaysuselatest->selected = $slot->requestedversion === null;
 810          array_unshift($versionoptions, $alwaysuselatest);
 811  
 812          return $versionoptions;
 813      }
 814  
 815      /**
 816       * Move a slot from its current location to a new location.
 817       *
 818       * After calling this method, this class will be in an invalid state, and
 819       * should be discarded if you want to manipulate the structure further.
 820       *
 821       * @param int $idmove id of slot to be moved
 822       * @param int $idmoveafter id of slot to come before slot being moved
 823       * @param int $page new page number of slot being moved
 824       */
 825      public function move_slot($idmove, $idmoveafter, $page) {
 826          global $DB;
 827  
 828          $this->check_can_be_edited();
 829  
 830          $movingslot = $this->get_slot_by_id($idmove);
 831          if (empty($movingslot)) {
 832              throw new \moodle_exception('Bad slot ID ' . $idmove);
 833          }
 834          $movingslotnumber = (int) $movingslot->slot;
 835  
 836          // Empty target slot means move slot to first.
 837          if (empty($idmoveafter)) {
 838              $moveafterslotnumber = 0;
 839          } else {
 840              $moveafterslotnumber = (int) $this->get_slot_by_id($idmoveafter)->slot;
 841          }
 842  
 843          // If the action came in as moving a slot to itself, normalise this to
 844          // moving the slot to after the previous slot.
 845          if ($moveafterslotnumber == $movingslotnumber) {
 846              $moveafterslotnumber = $moveafterslotnumber - 1;
 847          }
 848  
 849          $followingslotnumber = $moveafterslotnumber + 1;
 850          // Prevent checking against non-existence slot when already at the last slot.
 851          if ($followingslotnumber == $movingslotnumber && !$this->is_last_slot_in_quiz($followingslotnumber)) {
 852              $followingslotnumber += 1;
 853          }
 854  
 855          // Check the target page number is OK.
 856          if ($page == 0 || $page === '') {
 857              $page = 1;
 858          }
 859          if (($moveafterslotnumber > 0 && $page < $this->get_page_number_for_slot($moveafterslotnumber)) ||
 860                  $page < 1) {
 861              throw new \coding_exception('The target page number is too small.');
 862          } else if (!$this->is_last_slot_in_quiz($moveafterslotnumber) &&
 863                  $page > $this->get_page_number_for_slot($followingslotnumber)) {
 864              throw new \coding_exception('The target page number is too large.');
 865          }
 866  
 867          // Work out how things are being moved.
 868          $slotreorder = [];
 869          if ($moveafterslotnumber > $movingslotnumber) {
 870              // Moving down.
 871              $slotreorder[$movingslotnumber] = $moveafterslotnumber;
 872              for ($i = $movingslotnumber; $i < $moveafterslotnumber; $i++) {
 873                  $slotreorder[$i + 1] = $i;
 874              }
 875  
 876              $headingmoveafter = $movingslotnumber;
 877              if ($this->is_last_slot_in_quiz($moveafterslotnumber) ||
 878                      $page == $this->get_page_number_for_slot($moveafterslotnumber + 1)) {
 879                  // We are moving to the start of a section, so that heading needs
 880                  // to be included in the ones that move up.
 881                  $headingmovebefore = $moveafterslotnumber + 1;
 882              } else {
 883                  $headingmovebefore = $moveafterslotnumber;
 884              }
 885              $headingmovedirection = -1;
 886  
 887          } else if ($moveafterslotnumber < $movingslotnumber - 1) {
 888              // Moving up.
 889              $slotreorder[$movingslotnumber] = $moveafterslotnumber + 1;
 890              for ($i = $moveafterslotnumber + 1; $i < $movingslotnumber; $i++) {
 891                  $slotreorder[$i] = $i + 1;
 892              }
 893  
 894              if ($page == $this->get_page_number_for_slot($moveafterslotnumber + 1)) {
 895                  // Moving to the start of a section, don't move that section.
 896                  $headingmoveafter = $moveafterslotnumber + 1;
 897              } else {
 898                  // Moving tot the end of the previous section, so move the heading down too.
 899                  $headingmoveafter = $moveafterslotnumber;
 900              }
 901              $headingmovebefore = $movingslotnumber + 1;
 902              $headingmovedirection = 1;
 903          } else {
 904              // Staying in the same place, but possibly changing page/section.
 905              if ($page > $movingslot->page) {
 906                  $headingmoveafter = $movingslotnumber;
 907                  $headingmovebefore = $movingslotnumber + 2;
 908                  $headingmovedirection = -1;
 909              } else if ($page < $movingslot->page) {
 910                  $headingmoveafter = $movingslotnumber - 1;
 911                  $headingmovebefore = $movingslotnumber + 1;
 912                  $headingmovedirection = 1;
 913              } else {
 914                  return; // Nothing to do.
 915              }
 916          }
 917  
 918          if ($this->is_only_slot_in_section($movingslotnumber)) {
 919              throw new \coding_exception('You cannot remove the last slot in a section.');
 920          }
 921  
 922          $trans = $DB->start_delegated_transaction();
 923  
 924          // Slot has moved record new order.
 925          if ($slotreorder) {
 926              update_field_with_unique_index('quiz_slots', 'slot', $slotreorder,
 927                      ['quizid' => $this->get_quizid()]);
 928          }
 929  
 930          // Page has changed. Record it.
 931          if ($movingslot->page != $page) {
 932              $DB->set_field('quiz_slots', 'page', $page,
 933                      ['id' => $movingslot->id]);
 934          }
 935  
 936          // Update section fist slots.
 937          quiz_update_section_firstslots($this->get_quizid(), $headingmovedirection,
 938                  $headingmoveafter, $headingmovebefore);
 939  
 940          // If any pages are now empty, remove them.
 941          $emptypages = $DB->get_fieldset_sql("
 942                  SELECT DISTINCT page - 1
 943                    FROM {quiz_slots} slot
 944                   WHERE quizid = ?
 945                     AND page > 1
 946                     AND NOT EXISTS (SELECT 1 FROM {quiz_slots} WHERE quizid = ? AND page = slot.page - 1)
 947                ORDER BY page - 1 DESC
 948                  ", [$this->get_quizid(), $this->get_quizid()]);
 949  
 950          foreach ($emptypages as $emptypage) {
 951              $DB->execute("
 952                      UPDATE {quiz_slots}
 953                         SET page = page - 1
 954                       WHERE quizid = ?
 955                         AND page > ?
 956                      ", [$this->get_quizid(), $emptypage]);
 957          }
 958  
 959          $trans->allow_commit();
 960  
 961          // Log slot moved event.
 962          $event = \mod_quiz\event\slot_moved::create([
 963              'context' => $this->quizobj->get_context(),
 964              'objectid' => $idmove,
 965              'other' => [
 966                  'quizid' => $this->quizobj->get_quizid(),
 967                  'previousslotnumber' => $movingslotnumber,
 968                  'afterslotnumber' => $moveafterslotnumber,
 969                  'page' => $page
 970               ]
 971          ]);
 972          $event->trigger();
 973      }
 974  
 975      /**
 976       * Refresh page numbering of quiz slots.
 977       * @param stdClass[] $slots (optional) array of slot objects.
 978       * @return stdClass[] array of slot objects.
 979       */
 980      public function refresh_page_numbers($slots = []) {
 981          global $DB;
 982          // Get slots ordered by page then slot.
 983          if (!count($slots)) {
 984              $slots = $DB->get_records('quiz_slots', ['quizid' => $this->get_quizid()], 'slot, page');
 985          }
 986  
 987          // Loop slots. Start the page number at 1 and increment as required.
 988          $pagenumbers = ['new' => 0, 'old' => 0];
 989  
 990          foreach ($slots as $slot) {
 991              if ($slot->page !== $pagenumbers['old']) {
 992                  $pagenumbers['old'] = $slot->page;
 993                  ++$pagenumbers['new'];
 994              }
 995  
 996              if ($pagenumbers['new'] == $slot->page) {
 997                  continue;
 998              }
 999              $slot->page = $pagenumbers['new'];
1000          }
1001  
1002          return $slots;
1003      }
1004  
1005      /**
1006       * Refresh page numbering of quiz slots and save to the database.
1007       *
1008       * @return stdClass[] array of slot objects.
1009       */
1010      public function refresh_page_numbers_and_update_db() {
1011          global $DB;
1012          $this->check_can_be_edited();
1013  
1014          $slots = $this->refresh_page_numbers();
1015  
1016          // Record new page order.
1017          foreach ($slots as $slot) {
1018              $DB->set_field('quiz_slots', 'page', $slot->page,
1019                      ['id' => $slot->id]);
1020          }
1021  
1022          return $slots;
1023      }
1024  
1025      /**
1026       * Remove a slot from a quiz.
1027       *
1028       * @param int $slotnumber The number of the slot to be deleted.
1029       * @throws \coding_exception
1030       */
1031      public function remove_slot($slotnumber) {
1032          global $DB;
1033  
1034          $this->check_can_be_edited();
1035  
1036          if ($this->is_only_slot_in_section($slotnumber) && $this->get_section_count() > 1) {
1037              throw new \coding_exception('You cannot remove the last slot in a section.');
1038          }
1039  
1040          $slot = $DB->get_record('quiz_slots', ['quizid' => $this->get_quizid(), 'slot' => $slotnumber]);
1041          if (!$slot) {
1042              return;
1043          }
1044          $maxslot = $DB->get_field_sql('SELECT MAX(slot) FROM {quiz_slots} WHERE quizid = ?', [$this->get_quizid()]);
1045  
1046          $trans = $DB->start_delegated_transaction();
1047          // Delete the reference if it is a question.
1048          $questionreference = $DB->get_record('question_references',
1049                  ['component' => 'mod_quiz', 'questionarea' => 'slot', 'itemid' => $slot->id]);
1050          if ($questionreference) {
1051              $DB->delete_records('question_references', ['id' => $questionreference->id]);
1052          }
1053          // Delete the set reference if it is a random question.
1054          $questionsetreference = $DB->get_record('question_set_references',
1055                  ['component' => 'mod_quiz', 'questionarea' => 'slot', 'itemid' => $slot->id]);
1056          if ($questionsetreference) {
1057              $DB->delete_records('question_set_references',
1058                  ['id' => $questionsetreference->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
1059          }
1060          $DB->delete_records('quiz_slots', ['id' => $slot->id]);
1061          for ($i = $slot->slot + 1; $i <= $maxslot; $i++) {
1062              $DB->set_field('quiz_slots', 'slot', $i - 1,
1063                      ['quizid' => $this->get_quizid(), 'slot' => $i]);
1064              $this->slotsinorder[$i]->slot = $i - 1;
1065              $this->slotsinorder[$i - 1] = $this->slotsinorder[$i];
1066              unset($this->slotsinorder[$i]);
1067          }
1068  
1069          quiz_update_section_firstslots($this->get_quizid(), -1, $slotnumber);
1070          foreach ($this->sections as $key => $section) {
1071              if ($section->firstslot > $slotnumber) {
1072                  $this->sections[$key]->firstslot--;
1073              }
1074          }
1075          $this->populate_slots_with_sections();
1076          $this->populate_question_numbers();
1077          $this->unset_question($slot->id);
1078  
1079          $this->refresh_page_numbers_and_update_db();
1080  
1081          $trans->allow_commit();
1082  
1083          // Log slot deleted event.
1084          $event = \mod_quiz\event\slot_deleted::create([
1085              'context' => $this->quizobj->get_context(),
1086              'objectid' => $slot->id,
1087              'other' => [
1088                  'quizid' => $this->get_quizid(),
1089                  'slotnumber' => $slotnumber,
1090              ]
1091          ]);
1092          $event->trigger();
1093      }
1094  
1095      /**
1096       * Unset the question object after deletion.
1097       *
1098       * @param int $slotid
1099       */
1100      public function unset_question($slotid) {
1101          foreach ($this->questions as $key => $question) {
1102              if ($question->slotid === $slotid) {
1103                  unset($this->questions[$key]);
1104              }
1105          }
1106      }
1107  
1108      /**
1109       * Change the max mark for a slot.
1110       *
1111       * Save changes to the question grades in the quiz_slots table and any
1112       * corresponding question_attempts.
1113       *
1114       * It does not update 'sumgrades' in the quiz table.
1115       *
1116       * @param stdClass $slot row from the quiz_slots table.
1117       * @param float $maxmark the new maxmark.
1118       * @return bool true if the new grade is different from the old one.
1119       */
1120      public function update_slot_maxmark($slot, $maxmark) {
1121          global $DB;
1122  
1123          if (abs($maxmark - $slot->maxmark) < 1e-7) {
1124              // Grade has not changed. Nothing to do.
1125              return false;
1126          }
1127  
1128          $trans = $DB->start_delegated_transaction();
1129          $previousmaxmark = $slot->maxmark;
1130          $slot->maxmark = $maxmark;
1131          $DB->update_record('quiz_slots', $slot);
1132          \question_engine::set_max_mark_in_attempts(new qubaids_for_quiz($slot->quizid),
1133                  $slot->slot, $maxmark);
1134          $trans->allow_commit();
1135  
1136          // Log slot mark updated event.
1137          // We use $num + 0 as a trick to remove the useless 0 digits from decimals.
1138          $event = \mod_quiz\event\slot_mark_updated::create([
1139              'context' => $this->quizobj->get_context(),
1140              'objectid' => $slot->id,
1141              'other' => [
1142                  'quizid' => $this->get_quizid(),
1143                  'previousmaxmark' => $previousmaxmark + 0,
1144                  'newmaxmark' => $maxmark + 0
1145              ]
1146          ]);
1147          $event->trigger();
1148  
1149          return true;
1150      }
1151  
1152      /**
1153       * Set whether the question in a particular slot requires the previous one.
1154       * @param int $slotid id of slot.
1155       * @param bool $requireprevious if true, set this question to require the previous one.
1156       */
1157      public function update_question_dependency($slotid, $requireprevious) {
1158          global $DB;
1159          $DB->set_field('quiz_slots', 'requireprevious', $requireprevious, ['id' => $slotid]);
1160  
1161          // Log slot require previous event.
1162          $event = \mod_quiz\event\slot_requireprevious_updated::create([
1163              'context' => $this->quizobj->get_context(),
1164              'objectid' => $slotid,
1165              'other' => [
1166                  'quizid' => $this->get_quizid(),
1167                  'requireprevious' => $requireprevious ? 1 : 0
1168              ]
1169          ]);
1170          $event->trigger();
1171      }
1172  
1173      /**
1174       * Update the question display number when is set as customised display number or empy string.
1175       * When the field displaynumber is set to empty string, the automated numbering is used.
1176       * Log the updated displatnumber field.
1177       *
1178       * @param int $slotid id of slot.
1179       * @param string $displaynumber set to customised string as question number or empty string fo autonumbering.
1180       */
1181      public function update_slot_display_number(int $slotid, string $displaynumber): void {
1182          global $DB;
1183          $DB->set_field('quiz_slots', 'displaynumber', $displaynumber, ['id' => $slotid]);
1184          $this->populate_structure();
1185  
1186          // Log slot displaynumber event (customised question number).
1187          $event = \mod_quiz\event\slot_displaynumber_updated::create([
1188                  'context' => $this->quizobj->get_context(),
1189                  'objectid' => $slotid,
1190                  'other' => [
1191                          'quizid' => $this->get_quizid(),
1192                          'displaynumber' => $displaynumber
1193                  ]
1194          ]);
1195          $event->trigger();
1196      }
1197  
1198      /**
1199       * Add/Remove a pagebreak.
1200       *
1201       * Save changes to the slot page relationship in the quiz_slots table and reorders the paging
1202       * for subsequent slots.
1203       *
1204       * @param int $slotid id of slot which we will add/remove the page break before.
1205       * @param int $type repaginate::LINK or repaginate::UNLINK.
1206       * @return stdClass[] array of slot objects.
1207       */
1208      public function update_page_break($slotid, $type) {
1209          global $DB;
1210  
1211          $this->check_can_be_edited();
1212  
1213          $quizslots = $DB->get_records('quiz_slots', ['quizid' => $this->get_quizid()], 'slot');
1214          $repaginate = new repaginate($this->get_quizid(), $quizslots);
1215          $repaginate->repaginate_slots($quizslots[$slotid]->slot, $type);
1216          $slots = $this->refresh_page_numbers_and_update_db();
1217  
1218          if ($type == repaginate::LINK) {
1219              // Log page break created event.
1220              $event = \mod_quiz\event\page_break_deleted::create([
1221                  'context' => $this->quizobj->get_context(),
1222                  'objectid' => $slotid,
1223                  'other' => [
1224                      'quizid' => $this->get_quizid(),
1225                      'slotnumber' => $quizslots[$slotid]->slot
1226                  ]
1227              ]);
1228              $event->trigger();
1229          } else {
1230              // Log page deleted created event.
1231              $event = \mod_quiz\event\page_break_created::create([
1232                  'context' => $this->quizobj->get_context(),
1233                  'objectid' => $slotid,
1234                  'other' => [
1235                      'quizid' => $this->get_quizid(),
1236                      'slotnumber' => $quizslots[$slotid]->slot
1237                  ]
1238              ]);
1239              $event->trigger();
1240          }
1241  
1242          return $slots;
1243      }
1244  
1245      /**
1246       * Add a section heading on a given page and return the sectionid
1247       * @param int $pagenumber the number of the page where the section heading begins.
1248       * @param string|null $heading the heading to add. If not given, a default is used.
1249       */
1250      public function add_section_heading($pagenumber, $heading = null) {
1251          global $DB;
1252          $section = new stdClass();
1253          if ($heading !== null) {
1254              $section->heading = $heading;
1255          } else {
1256              $section->heading = get_string('newsectionheading', 'quiz');
1257          }
1258          $section->quizid = $this->get_quizid();
1259          $slotsonpage = $DB->get_records('quiz_slots', ['quizid' => $this->get_quizid(), 'page' => $pagenumber], 'slot DESC');
1260          $firstslot = end($slotsonpage);
1261          $section->firstslot = $firstslot->slot;
1262          $section->shufflequestions = 0;
1263          $sectionid = $DB->insert_record('quiz_sections', $section);
1264  
1265          // Log section break created event.
1266          $event = \mod_quiz\event\section_break_created::create([
1267              'context' => $this->quizobj->get_context(),
1268              'objectid' => $sectionid,
1269              'other' => [
1270                  'quizid' => $this->get_quizid(),
1271                  'firstslotnumber' => $firstslot->slot,
1272                  'firstslotid' => $firstslot->id,
1273                  'title' => $section->heading,
1274              ]
1275          ]);
1276          $event->trigger();
1277  
1278          return $sectionid;
1279      }
1280  
1281      /**
1282       * Change the heading for a section.
1283       * @param int $id the id of the section to change.
1284       * @param string $newheading the new heading for this section.
1285       */
1286      public function set_section_heading($id, $newheading) {
1287          global $DB;
1288          $section = $DB->get_record('quiz_sections', ['id' => $id], '*', MUST_EXIST);
1289          $section->heading = $newheading;
1290          $DB->update_record('quiz_sections', $section);
1291  
1292          // Log section title updated event.
1293          $firstslot = $DB->get_record('quiz_slots', ['quizid' => $this->get_quizid(), 'slot' => $section->firstslot]);
1294          $event = \mod_quiz\event\section_title_updated::create([
1295              'context' => $this->quizobj->get_context(),
1296              'objectid' => $id,
1297              'other' => [
1298                  'quizid' => $this->get_quizid(),
1299                  'firstslotid' => $firstslot ? $firstslot->id : null,
1300                  'firstslotnumber' => $firstslot ? $firstslot->slot : null,
1301                  'newtitle' => $newheading
1302              ]
1303          ]);
1304          $event->trigger();
1305      }
1306  
1307      /**
1308       * Change the shuffle setting for a section.
1309       * @param int $id the id of the section to change.
1310       * @param bool $shuffle whether this section should be shuffled.
1311       */
1312      public function set_section_shuffle($id, $shuffle) {
1313          global $DB;
1314          $section = $DB->get_record('quiz_sections', ['id' => $id], '*', MUST_EXIST);
1315          $section->shufflequestions = $shuffle;
1316          $DB->update_record('quiz_sections', $section);
1317  
1318          // Log section shuffle updated event.
1319          $event = \mod_quiz\event\section_shuffle_updated::create([
1320              'context' => $this->quizobj->get_context(),
1321              'objectid' => $id,
1322              'other' => [
1323                  'quizid' => $this->get_quizid(),
1324                  'firstslotnumber' => $section->firstslot,
1325                  'shuffle' => $shuffle
1326              ]
1327          ]);
1328          $event->trigger();
1329      }
1330  
1331      /**
1332       * Remove the section heading with the given id
1333       * @param int $sectionid the section to remove.
1334       */
1335      public function remove_section_heading($sectionid) {
1336          global $DB;
1337          $section = $DB->get_record('quiz_sections', ['id' => $sectionid], '*', MUST_EXIST);
1338          if ($section->firstslot == 1) {
1339              throw new \coding_exception('Cannot remove the first section in a quiz.');
1340          }
1341          $DB->delete_records('quiz_sections', ['id' => $sectionid]);
1342  
1343          // Log page deleted created event.
1344          $firstslot = $DB->get_record('quiz_slots', ['quizid' => $this->get_quizid(), 'slot' => $section->firstslot]);
1345          $event = \mod_quiz\event\section_break_deleted::create([
1346              'context' => $this->quizobj->get_context(),
1347              'objectid' => $sectionid,
1348              'other' => [
1349                  'quizid' => $this->get_quizid(),
1350                  'firstslotid' => $firstslot->id,
1351                  'firstslotnumber' => $firstslot->slot
1352              ]
1353          ]);
1354          $event->trigger();
1355      }
1356  
1357      /**
1358       * Whether the current user can add random questions to the quiz or not.
1359       * It is only possible to add a random question if the user has the moodle/question:useall capability
1360       * on at least one of the contexts related to the one where we are currently editing questions.
1361       *
1362       * @return bool
1363       */
1364      public function can_add_random_questions() {
1365          if ($this->canaddrandom === null) {
1366              $quizcontext = $this->quizobj->get_context();
1367              $relatedcontexts = new \core_question\local\bank\question_edit_contexts($quizcontext);
1368              $usablecontexts = $relatedcontexts->having_cap('moodle/question:useall');
1369  
1370              $this->canaddrandom = !empty($usablecontexts);
1371          }
1372  
1373          return $this->canaddrandom;
1374      }
1375  
1376  
1377      /**
1378       * Retrieve the list of slot tags for the given slot id.
1379       *
1380       * @param int $slotid The id for the slot
1381       * @return stdClass[] The list of slot tag records
1382       * @deprecated since Moodle 4.0 MDL-71573
1383       * @todo Final deprecation on Moodle 4.4 MDL-72438
1384       */
1385      public function get_slot_tags_for_slot_id($slotid) {
1386          debugging('Function get_slot_tags_for_slot_id() has been deprecated and the structure
1387           for this method have been moved to filtercondition in question_set_reference table, please
1388            use the new structure instead.', DEBUG_DEVELOPER);
1389          // All the associated code for this method have been removed to get rid of accidental call or errors.
1390          return [];
1391      }
1392  
1393      /**
1394       * Add a random question to the quiz at a given point.
1395       *
1396       * @param int $addonpage the page on which to add the question.
1397       * @param int $number the number of random questions to add.
1398       * @param array $filtercondition the filter condition. Must contain at least a category filter.
1399       */
1400      public function add_random_questions(int $addonpage, int $number, array $filtercondition): void {
1401          global $DB;
1402  
1403          if (!isset($filtercondition['filter']['category'])) {
1404              throw new \invalid_parameter_exception('$filtercondition must contain at least a category filter.');
1405          }
1406          $categoryid = $filtercondition['filter']['category']['values'][0];
1407  
1408          $category = $DB->get_record('question_categories', ['id' => $categoryid]);
1409          if (!$category) {
1410              new \moodle_exception('invalidcategoryid');
1411          }
1412  
1413          $catcontext = \context::instance_by_id($category->contextid);
1414          require_capability('moodle/question:useall', $catcontext);
1415  
1416          // Create the selected number of random questions.
1417          for ($i = 0; $i < $number; $i++) {
1418              // Slot data.
1419              $randomslotdata = new stdClass();
1420              $randomslotdata->quizid = $this->get_quizid();
1421              $randomslotdata->usingcontextid = context_module::instance($this->get_cmid())->id;
1422              $randomslotdata->questionscontextid = $category->contextid;
1423              $randomslotdata->maxmark = 1;
1424  
1425              $randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
1426              $randomslot->set_quiz($this->get_quiz());
1427              $randomslot->set_filter_condition(json_encode($filtercondition));
1428              $randomslot->insert($addonpage);
1429          }
1430      }
1431  }