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