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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body