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 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 cm_info;
  20  use coding_exception;
  21  use context;
  22  use context_module;
  23  use core_question\local\bank\question_version_status;
  24  use mod_quiz\question\bank\qbank_helper;
  25  use mod_quiz\question\display_options;
  26  use moodle_exception;
  27  use moodle_url;
  28  use question_bank;
  29  use stdClass;
  30  
  31  /**
  32   * A class encapsulating the settings for a quiz.
  33   *
  34   * When this class is initialised, it may have the settings adjusted to account
  35   * for the overrides for a particular user. See the create methods.
  36   *
  37   * Initially, it only loads a minimal amount of information about each question - loading
  38   * extra information only when necessary or when asked. The class tracks which questions
  39   * are loaded.
  40   *
  41   * @package   mod_quiz
  42   * @copyright 2008 Tim Hunt
  43   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  class quiz_settings {
  46      /** @var stdClass the course settings from the database. */
  47      protected $course;
  48      /** @var cm_info the course_module settings from the database. */
  49      protected $cm;
  50      /** @var stdClass the quiz settings from the database. */
  51      protected $quiz;
  52      /** @var context the quiz context. */
  53      protected $context;
  54  
  55      /**
  56       * @var stdClass[] of questions augmented with slot information. For non-random
  57       *     questions, the array key is question id. For random quesions it is 's' . $slotid.
  58       *     probalby best to use ->questionid field of the object instead.
  59       */
  60      protected $questions = null;
  61      /** @var stdClass[] of quiz_section rows. */
  62      protected $sections = null;
  63      /** @var access_manager the access manager for this quiz. */
  64      protected $accessmanager = null;
  65      /** @var bool whether the current user has capability mod/quiz:preview. */
  66      protected $ispreviewuser = null;
  67  
  68      // Constructor =============================================================.
  69  
  70      /**
  71       * Constructor, assuming we already have the necessary data loaded.
  72       *
  73       * @param stdClass $quiz the row from the quiz table.
  74       * @param stdClass $cm the course_module object for this quiz.
  75       * @param stdClass $course the row from the course table for the course we belong to.
  76       * @param bool $getcontext intended for testing - stops the constructor getting the context.
  77       */
  78      public function __construct($quiz, $cm, $course, $getcontext = true) {
  79          $this->quiz = $quiz;
  80          $this->cm = $cm;
  81          $this->quiz->cmid = $this->cm->id;
  82          $this->course = $course;
  83          if ($getcontext && !empty($cm->id)) {
  84              $this->context = context_module::instance($cm->id);
  85          }
  86      }
  87  
  88      /**
  89       * Helper used by the other factory methods.
  90       *
  91       * @param stdClass $quiz
  92       * @param cm_info $cm
  93       * @param stdClass $course
  94       * @param int|null $userid the the userid (optional). If passed, relevant overrides are applied.
  95       * @return quiz_settings the new quiz settings object.
  96       */
  97      protected static function create_helper(stdClass $quiz, cm_info $cm, stdClass $course, ?int $userid): self {
  98          // Update quiz with override information.
  99          if ($userid) {
 100              $quiz = quiz_update_effective_access($quiz, $userid);
 101          }
 102  
 103          return new quiz_settings($quiz, $cm, $course);
 104      }
 105  
 106      /**
 107       * Static function to create a new quiz settings object from a quiz id, for a specific user.
 108       *
 109       * @param int $quizid the quiz id.
 110       * @param int|null $userid the the userid (optional). If passed, relevant overrides are applied.
 111       * @return quiz_settings the new quiz settings object.
 112       */
 113      public static function create(int $quizid, int $userid = null): self {
 114          $quiz = access_manager::load_quiz_and_settings($quizid);
 115          [$course, $cm] = get_course_and_cm_from_instance($quiz, 'quiz');
 116  
 117          return self::create_helper($quiz, $cm, $course, $userid);
 118      }
 119  
 120      /**
 121       * Static function to create a new quiz settings object from a cmid, for a specific user.
 122       *
 123       * @param int $cmid the course-module id.
 124       * @param int|null $userid the the userid (optional). If passed, relevant overrides are applied.
 125       * @return quiz_settings the new quiz settings object.
 126       */
 127      public static function create_for_cmid(int $cmid, int $userid = null): self {
 128          [$course, $cm] = get_course_and_cm_from_cmid($cmid, 'quiz');
 129          $quiz = access_manager::load_quiz_and_settings($cm->instance);
 130  
 131          return self::create_helper($quiz, $cm, $course, $userid);
 132      }
 133  
 134      /**
 135       * Create a {@see quiz_attempt} for an attempt at this quiz.
 136       *
 137       * @param stdClass $attemptdata row from the quiz_attempts table.
 138       * @return quiz_attempt the new quiz_attempt object.
 139       */
 140      public function create_attempt_object($attemptdata) {
 141          return new quiz_attempt($attemptdata, $this->quiz, $this->cm, $this->course);
 142      }
 143  
 144      // Functions for loading more data =========================================.
 145  
 146      /**
 147       * Load just basic information about all the questions in this quiz.
 148       */
 149      public function preload_questions() {
 150          $slots = qbank_helper::get_question_structure($this->quiz->id, $this->context);
 151          $this->questions = [];
 152          foreach ($slots as $slot) {
 153              $this->questions[$slot->questionid] = $slot;
 154          }
 155      }
 156  
 157      /**
 158       * Fully load some or all of the questions for this quiz. You must call
 159       * {@see preload_questions()} first.
 160       *
 161       * @param array|null $deprecated no longer supported (it was not used).
 162       */
 163      public function load_questions($deprecated = null) {
 164          if ($deprecated !== null) {
 165              debugging('The argument to quiz::load_questions is no longer supported. ' .
 166                      'All questions are always loaded.', DEBUG_DEVELOPER);
 167          }
 168          if ($this->questions === null) {
 169              throw new coding_exception('You must call preload_questions before calling load_questions.');
 170          }
 171  
 172          $questionstoprocess = [];
 173          foreach ($this->questions as $question) {
 174              if (is_number($question->questionid)) {
 175                  $question->id = $question->questionid;
 176                  $questionstoprocess[$question->questionid] = $question;
 177              }
 178          }
 179          get_question_options($questionstoprocess);
 180      }
 181  
 182      /**
 183       * Get an instance of the {@see \mod_quiz\structure} class for this quiz.
 184       *
 185       * @return structure describes the questions in the quiz.
 186       */
 187      public function get_structure() {
 188          return structure::create_for_quiz($this);
 189      }
 190  
 191      // Simple getters ==========================================================.
 192  
 193      /**
 194       * Get the id of the course this quiz belongs to.
 195       *
 196       * @return int the course id.
 197       */
 198      public function get_courseid() {
 199          return $this->course->id;
 200      }
 201  
 202      /**
 203       * Get the course settings object that this quiz belongs to.
 204       *
 205       * @return stdClass the row of the course table.
 206       */
 207      public function get_course() {
 208          return $this->course;
 209      }
 210  
 211      /**
 212       * Get this quiz's id (in the quiz table).
 213       *
 214       * @return int the quiz id.
 215       */
 216      public function get_quizid() {
 217          return $this->quiz->id;
 218      }
 219  
 220      /**
 221       * Get the quiz settings object.
 222       *
 223       * @return stdClass the row of the quiz table.
 224       */
 225      public function get_quiz() {
 226          return $this->quiz;
 227      }
 228  
 229      /**
 230       * Get the quiz name.
 231       *
 232       * @return string the name of this quiz.
 233       */
 234      public function get_quiz_name() {
 235          return $this->quiz->name;
 236      }
 237  
 238      /**
 239       * Get the navigation method in use.
 240       *
 241       * @return int QUIZ_NAVMETHOD_FREE or QUIZ_NAVMETHOD_SEQ.
 242       */
 243      public function get_navigation_method() {
 244          return $this->quiz->navmethod;
 245      }
 246  
 247      /**
 248       * How many attepts is the user allowed at this quiz?
 249       *
 250       * @return int the number of attempts allowed at this quiz (0 = infinite).
 251       */
 252      public function get_num_attempts_allowed() {
 253          return $this->quiz->attempts;
 254      }
 255  
 256      /**
 257       * Get the course-module id for this quiz.
 258       *
 259       * @return int the course_module id.
 260       */
 261      public function get_cmid() {
 262          return $this->cm->id;
 263      }
 264  
 265      /**
 266       * Get the course-module object for this quiz.
 267       *
 268       * @return cm_info the course_module object.
 269       */
 270      public function get_cm() {
 271          return $this->cm;
 272      }
 273  
 274      /**
 275       * Get the quiz context.
 276       *
 277       * @return context_module the module context for this quiz.
 278       */
 279      public function get_context() {
 280          return $this->context;
 281      }
 282  
 283      /**
 284       * Is the current user is someone who previews the quiz, rather than attempting it?
 285       *
 286       * @return bool true user is a preview user. False, if they can do real attempts.
 287       */
 288      public function is_preview_user() {
 289          if (is_null($this->ispreviewuser)) {
 290              $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context);
 291          }
 292          return $this->ispreviewuser;
 293      }
 294  
 295      /**
 296       * Checks user enrollment in the current course.
 297       *
 298       * @param int $userid the id of the user to check.
 299       * @return bool whether the user is enrolled.
 300       */
 301      public function is_participant($userid) {
 302          return is_enrolled($this->get_context(), $userid, 'mod/quiz:attempt', $this->show_only_active_users());
 303      }
 304  
 305      /**
 306       * Check is only active users in course should be shown.
 307       *
 308       * @return bool true if only active users should be shown.
 309       */
 310      public function show_only_active_users() {
 311          return !has_capability('moodle/course:viewsuspendedusers', $this->get_context());
 312      }
 313  
 314      /**
 315       * Have any questions been added to this quiz yet?
 316       *
 317       * @return bool whether any questions have been added to this quiz.
 318       */
 319      public function has_questions() {
 320          if ($this->questions === null) {
 321              $this->preload_questions();
 322          }
 323          return !empty($this->questions);
 324      }
 325  
 326      /**
 327       * Get a particular question in this quiz, by its id.
 328       *
 329       * @param int $id the question id.
 330       * @return stdClass the question object with that id.
 331       */
 332      public function get_question($id) {
 333          return $this->questions[$id];
 334      }
 335  
 336      /**
 337       * Get some of the question in this quiz.
 338       *
 339       * @param array|null $questionids question ids of the questions to load. null for all.
 340       * @param bool $requirequestionfullyloaded Whether to require that a particular question is fully loaded.
 341       * @return stdClass[] the question data objects.
 342       */
 343      public function get_questions(?array $questionids = null, bool $requirequestionfullyloaded = true) {
 344          if (is_null($questionids)) {
 345              $questionids = array_keys($this->questions);
 346          }
 347          $questions = [];
 348          foreach ($questionids as $id) {
 349              if (!array_key_exists($id, $this->questions)) {
 350                  throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url());
 351              }
 352              $questions[$id] = $this->questions[$id];
 353              if ($requirequestionfullyloaded) {
 354                  $this->ensure_question_loaded($id);
 355              }
 356          }
 357          return $questions;
 358      }
 359  
 360      /**
 361       * Get all the sections in this quiz.
 362       *
 363       * @return array 0, 1, 2, ... => quiz_sections row from the database.
 364       */
 365      public function get_sections() {
 366          global $DB;
 367          if ($this->sections === null) {
 368              $this->sections = array_values($DB->get_records('quiz_sections',
 369                      ['quizid' => $this->get_quizid()], 'firstslot'));
 370          }
 371          return $this->sections;
 372      }
 373  
 374      /**
 375       * Return access_manager and instance of the access_manager class
 376       * for this quiz at this time.
 377       *
 378       * @param int $timenow the current time as a unix timestamp.
 379       * @return access_manager an instance of the access_manager class
 380       *      for this quiz at this time.
 381       */
 382      public function get_access_manager($timenow) {
 383          if (is_null($this->accessmanager)) {
 384              $this->accessmanager = new access_manager($this, $timenow,
 385                      has_capability('mod/quiz:ignoretimelimits', $this->context, null, false));
 386          }
 387          return $this->accessmanager;
 388      }
 389  
 390      /**
 391       * Return the grade_calculator object for this quiz.
 392       *
 393       * @return grade_calculator
 394       */
 395      public function get_grade_calculator(): grade_calculator {
 396          return grade_calculator::create($this);
 397      }
 398  
 399      /**
 400       * Wrapper round the has_capability funciton that automatically passes in the quiz context.
 401       *
 402       * @param string $capability the name of the capability to check. For example mod/quiz:view.
 403       * @param int|null $userid A user id. By default (null) checks the permissions of the current user.
 404       * @param bool $doanything If false, ignore effect of admin role assignment.
 405       * @return boolean true if the user has this capability. Otherwise false.
 406       */
 407      public function has_capability($capability, $userid = null, $doanything = true) {
 408          return has_capability($capability, $this->context, $userid, $doanything);
 409      }
 410  
 411      /**
 412       * Wrapper round the require_capability function that automatically passes in the quiz context.
 413       *
 414       * @param string $capability the name of the capability to check. For example mod/quiz:view.
 415       * @param int|null $userid A user id. By default (null) checks the permissions of the current user.
 416       * @param bool $doanything If false, ignore effect of admin role assignment.
 417       */
 418      public function require_capability($capability, $userid = null, $doanything = true) {
 419          require_capability($capability, $this->context, $userid, $doanything);
 420      }
 421  
 422      // URLs related to this attempt ============================================.
 423  
 424      /**
 425       * Get the URL of this quiz's view.php page.
 426       *
 427       * @return moodle_url the URL of this quiz's view page.
 428       */
 429      public function view_url() {
 430          return new moodle_url('/mod/quiz/view.php', ['id' => $this->cm->id]);
 431      }
 432  
 433      /**
 434       * Get the URL of this quiz's edit questions page.
 435       *
 436       * @return moodle_url the URL of this quiz's edit page.
 437       */
 438      public function edit_url() {
 439          return new moodle_url('/mod/quiz/edit.php', ['cmid' => $this->cm->id]);
 440      }
 441  
 442      /**
 443       * Get the URL of a particular page within an attempt.
 444       *
 445       * @param int $attemptid the id of an attempt.
 446       * @param int $page optional page number to go to in the attempt.
 447       * @return moodle_url the URL of that attempt.
 448       */
 449      public function attempt_url($attemptid, $page = 0) {
 450          $params = ['attempt' => $attemptid, 'cmid' => $this->get_cmid()];
 451          if ($page) {
 452              $params['page'] = $page;
 453          }
 454          return new moodle_url('/mod/quiz/attempt.php', $params);
 455      }
 456  
 457      /**
 458       * Get the URL to start/continue an attempt.
 459       *
 460       * @param int $page page in the attempt to start on (optional).
 461       * @return moodle_url the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
 462       */
 463      public function start_attempt_url($page = 0) {
 464          $params = ['cmid' => $this->cm->id, 'sesskey' => sesskey()];
 465          if ($page) {
 466              $params['page'] = $page;
 467          }
 468          return new moodle_url('/mod/quiz/startattempt.php', $params);
 469      }
 470  
 471      /**
 472       * Get the URL to review a particular quiz attempt.
 473       *
 474       * @param int $attemptid the id of an attempt.
 475       * @return string the URL of the review of that attempt.
 476       */
 477      public function review_url($attemptid) {
 478          return new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptid, 'cmid' => $this->get_cmid()]);
 479      }
 480  
 481      /**
 482       * Get the URL for the summary page for a particular attempt.
 483       *
 484       * @param int $attemptid the id of an attempt.
 485       * @return string the URL of the review of that attempt.
 486       */
 487      public function summary_url($attemptid) {
 488          return new moodle_url('/mod/quiz/summary.php', ['attempt' => $attemptid, 'cmid' => $this->get_cmid()]);
 489      }
 490  
 491      // Bits of content =========================================================.
 492  
 493      /**
 494       * If $reviewoptions->attempt is false, meaning that students can't review this
 495       * attempt at the moment, return an appropriate string explaining why.
 496       *
 497       * @param int $when One of the display_options::DURING,
 498       *      IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
 499       * @param bool $short if true, return a shorter string.
 500       * @return string an appropraite message.
 501       */
 502      public function cannot_review_message($when, $short = false) {
 503  
 504          if ($short) {
 505              $langstrsuffix = 'short';
 506              $dateformat = get_string('strftimedatetimeshort', 'langconfig');
 507          } else {
 508              $langstrsuffix = '';
 509              $dateformat = '';
 510          }
 511  
 512          if ($when == display_options::DURING ||
 513                  $when == display_options::IMMEDIATELY_AFTER) {
 514              return '';
 515          } else {
 516              if ($when == display_options::LATER_WHILE_OPEN && $this->quiz->timeclose &&
 517                      $this->quiz->reviewattempt & display_options::AFTER_CLOSE) {
 518                  return get_string('noreviewuntil' . $langstrsuffix, 'quiz',
 519                          userdate($this->quiz->timeclose, $dateformat));
 520              } else {
 521                  return get_string('noreview' . $langstrsuffix, 'quiz');
 522              }
 523          }
 524      }
 525  
 526      /**
 527       * Probably not used any more, but left for backwards compatibility.
 528       *
 529       * @param string $title the name of this particular quiz page.
 530       * @return string always returns ''.
 531       */
 532      public function navigation($title) {
 533          global $PAGE;
 534          $PAGE->navbar->add($title);
 535          return '';
 536      }
 537  
 538      // Private methods =========================================================.
 539  
 540      /**
 541       * Check that the definition of a particular question is loaded, and if not throw an exception.
 542       *
 543       * @param int $id a question id.
 544       */
 545      protected function ensure_question_loaded($id) {
 546          if (isset($this->questions[$id]->_partiallyloaded)) {
 547              throw new moodle_exception('questionnotloaded', 'quiz', $this->view_url(), $id);
 548          }
 549      }
 550  
 551      /**
 552       * Return all the question types used in this quiz.
 553       *
 554       * @param boolean $includepotential if the quiz include random questions,
 555       *      setting this flag to true will make the function to return all the
 556       *      possible question types in the random questions category.
 557       * @return array a sorted array including the different question types.
 558       * @since  Moodle 3.1
 559       */
 560      public function get_all_question_types_used($includepotential = false) {
 561          $questiontypes = [];
 562  
 563          // To control if we need to look in categories for questions.
 564          $qcategories = [];
 565  
 566          foreach ($this->get_questions(null, false) as $questiondata) {
 567              if ($questiondata->status == question_version_status::QUESTION_STATUS_DRAFT) {
 568                  // Skip questions where all versions are draft.
 569                  continue;
 570              }
 571              if ($questiondata->qtype === 'random' && $includepotential) {
 572                  $filtercondition = $questiondata->filtercondition;
 573                  if (!empty($filtercondition)) {
 574                      $filter = $filtercondition['filter'];
 575                      if (isset($filter['category'])) {
 576                          foreach ($filter['category']['values'] as $catid) {
 577                              $qcategories[$catid] = $filter['category']['filteroptions']['includesubcategories'];
 578                          }
 579                      }
 580                  }
 581              } else {
 582                  if (!in_array($questiondata->qtype, $questiontypes)) {
 583                      $questiontypes[] = $questiondata->qtype;
 584                  }
 585              }
 586          }
 587  
 588          if (!empty($qcategories)) {
 589              // We have to look for all the question types in these categories.
 590              $categoriestolook = [];
 591              foreach ($qcategories as $cat => $includesubcats) {
 592                  if ($includesubcats) {
 593                      $categoriestolook = array_merge($categoriestolook, question_categorylist($cat));
 594                  } else {
 595                      $categoriestolook[] = $cat;
 596                  }
 597              }
 598              $questiontypesincategories = question_bank::get_all_question_types_in_categories($categoriestolook);
 599              $questiontypes = array_merge($questiontypes, $questiontypesincategories);
 600          }
 601          $questiontypes = array_unique($questiontypes);
 602          sort($questiontypes);
 603  
 604          return $questiontypes;
 605      }
 606  }