Search moodle.org's
Developer Documentation

See Release Notes

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