[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/mod/quiz/classes/ -> structure.php (source)

   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   * Defines the \mod_quiz\structure class.
  19   *
  20   * @package   mod_quiz
  21   * @copyright 2013 The Open University
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace mod_quiz;
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**
  29   * Quiz structure class.
  30   *
  31   * The structure of the quiz. That is, which questions it is built up
  32   * from. This is used on the Edit quiz page (edit.php) and also when
  33   * starting an attempt at the quiz (startattempt.php). Once an attempt
  34   * has been started, then the attempt holds the specific set of questions
  35   * that that student should answer, and we no longer use this class.
  36   *
  37   * @copyright 2014 The Open University
  38   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class structure {
  41      /** @var \quiz the quiz this is the structure of. */
  42      protected $quizobj = null;
  43  
  44      /**
  45       * @var \stdClass[] the questions in this quiz. Contains the row from the questions
  46       * table, with the data from the quiz_slots table added, and also question_categories.contextid.
  47       */
  48      protected $questions = array();
  49  
  50      /** @var \stdClass[] quiz_slots.id => the quiz_slots rows for this quiz, agumented by sectionid. */
  51      protected $slots = array();
  52  
  53      /** @var \stdClass[] quiz_slots.slot => the quiz_slots rows for this quiz, agumented by sectionid. */
  54      protected $slotsinorder = array();
  55  
  56      /**
  57       * @var \stdClass[] currently a dummy. Holds data that will match the
  58       * quiz_sections, once it exists.
  59       */
  60      protected $sections = array();
  61  
  62      /** @var bool caches the results of can_be_edited. */
  63      protected $canbeedited = null;
  64  
  65      /**
  66       * Create an instance of this class representing an empty quiz.
  67       * @return structure
  68       */
  69      public static function create() {
  70          return new self();
  71      }
  72  
  73      /**
  74       * Create an instance of this class representing the structure of a given quiz.
  75       * @param \quiz $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($quizobj->get_quiz());
  82          return $structure;
  83      }
  84  
  85      /**
  86       * Whether there are any questions in the quiz.
  87       * @return bool true if there is at least one question in the quiz.
  88       */
  89      public function has_questions() {
  90          return !empty($this->questions);
  91      }
  92  
  93      /**
  94       * Get the number of questions in the quiz.
  95       * @return int the number of questions in the quiz.
  96       */
  97      public function get_question_count() {
  98          return count($this->questions);
  99      }
 100  
 101      /**
 102       * Get the information about the question with this id.
 103       * @param int $questionid The question id.
 104       * @return \stdClass the data from the questions table, augmented with
 105       * question_category.contextid, and the quiz_slots data for the question in this quiz.
 106       */
 107      public function get_question_by_id($questionid) {
 108          return $this->questions[$questionid];
 109      }
 110  
 111      /**
 112       * Get the information about the question in a given slot.
 113       * @param int $slotnumber the index of the slot in question.
 114       * @return \stdClass the data from the questions table, augmented with
 115       * question_category.contextid, and the quiz_slots data for the question in this quiz.
 116       */
 117      public function get_question_in_slot($slotnumber) {
 118          return $this->questions[$this->slotsinorder[$slotnumber]->questionid];
 119      }
 120  
 121      /**
 122       * Get the displayed question number (or 'i') for a given slot.
 123       * @param int $slotnumber the index of the slot in question.
 124       * @return string the question number ot display for this slot.
 125       */
 126      public function get_displayed_number_for_slot($slotnumber) {
 127          return $this->slotsinorder[$slotnumber]->displayednumber;
 128      }
 129  
 130      /**
 131       * Get the page a given slot is on.
 132       * @param int $slotnumber the index of the slot in question.
 133       * @return int the page number of the page that slot is on.
 134       */
 135      public function get_page_number_for_slot($slotnumber) {
 136          return $this->slotsinorder[$slotnumber]->page;
 137      }
 138  
 139      /**
 140       * Get the slot id of a given slot slot.
 141       * @param int $slotnumber the index of the slot in question.
 142       * @return int the page number of the page that slot is on.
 143       */
 144      public function get_slot_id_for_slot($slotnumber) {
 145          return $this->slotsinorder[$slotnumber]->id;
 146      }
 147  
 148      /**
 149       * Get the question type in a given slot.
 150       * @param int $slotnumber the index of the slot in question.
 151       * @return string the question type (e.g. multichoice).
 152       */
 153      public function get_question_type_for_slot($slotnumber) {
 154          return $this->questions[$this->slotsinorder[$slotnumber]->questionid]->qtype;
 155      }
 156  
 157      /**
 158       * Whether it would be possible, given the question types, etc. for the
 159       * question in the given slot to require that the previous question had been
 160       * answered before this one is displayed.
 161       * @param int $slotnumber the index of the slot in question.
 162       * @return bool can this question require the previous one.
 163       */
 164      public function can_question_depend_on_previous_slot($slotnumber) {
 165          return $slotnumber > 1 && $this->can_finish_during_the_attempt($slotnumber - 1);
 166      }
 167  
 168      /**
 169       * Whether it is possible for another question to depend on this one finishing.
 170       * Note that the answer is not exact, because of random questions, and sometimes
 171       * questions cannot be depended upon because of quiz options.
 172       * @param int $slotnumber the index of the slot in question.
 173       * @return bool can this question finish naturally during the attempt?
 174       */
 175      public function can_finish_during_the_attempt($slotnumber) {
 176          if ($this->quizobj->get_navigation_method() == QUIZ_NAVMETHOD_SEQ) {
 177              return false;
 178          }
 179  
 180          if ($this->slotsinorder[$slotnumber]->section->shufflequestions) {
 181              return false;
 182          }
 183  
 184          if (in_array($this->get_question_type_for_slot($slotnumber), array('random', 'missingtype'))) {
 185              return \question_engine::can_questions_finish_during_the_attempt(
 186                      $this->quizobj->get_quiz()->preferredbehaviour);
 187          }
 188  
 189          if (isset($this->slotsinorder[$slotnumber]->canfinish)) {
 190              return $this->slotsinorder[$slotnumber]->canfinish;
 191          }
 192  
 193          try {
 194              $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context());
 195              $tempslot = $quba->add_question(\question_bank::load_question(
 196                      $this->slotsinorder[$slotnumber]->questionid));
 197              $quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour);
 198              $quba->start_all_questions();
 199  
 200              $this->slotsinorder[$slotnumber]->canfinish = $quba->can_question_finish_during_attempt($tempslot);
 201              return $this->slotsinorder[$slotnumber]->canfinish;
 202          } catch (\Exception $e) {
 203              // If the question fails to start, this should not block editing.
 204              return false;
 205          }
 206      }
 207  
 208      /**
 209       * Whether it would be possible, given the question types, etc. for the
 210       * question in the given slot to require that the previous question had been
 211       * answered before this one is displayed.
 212       * @param int $slotnumber the index of the slot in question.
 213       * @return bool can this question require the previous one.
 214       */
 215      public function is_question_dependent_on_previous_slot($slotnumber) {
 216          return $this->slotsinorder[$slotnumber]->requireprevious;
 217      }
 218  
 219      /**
 220       * Is a particular question in this attempt a real question, or something like a description.
 221       * @param int $slotnumber the index of the slot in question.
 222       * @return bool whether that question is a real question.
 223       */
 224      public function is_real_question($slotnumber) {
 225          return $this->get_question_in_slot($slotnumber)->length != 0;
 226      }
 227  
 228      /**
 229       * Get the course id that the quiz belongs to.
 230       * @return int the course.id for the quiz.
 231       */
 232      public function get_courseid() {
 233          return $this->quizobj->get_courseid();
 234      }
 235  
 236      /**
 237       * Get the course module id of the quiz.
 238       * @return int the course_modules.id for the quiz.
 239       */
 240      public function get_cmid() {
 241          return $this->quizobj->get_cmid();
 242      }
 243  
 244      /**
 245       * Get id of the quiz.
 246       * @return int the quiz.id for the quiz.
 247       */
 248      public function get_quizid() {
 249          return $this->quizobj->get_quizid();
 250      }
 251  
 252      /**
 253       * Get the quiz object.
 254       * @return \stdClass the quiz settings row from the database.
 255       */
 256      public function get_quiz() {
 257          return $this->quizobj->get_quiz();
 258      }
 259  
 260      /**
 261       * Quizzes can only be repaginated if they have not been attempted, the
 262       * questions are not shuffled, and there are two or more questions.
 263       * @return bool whether this quiz can be repaginated.
 264       */
 265      public function can_be_repaginated() {
 266          return $this->can_be_edited() && $this->get_question_count() >= 2;
 267      }
 268  
 269      /**
 270       * Quizzes can only be edited if they have not been attempted.
 271       * @return bool whether the quiz can be edited.
 272       */
 273      public function can_be_edited() {
 274          if ($this->canbeedited === null) {
 275              $this->canbeedited = !quiz_has_attempts($this->quizobj->get_quizid());
 276          }
 277          return $this->canbeedited;
 278      }
 279  
 280      /**
 281       * This quiz can only be edited if they have not been attempted.
 282       * Throw an exception if this is not the case.
 283       */
 284      public function check_can_be_edited() {
 285          if (!$this->can_be_edited()) {
 286              $reportlink = quiz_attempt_summary_link_to_reports($this->get_quiz(),
 287                      $this->quizobj->get_cm(), $this->quizobj->get_context());
 288              throw new \moodle_exception('cannoteditafterattempts', 'quiz',
 289                      new \moodle_url('/mod/quiz/edit.php', array('cmid' => $this->get_cmid())), $reportlink);
 290          }
 291      }
 292  
 293      /**
 294       * How many questions are allowed per page in the quiz.
 295       * This setting controls how frequently extra page-breaks should be inserted
 296       * automatically when questions are added to the quiz.
 297       * @return int the number of questions that should be on each page of the
 298       * quiz by default.
 299       */
 300      public function get_questions_per_page() {
 301          return $this->quizobj->get_quiz()->questionsperpage;
 302      }
 303  
 304      /**
 305       * Get quiz slots.
 306       * @return \stdClass[] the slots in this quiz.
 307       */
 308      public function get_slots() {
 309          return $this->slots;
 310      }
 311  
 312      /**
 313       * Is this slot the first one on its page?
 314       * @param int $slotnumber the index of the slot in question.
 315       * @return bool whether this slot the first one on its page.
 316       */
 317      public function is_first_slot_on_page($slotnumber) {
 318          if ($slotnumber == 1) {
 319              return true;
 320          }
 321          return $this->slotsinorder[$slotnumber]->page != $this->slotsinorder[$slotnumber - 1]->page;
 322      }
 323  
 324      /**
 325       * Is this slot the last one on its page?
 326       * @param int $slotnumber the index of the slot in question.
 327       * @return bool whether this slot the last one on its page.
 328       */
 329      public function is_last_slot_on_page($slotnumber) {
 330          if (!isset($this->slotsinorder[$slotnumber + 1])) {
 331              return true;
 332          }
 333          return $this->slotsinorder[$slotnumber]->page != $this->slotsinorder[$slotnumber + 1]->page;
 334      }
 335  
 336      /**
 337       * Is this slot the last one in its section?
 338       * @param int $slotnumber the index of the slot in question.
 339       * @return bool whether this slot the last one on its section.
 340       */
 341      public function is_last_slot_in_section($slotnumber) {
 342          return $slotnumber == $this->slotsinorder[$slotnumber]->section->lastslot;
 343      }
 344  
 345      /**
 346       * Is this slot the only one in its section?
 347       * @param int $slotnumber the index of the slot in question.
 348       * @return bool whether this slot the only one on its section.
 349       */
 350      public function is_only_slot_in_section($slotnumber) {
 351          return $this->slotsinorder[$slotnumber]->section->firstslot ==
 352                  $this->slotsinorder[$slotnumber]->section->lastslot;
 353      }
 354  
 355      /**
 356       * Is this slot the last one in the quiz?
 357       * @param int $slotnumber the index of the slot in question.
 358       * @return bool whether this slot the last one in the quiz.
 359       */
 360      public function is_last_slot_in_quiz($slotnumber) {
 361          end($this->slotsinorder);
 362          return $slotnumber == key($this->slotsinorder);
 363      }
 364  
 365      /**
 366       * Is this the first section in the quiz?
 367       * @param \stdClass $section the quiz_sections row.
 368       * @return bool whether this is first section in the quiz.
 369       */
 370      public function is_first_section($section) {
 371          return $section->firstslot == 1;
 372      }
 373  
 374      /**
 375       * Is this the last section in the quiz?
 376       * @param \stdClass $section the quiz_sections row.
 377       * @return bool whether this is first section in the quiz.
 378       */
 379      public function is_last_section($section) {
 380          return $section->id == end($this->sections)->id;
 381      }
 382  
 383      /**
 384       * Does this section only contain one slot?
 385       * @param \stdClass $section the quiz_sections row.
 386       * @return bool whether this section contains only one slot.
 387       */
 388      public function is_only_one_slot_in_section($section) {
 389          return $section->firstslot == $section->lastslot;
 390      }
 391  
 392      /**
 393       * Get the final slot in the quiz.
 394       * @return \stdClass the quiz_slots for for the final slot in the quiz.
 395       */
 396      public function get_last_slot() {
 397          return end($this->slotsinorder);
 398      }
 399  
 400      /**
 401       * Get a slot by it's id. Throws an exception if it is missing.
 402       * @param int $slotid the slot id.
 403       * @return \stdClass the requested quiz_slots row.
 404       */
 405      public function get_slot_by_id($slotid) {
 406          if (!array_key_exists($slotid, $this->slots)) {
 407              throw new \coding_exception('The \'slotid\' could not be found.');
 408          }
 409          return $this->slots[$slotid];
 410      }
 411  
 412      /**
 413       * Check whether adding a section heading is possible
 414       * @param int $pagenumber the number of the page.
 415       * @return boolean
 416       */
 417      public function can_add_section_heading($pagenumber) {
 418          // There is a default section heading on this page,
 419          // do not show adding new section heading in the Add menu.
 420          if ($pagenumber == 1) {
 421              return false;
 422          }
 423          // Get an array of firstslots.
 424          $firstslots = array();
 425          foreach ($this->sections as $section) {
 426              $firstslots[] = $section->firstslot;
 427          }
 428          foreach ($this->slotsinorder as $slot) {
 429              if ($slot->page == $pagenumber) {
 430                  if (in_array($slot->slot, $firstslots)) {
 431                      return false;
 432                  }
 433              }
 434          }
 435          // Do not show the adding section heading on the last add menu.
 436          if ($pagenumber == 0) {
 437              return false;
 438          }
 439          return true;
 440      }
 441  
 442      /**
 443       * Get all the slots in a section of the quiz.
 444       * @param int $sectionid the section id.
 445       * @return int[] slot numbers.
 446       */
 447      public function get_slots_in_section($sectionid) {
 448          $slots = array();
 449          foreach ($this->slotsinorder as $slot) {
 450              if ($slot->section->id == $sectionid) {
 451                  $slots[] = $slot->slot;
 452              }
 453          }
 454          return $slots;
 455      }
 456  
 457      /**
 458       * Get all the sections of the quiz.
 459       * @return \stdClass[] the sections in this quiz.
 460       */
 461      public function get_sections() {
 462          return $this->sections;
 463      }
 464  
 465      /**
 466       * Get a particular section by id.
 467       * @return \stdClass the section.
 468       */
 469      public function get_section_by_id($sectionid) {
 470          return $this->sections[$sectionid];
 471      }
 472  
 473      /**
 474       * Get the number of questions in the quiz.
 475       * @return int the number of questions in the quiz.
 476       */
 477      public function get_section_count() {
 478          return count($this->sections);
 479      }
 480  
 481      /**
 482       * Get the overall quiz grade formatted for display.
 483       * @return string the maximum grade for this quiz.
 484       */
 485      public function formatted_quiz_grade() {
 486          return quiz_format_grade($this->get_quiz(), $this->get_quiz()->grade);
 487      }
 488  
 489      /**
 490       * Get the maximum mark for a question, formatted for display.
 491       * @param int $slotnumber the index of the slot in question.
 492       * @return string the maximum mark for the question in this slot.
 493       */
 494      public function formatted_question_grade($slotnumber) {
 495          return quiz_format_question_grade($this->get_quiz(), $this->slotsinorder[$slotnumber]->maxmark);
 496      }
 497  
 498      /**
 499       * Get the number of decimal places for displyaing overall quiz grades or marks.
 500       * @return int the number of decimal places.
 501       */
 502      public function get_decimal_places_for_grades() {
 503          return $this->get_quiz()->decimalpoints;
 504      }
 505  
 506      /**
 507       * Get the number of decimal places for displyaing question marks.
 508       * @return int the number of decimal places.
 509       */
 510      public function get_decimal_places_for_question_marks() {
 511          return quiz_get_grade_format($this->get_quiz());
 512      }
 513  
 514      /**
 515       * Get any warnings to show at the top of the edit page.
 516       * @return string[] array of strings.
 517       */
 518      public function get_edit_page_warnings() {
 519          $warnings = array();
 520  
 521          if (quiz_has_attempts($this->quizobj->get_quizid())) {
 522              $reviewlink = quiz_attempt_summary_link_to_reports($this->quizobj->get_quiz(),
 523                      $this->quizobj->get_cm(), $this->quizobj->get_context());
 524              $warnings[] = get_string('cannoteditafterattempts', 'quiz', $reviewlink);
 525          }
 526  
 527          return $warnings;
 528      }
 529  
 530      /**
 531       * Get the date information about the current state of the quiz.
 532       * @return string[] array of two strings. First a short summary, then a longer
 533       * explanation of the current state, e.g. for a tool-tip.
 534       */
 535      public function get_dates_summary() {
 536          $timenow = time();
 537          $quiz = $this->quizobj->get_quiz();
 538  
 539          // Exact open and close dates for the tool-tip.
 540          $dates = array();
 541          if ($quiz->timeopen > 0) {
 542              if ($timenow > $quiz->timeopen) {
 543                  $dates[] = get_string('quizopenedon', 'quiz', userdate($quiz->timeopen));
 544              } else {
 545                  $dates[] = get_string('quizwillopen', 'quiz', userdate($quiz->timeopen));
 546              }
 547          }
 548          if ($quiz->timeclose > 0) {
 549              if ($timenow > $quiz->timeclose) {
 550                  $dates[] = get_string('quizclosed', 'quiz', userdate($quiz->timeclose));
 551              } else {
 552                  $dates[] = get_string('quizcloseson', 'quiz', userdate($quiz->timeclose));
 553              }
 554          }
 555          if (empty($dates)) {
 556              $dates[] = get_string('alwaysavailable', 'quiz');
 557          }
 558          $explanation = implode(', ', $dates);
 559  
 560          // Brief summary on the page.
 561          if ($timenow < $quiz->timeopen) {
 562              $currentstatus = get_string('quizisclosedwillopen', 'quiz',
 563                      userdate($quiz->timeopen, get_string('strftimedatetimeshort', 'langconfig')));
 564          } else if ($quiz->timeclose && $timenow <= $quiz->timeclose) {
 565              $currentstatus = get_string('quizisopenwillclose', 'quiz',
 566                      userdate($quiz->timeclose, get_string('strftimedatetimeshort', 'langconfig')));
 567          } else if ($quiz->timeclose && $timenow > $quiz->timeclose) {
 568              $currentstatus = get_string('quizisclosed', 'quiz');
 569          } else {
 570              $currentstatus = get_string('quizisopen', 'quiz');
 571          }
 572  
 573          return array($currentstatus, $explanation);
 574      }
 575  
 576      /**
 577       * Set up this class with the structure for a given quiz.
 578       * @param \stdClass $quiz the quiz settings.
 579       */
 580      public function populate_structure($quiz) {
 581          global $DB;
 582  
 583          $slots = $DB->get_records_sql("
 584                  SELECT slot.id AS slotid, slot.slot, slot.questionid, slot.page, slot.maxmark,
 585                          slot.requireprevious, q.*, qc.contextid
 586                    FROM {quiz_slots} slot
 587                    LEFT JOIN {question} q ON q.id = slot.questionid
 588                    LEFT JOIN {question_categories} qc ON qc.id = q.category
 589                   WHERE slot.quizid = ?
 590                ORDER BY slot.slot", array($quiz->id));
 591  
 592          $slots = $this->populate_missing_questions($slots);
 593  
 594          $this->questions = array();
 595          $this->slots = array();
 596          $this->slotsinorder = array();
 597          foreach ($slots as $slotdata) {
 598              $this->questions[$slotdata->questionid] = $slotdata;
 599  
 600              $slot = new \stdClass();
 601              $slot->id = $slotdata->slotid;
 602              $slot->slot = $slotdata->slot;
 603              $slot->quizid = $quiz->id;
 604              $slot->page = $slotdata->page;
 605              $slot->questionid = $slotdata->questionid;
 606              $slot->maxmark = $slotdata->maxmark;
 607              $slot->requireprevious = $slotdata->requireprevious;
 608  
 609              $this->slots[$slot->id] = $slot;
 610              $this->slotsinorder[$slot->slot] = $slot;
 611          }
 612  
 613          // Get quiz sections in ascending order of the firstslot.
 614          $this->sections = $DB->get_records('quiz_sections', array('quizid' => $quiz->id), 'firstslot ASC');
 615          $this->populate_slots_with_sections();
 616          $this->populate_question_numbers();
 617      }
 618  
 619      /**
 620       * Used by populate. Make up fake data for any missing questions.
 621       * @param \stdClass[] $slots the data about the slots and questions in the quiz.
 622       * @return \stdClass[] updated $slots array.
 623       */
 624      protected function populate_missing_questions($slots) {
 625          // Address missing question types.
 626          foreach ($slots as $slot) {
 627              if ($slot->qtype === null) {
 628                  // If the questiontype is missing change the question type.
 629                  $slot->id = $slot->questionid;
 630                  $slot->category = 0;
 631                  $slot->qtype = 'missingtype';
 632                  $slot->name = get_string('missingquestion', 'quiz');
 633                  $slot->slot = $slot->slot;
 634                  $slot->maxmark = 0;
 635                  $slot->requireprevious = 0;
 636                  $slot->questiontext = ' ';
 637                  $slot->questiontextformat = FORMAT_HTML;
 638                  $slot->length = 1;
 639  
 640              } else if (!\question_bank::qtype_exists($slot->qtype)) {
 641                  $slot->qtype = 'missingtype';
 642              }
 643          }
 644  
 645          return $slots;
 646      }
 647  
 648      /**
 649       * Fill in the section ids for each slot.
 650       */
 651      public function populate_slots_with_sections() {
 652          $sections = array_values($this->sections);
 653          foreach ($sections as $i => $section) {
 654              if (isset($sections[$i + 1])) {
 655                  $section->lastslot = $sections[$i + 1]->firstslot - 1;
 656              } else {
 657                  $section->lastslot = count($this->slotsinorder);
 658              }
 659              for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) {
 660                  $this->slotsinorder[$slot]->section = $section;
 661              }
 662          }
 663      }
 664  
 665      /**
 666       * Number the questions.
 667       */
 668      protected function populate_question_numbers() {
 669          $number = 1;
 670          foreach ($this->slots as $slot) {
 671              if ($this->questions[$slot->questionid]->length == 0) {
 672                  $slot->displayednumber = get_string('infoshort', 'quiz');
 673              } else {
 674                  $slot->displayednumber = $number;
 675                  $number += 1;
 676              }
 677          }
 678      }
 679  
 680      /**
 681       * Move a slot from its current location to a new location.
 682       *
 683       * After callig this method, this class will be in an invalid state, and
 684       * should be discarded if you want to manipulate the structure further.
 685       *
 686       * @param int $idmove id of slot to be moved
 687       * @param int $idmoveafter id of slot to come before slot being moved
 688       * @param int $page new page number of slot being moved
 689       * @param bool $insection if the question is moving to a place where a new
 690       *      section starts, include it in that section.
 691       * @return void
 692       */
 693      public function move_slot($idmove, $idmoveafter, $page) {
 694          global $DB;
 695  
 696          $this->check_can_be_edited();
 697  
 698          $movingslot = $this->slots[$idmove];
 699          if (empty($movingslot)) {
 700              throw new \moodle_exception('Bad slot ID ' . $idmove);
 701          }
 702          $movingslotnumber = (int) $movingslot->slot;
 703  
 704          // Empty target slot means move slot to first.
 705          if (empty($idmoveafter)) {
 706              $moveafterslotnumber = 0;
 707          } else {
 708              $moveafterslotnumber = (int) $this->slots[$idmoveafter]->slot;
 709          }
 710  
 711          // If the action came in as moving a slot to itself, normalise this to
 712          // moving the slot to after the previous slot.
 713          if ($moveafterslotnumber == $movingslotnumber) {
 714              $moveafterslotnumber = $moveafterslotnumber - 1;
 715          }
 716  
 717          $followingslotnumber = $moveafterslotnumber + 1;
 718          if ($followingslotnumber == $movingslotnumber) {
 719              $followingslotnumber += 1;
 720          }
 721  
 722          // Check the target page number is OK.
 723          if ($page == 0) {
 724              $page = 1;
 725          }
 726          if (($moveafterslotnumber > 0 && $page < $this->get_page_number_for_slot($moveafterslotnumber)) ||
 727                  $page < 1) {
 728              throw new \coding_exception('The target page number is too small.');
 729          } else if (!$this->is_last_slot_in_quiz($moveafterslotnumber) &&
 730                  $page > $this->get_page_number_for_slot($followingslotnumber)) {
 731              throw new \coding_exception('The target page number is too large.');
 732          }
 733  
 734          // Work out how things are being moved.
 735          $slotreorder = array();
 736          if ($moveafterslotnumber > $movingslotnumber) {
 737              // Moving down.
 738              $slotreorder[$movingslotnumber] = $moveafterslotnumber;
 739              for ($i = $movingslotnumber; $i < $moveafterslotnumber; $i++) {
 740                  $slotreorder[$i + 1] = $i;
 741              }
 742  
 743              $headingmoveafter = $movingslotnumber;
 744              if ($this->is_last_slot_in_quiz($moveafterslotnumber) ||
 745                      $page == $this->get_page_number_for_slot($moveafterslotnumber + 1)) {
 746                  // We are moving to the start of a section, so that heading needs
 747                  // to be included in the ones that move up.
 748                  $headingmovebefore = $moveafterslotnumber + 1;
 749              } else {
 750                  $headingmovebefore = $moveafterslotnumber;
 751              }
 752              $headingmovedirection = -1;
 753  
 754          } else if ($moveafterslotnumber < $movingslotnumber - 1) {
 755              // Moving up.
 756              $slotreorder[$movingslotnumber] = $moveafterslotnumber + 1;
 757              for ($i = $moveafterslotnumber + 1; $i < $movingslotnumber; $i++) {
 758                  $slotreorder[$i] = $i + 1;
 759              }
 760  
 761              if ($page == $this->get_page_number_for_slot($moveafterslotnumber + 1)) {
 762                  // Moving to the start of a section, don't move that section.
 763                  $headingmoveafter = $moveafterslotnumber + 1;
 764              } else {
 765                  // Moving tot the end of the previous section, so move the heading down too.
 766                  $headingmoveafter = $moveafterslotnumber;
 767              }
 768              $headingmovebefore = $movingslotnumber + 1;
 769              $headingmovedirection = 1;
 770          } else {
 771              // Staying in the same place, but possibly changing page/section.
 772              if ($page > $movingslot->page) {
 773                  $headingmoveafter = $movingslotnumber;
 774                  $headingmovebefore = $movingslotnumber + 2;
 775                  $headingmovedirection = -1;
 776              } else if ($page < $movingslot->page) {
 777                  $headingmoveafter = $movingslotnumber - 1;
 778                  $headingmovebefore = $movingslotnumber + 1;
 779                  $headingmovedirection = 1;
 780              } else {
 781                  return; // Nothing to do.
 782              }
 783          }
 784  
 785          if ($this->is_only_slot_in_section($movingslotnumber)) {
 786              throw new \coding_exception('You cannot remove the last slot in a section.');
 787          }
 788  
 789          $trans = $DB->start_delegated_transaction();
 790  
 791          // Slot has moved record new order.
 792          if ($slotreorder) {
 793              update_field_with_unique_index('quiz_slots', 'slot', $slotreorder,
 794                      array('quizid' => $this->get_quizid()));
 795          }
 796  
 797          // Page has changed. Record it.
 798          if ($movingslot->page != $page) {
 799              $DB->set_field('quiz_slots', 'page', $page,
 800                      array('id' => $movingslot->id));
 801          }
 802  
 803          // Update section fist slots.
 804          $DB->execute("
 805                  UPDATE {quiz_sections}
 806                     SET firstslot = firstslot + ?
 807                   WHERE quizid = ?
 808                     AND firstslot > ?
 809                     AND firstslot < ?
 810                  ", array($headingmovedirection, $this->get_quizid(),
 811                          $headingmoveafter, $headingmovebefore));
 812  
 813          // If any pages are now empty, remove them.
 814          $emptypages = $DB->get_fieldset_sql("
 815                  SELECT DISTINCT page - 1
 816                    FROM {quiz_slots} slot
 817                   WHERE quizid = ?
 818                     AND page > 1
 819                     AND NOT EXISTS (SELECT 1 FROM {quiz_slots} WHERE quizid = ? AND page = slot.page - 1)
 820                ORDER BY page - 1 DESC
 821                  ", array($this->get_quizid(), $this->get_quizid()));
 822  
 823          foreach ($emptypages as $page) {
 824              $DB->execute("
 825                      UPDATE {quiz_slots}
 826                         SET page = page - 1
 827                       WHERE quizid = ?
 828                         AND page > ?
 829                      ", array($this->get_quizid(), $page));
 830          }
 831  
 832          $trans->allow_commit();
 833      }
 834  
 835      /**
 836       * Refresh page numbering of quiz slots.
 837       * @param \stdClass[] $slots (optional) array of slot objects.
 838       * @return \stdClass[] array of slot objects.
 839       */
 840      public function refresh_page_numbers($slots = array()) {
 841          global $DB;
 842          // Get slots ordered by page then slot.
 843          if (!count($slots)) {
 844              $slots = $DB->get_records('quiz_slots', array('quizid' => $this->get_quizid()), 'slot, page');
 845          }
 846  
 847          // Loop slots. Start Page number at 1 and increment as required.
 848          $pagenumbers = array('new' => 0, 'old' => 0);
 849  
 850          foreach ($slots as $slot) {
 851              if ($slot->page !== $pagenumbers['old']) {
 852                  $pagenumbers['old'] = $slot->page;
 853                  ++$pagenumbers['new'];
 854              }
 855  
 856              if ($pagenumbers['new'] == $slot->page) {
 857                  continue;
 858              }
 859              $slot->page = $pagenumbers['new'];
 860          }
 861  
 862          return $slots;
 863      }
 864  
 865      /**
 866       * Refresh page numbering of quiz slots and save to the database.
 867       * @param \stdClass $quiz the quiz object.
 868       * @return \stdClass[] array of slot objects.
 869       */
 870      public function refresh_page_numbers_and_update_db() {
 871          global $DB;
 872          $this->check_can_be_edited();
 873  
 874          $slots = $this->refresh_page_numbers();
 875  
 876          // Record new page order.
 877          foreach ($slots as $slot) {
 878              $DB->set_field('quiz_slots', 'page', $slot->page,
 879                      array('id' => $slot->id));
 880          }
 881  
 882          return $slots;
 883      }
 884  
 885      /**
 886       * Remove a slot from a quiz
 887       * @param int $slotnumber The number of the slot to be deleted.
 888       */
 889      public function remove_slot($slotnumber) {
 890          global $DB;
 891  
 892          $this->check_can_be_edited();
 893  
 894          if ($this->is_only_slot_in_section($slotnumber) && $this->get_section_count() > 1) {
 895              throw new \coding_exception('You cannot remove the last slot in a section.');
 896          }
 897  
 898          $slot = $DB->get_record('quiz_slots', array('quizid' => $this->get_quizid(), 'slot' => $slotnumber));
 899          if (!$slot) {
 900              return;
 901          }
 902          $maxslot = $DB->get_field_sql('SELECT MAX(slot) FROM {quiz_slots} WHERE quizid = ?', array($this->get_quizid()));
 903  
 904          $trans = $DB->start_delegated_transaction();
 905          $DB->delete_records('quiz_slots', array('id' => $slot->id));
 906          for ($i = $slot->slot + 1; $i <= $maxslot; $i++) {
 907              $DB->set_field('quiz_slots', 'slot', $i - 1,
 908                      array('quizid' => $this->get_quizid(), 'slot' => $i));
 909          }
 910  
 911          $qtype = $DB->get_field('question', 'qtype', array('id' => $slot->questionid));
 912          if ($qtype === 'random') {
 913              // This function automatically checks if the question is in use, and won't delete if it is.
 914              question_delete_question($slot->questionid);
 915          }
 916  
 917          $DB->execute("
 918                  UPDATE {quiz_sections}
 919                     SET firstslot = firstslot - 1
 920                   WHERE quizid = ?
 921                     AND firstslot > ?
 922                  ", array($this->get_quizid(), $slotnumber));
 923          unset($this->questions[$slot->questionid]);
 924  
 925          $this->refresh_page_numbers_and_update_db();
 926  
 927          $trans->allow_commit();
 928      }
 929  
 930      /**
 931       * Change the max mark for a slot.
 932       *
 933       * Saves changes to the question grades in the quiz_slots table and any
 934       * corresponding question_attempts.
 935       * It does not update 'sumgrades' in the quiz table.
 936       *
 937       * @param \stdClass $slot row from the quiz_slots table.
 938       * @param float $maxmark the new maxmark.
 939       * @return bool true if the new grade is different from the old one.
 940       */
 941      public function update_slot_maxmark($slot, $maxmark) {
 942          global $DB;
 943  
 944          if (abs($maxmark - $slot->maxmark) < 1e-7) {
 945              // Grade has not changed. Nothing to do.
 946              return false;
 947          }
 948  
 949          $trans = $DB->start_delegated_transaction();
 950          $slot->maxmark = $maxmark;
 951          $DB->update_record('quiz_slots', $slot);
 952          \question_engine::set_max_mark_in_attempts(new \qubaids_for_quiz($slot->quizid),
 953                  $slot->slot, $maxmark);
 954          $trans->allow_commit();
 955  
 956          return true;
 957      }
 958  
 959      /**
 960       * Set whether the question in a particular slot requires the previous one.
 961       * @param int $slotid id of slot.
 962       * @param bool $requireprevious if true, set this question to require the previous one.
 963       */
 964      public function update_question_dependency($slotid, $requireprevious) {
 965          global $DB;
 966          $DB->set_field('quiz_slots', 'requireprevious', $requireprevious, array('id' => $slotid));
 967      }
 968  
 969      /**
 970       * Add/Remove a pagebreak.
 971       *
 972       * Saves changes to the slot page relationship in the quiz_slots table and reorders the paging
 973       * for subsequent slots.
 974       *
 975       * @param int $slotid id of slot.
 976       * @param int $type repaginate::LINK or repaginate::UNLINK.
 977       * @return \stdClass[] array of slot objects.
 978       */
 979      public function update_page_break($slotid, $type) {
 980          global $DB;
 981  
 982          $this->check_can_be_edited();
 983  
 984          $quizslots = $DB->get_records('quiz_slots', array('quizid' => $this->get_quizid()), 'slot');
 985          $repaginate = new \mod_quiz\repaginate($this->get_quizid(), $quizslots);
 986          $repaginate->repaginate_slots($quizslots[$slotid]->slot, $type);
 987          $slots = $this->refresh_page_numbers_and_update_db();
 988  
 989          return $slots;
 990      }
 991  
 992      /**
 993       * Add a section heading on a given page and return the sectionid
 994       * @param int $pagenumber the number of the page where the section heading begins.
 995       * @param string $heading the heading to add.
 996       */
 997      public function add_section_heading($pagenumber, $heading = 'Section heading ...') {
 998          global $DB;
 999          $section = new \stdClass();
1000          $section->heading = $heading;
1001          $section->quizid = $this->get_quizid();
1002          $slotsonpage = $DB->get_records('quiz_slots', array('quizid' => $this->get_quizid(), 'page' => $pagenumber), 'slot DESC');
1003          $section->firstslot = end($slotsonpage)->slot;
1004          $section->shufflequestions = 0;
1005          return $DB->insert_record('quiz_sections', $section);
1006      }
1007  
1008      /**
1009       * Change the heading for a section.
1010       * @param int $id the id of the section to change.
1011       * @param string $newheading the new heading for this section.
1012       */
1013      public function set_section_heading($id, $newheading) {
1014          global $DB;
1015          $section = $DB->get_record('quiz_sections', array('id' => $id), '*', MUST_EXIST);
1016          $section->heading = $newheading;
1017          $DB->update_record('quiz_sections', $section);
1018      }
1019  
1020      /**
1021       * Change the shuffle setting for a section.
1022       * @param int $id the id of the section to change.
1023       * @param bool $shuffle whether this section should be shuffled.
1024       */
1025      public function set_section_shuffle($id, $shuffle) {
1026          global $DB;
1027          $section = $DB->get_record('quiz_sections', array('id' => $id), '*', MUST_EXIST);
1028          $section->shufflequestions = $shuffle;
1029          $DB->update_record('quiz_sections', $section);
1030      }
1031  
1032      /**
1033       * Remove the section heading with the given id
1034       * @param int $sectionid the section to remove.
1035       */
1036      public function remove_section_heading($sectionid) {
1037          global $DB;
1038          $section = $DB->get_record('quiz_sections', array('id' => $sectionid), '*', MUST_EXIST);
1039          if ($section->firstslot == 1) {
1040              throw new \coding_exception('Cannot remove the first section in a quiz.');
1041          }
1042          $DB->delete_records('quiz_sections', array('id' => $sectionid));
1043      }
1044  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1