[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
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 course module id of the quiz. 123 * @return int the course_modules.id for the quiz. 124 */ 125 public function get_cmid() { 126 return $this->quizobj->get_cmid(); 127 } 128 129 /** 130 * Get id of the quiz. 131 * @return int the quiz.id for the quiz. 132 */ 133 public function get_quizid() { 134 return $this->quizobj->get_quizid(); 135 } 136 137 /** 138 * Get the quiz object. 139 * @return \stdClass the quiz settings row from the database. 140 */ 141 public function get_quiz() { 142 return $this->quizobj->get_quiz(); 143 } 144 145 /** 146 * Whether the question in the quiz are shuffled for each attempt. 147 * @return bool true if the questions are shuffled. 148 */ 149 public function is_shuffled() { 150 return $this->quizobj->get_quiz()->shufflequestions; 151 } 152 153 /** 154 * Quizzes can only be repaginated if they have not been attempted, the 155 * questions are not shuffled, and there are two or more questions. 156 * @return bool whether this quiz can be repaginated. 157 */ 158 public function can_be_repaginated() { 159 return !$this->is_shuffled() && $this->can_be_edited() 160 && $this->get_question_count() >= 2; 161 } 162 163 /** 164 * Quizzes can only be edited if they have not been attempted. 165 * @return bool whether the quiz can be edited. 166 */ 167 public function can_be_edited() { 168 if ($this->canbeedited === null) { 169 $this->canbeedited = !quiz_has_attempts($this->quizobj->get_quizid()); 170 } 171 return $this->canbeedited; 172 } 173 174 /** 175 * This quiz can only be edited if they have not been attempted. 176 * Throw an exception if this is not the case. 177 */ 178 public function check_can_be_edited() { 179 if (!$this->can_be_edited()) { 180 $reportlink = quiz_attempt_summary_link_to_reports($this->get_quiz(), 181 $this->quizobj->get_cm(), $this->quizobj->get_context()); 182 throw new \moodle_exception('cannoteditafterattempts', 'quiz', 183 new \moodle_url('/mod/quiz/edit.php', array('cmid' => $this->get_cmid())), $reportlink); 184 } 185 } 186 187 /** 188 * How many questions are allowed per page in the quiz. 189 * This setting controls how frequently extra page-breaks should be inserted 190 * automatically when questions are added to the quiz. 191 * @return int the number of questions that should be on each page of the 192 * quiz by default. 193 */ 194 public function get_questions_per_page() { 195 return $this->quizobj->get_quiz()->questionsperpage; 196 } 197 198 /** 199 * Get quiz slots. 200 * @return \stdClass[] the slots in this quiz. 201 */ 202 public function get_slots() { 203 return $this->slots; 204 } 205 206 /** 207 * Is this slot the first one on its page? 208 * @param int $slotnumber the index of the slot in question. 209 * @return bool whether this slot the first one on its page. 210 */ 211 public function is_first_slot_on_page($slotnumber) { 212 if ($slotnumber == 1) { 213 return true; 214 } 215 return $this->slotsinorder[$slotnumber]->page != $this->slotsinorder[$slotnumber - 1]->page; 216 } 217 218 /** 219 * Is this slot the last one on its page? 220 * @param int $slotnumber the index of the slot in question. 221 * @return bool whether this slot the last one on its page. 222 */ 223 public function is_last_slot_on_page($slotnumber) { 224 if (!isset($this->slotsinorder[$slotnumber + 1])) { 225 return true; 226 } 227 return $this->slotsinorder[$slotnumber]->page != $this->slotsinorder[$slotnumber + 1]->page; 228 } 229 230 /** 231 * Is this slot the last one in the quiz? 232 * @param int $slotnumber the index of the slot in question. 233 * @return bool whether this slot the last one in the quiz. 234 */ 235 public function is_last_slot_in_quiz($slotnumber) { 236 end($this->slotsinorder); 237 return $slotnumber == key($this->slotsinorder); 238 } 239 240 /** 241 * Get the final slot in the quiz. 242 * @return \stdClass the quiz_slots for for the final slot in the quiz. 243 */ 244 public function get_last_slot() { 245 return end($this->slotsinorder); 246 } 247 248 /** 249 * Get a slot by it's id. Throws an exception if it is missing. 250 * @param int $slotid the slot id. 251 * @return \stdClass the requested quiz_slots row. 252 */ 253 public function get_slot_by_id($slotid) { 254 if (!array_key_exists($slotid, $this->slots)) { 255 throw new \coding_exception('The \'slotid\' could not be found.'); 256 } 257 return $this->slots[$slotid]; 258 } 259 260 /** 261 * Get all the questions in a section of the quiz. 262 * @param int $sectionid the section id. 263 * @return \stdClass[] of question/slot objects. 264 */ 265 public function get_questions_in_section($sectionid) { 266 $questions = array(); 267 foreach ($this->slotsinorder as $slot) { 268 if ($slot->sectionid == $sectionid) { 269 $questions[] = $this->questions[$slot->questionid]; 270 } 271 } 272 return $questions; 273 } 274 275 /** 276 * Get all the sections of the quiz. 277 * @return \stdClass[] the sections in this quiz. 278 */ 279 public function get_quiz_sections() { 280 return $this->sections; 281 } 282 283 /** 284 * Get any warnings to show at the top of the edit page. 285 * @return string[] array of strings. 286 */ 287 public function get_edit_page_warnings() { 288 $warnings = array(); 289 290 if (quiz_has_attempts($this->quizobj->get_quizid())) { 291 $reviewlink = quiz_attempt_summary_link_to_reports($this->quizobj->get_quiz(), 292 $this->quizobj->get_cm(), $this->quizobj->get_context()); 293 $warnings[] = get_string('cannoteditafterattempts', 'quiz', $reviewlink); 294 } 295 296 if ($this->is_shuffled()) { 297 $updateurl = new \moodle_url('/course/mod.php', 298 array('return' => 'true', 'update' => $this->quizobj->get_cmid(), 'sesskey' => sesskey())); 299 $updatelink = '<a href="'.$updateurl->out().'">' . get_string('updatethis', '', 300 get_string('modulename', 'quiz')) . '</a>'; 301 $warnings[] = get_string('shufflequestionsselected', 'quiz', $updatelink); 302 } 303 304 return $warnings; 305 } 306 307 /** 308 * Get the date information about the current state of the quiz. 309 * @return string[] array of two strings. First a short summary, then a longer 310 * explanation of the current state, e.g. for a tool-tip. 311 */ 312 public function get_dates_summary() { 313 $timenow = time(); 314 $quiz = $this->quizobj->get_quiz(); 315 316 // Exact open and close dates for the tool-tip. 317 $dates = array(); 318 if ($quiz->timeopen > 0) { 319 if ($timenow > $quiz->timeopen) { 320 $dates[] = get_string('quizopenedon', 'quiz', userdate($quiz->timeopen)); 321 } else { 322 $dates[] = get_string('quizwillopen', 'quiz', userdate($quiz->timeopen)); 323 } 324 } 325 if ($quiz->timeclose > 0) { 326 if ($timenow > $quiz->timeclose) { 327 $dates[] = get_string('quizclosed', 'quiz', userdate($quiz->timeclose)); 328 } else { 329 $dates[] = get_string('quizcloseson', 'quiz', userdate($quiz->timeclose)); 330 } 331 } 332 if (empty($dates)) { 333 $dates[] = get_string('alwaysavailable', 'quiz'); 334 } 335 $explanation = implode(', ', $dates); 336 337 // Brief summary on the page. 338 if ($timenow < $quiz->timeopen) { 339 $currentstatus = get_string('quizisclosedwillopen', 'quiz', 340 userdate($quiz->timeopen, get_string('strftimedatetimeshort', 'langconfig'))); 341 } else if ($quiz->timeclose && $timenow <= $quiz->timeclose) { 342 $currentstatus = get_string('quizisopenwillclose', 'quiz', 343 userdate($quiz->timeclose, get_string('strftimedatetimeshort', 'langconfig'))); 344 } else if ($quiz->timeclose && $timenow > $quiz->timeclose) { 345 $currentstatus = get_string('quizisclosed', 'quiz'); 346 } else { 347 $currentstatus = get_string('quizisopen', 'quiz'); 348 } 349 350 return array($currentstatus, $explanation); 351 } 352 353 /** 354 * Set up this class with the structure for a given quiz. 355 * @param \stdClass $quiz the quiz settings. 356 */ 357 public function populate_structure($quiz) { 358 global $DB; 359 360 $slots = $DB->get_records_sql(" 361 SELECT slot.id AS slotid, slot.slot, slot.questionid, slot.page, slot.maxmark, 362 q.*, qc.contextid 363 FROM {quiz_slots} slot 364 LEFT JOIN {question} q ON q.id = slot.questionid 365 LEFT JOIN {question_categories} qc ON qc.id = q.category 366 WHERE slot.quizid = ? 367 ORDER BY slot.slot", array($quiz->id)); 368 369 $slots = $this->populate_missing_questions($slots); 370 371 $this->questions = array(); 372 $this->slots = array(); 373 $this->slotsinorder = array(); 374 foreach ($slots as $slotdata) { 375 $this->questions[$slotdata->questionid] = $slotdata; 376 377 $slot = new \stdClass(); 378 $slot->id = $slotdata->slotid; 379 $slot->slot = $slotdata->slot; 380 $slot->quizid = $quiz->id; 381 $slot->page = $slotdata->page; 382 $slot->questionid = $slotdata->questionid; 383 $slot->maxmark = $slotdata->maxmark; 384 385 $this->slots[$slot->id] = $slot; 386 $this->slotsinorder[$slot->slot] = $slot; 387 } 388 389 $section = new \stdClass(); 390 $section->id = 1; 391 $section->quizid = $quiz->id; 392 $section->heading = ''; 393 $section->firstslot = 1; 394 $section->shuffle = false; 395 $this->sections = array(1 => $section); 396 397 $this->populate_slots_with_sectionids(); 398 $this->populate_question_numbers(); 399 } 400 401 /** 402 * Used by populate. Make up fake data for any missing questions. 403 * @param \stdClass[] $slots the data about the slots and questions in the quiz. 404 * @return \stdClass[] updated $slots array. 405 */ 406 protected function populate_missing_questions($slots) { 407 // Address missing question types. 408 foreach ($slots as $slot) { 409 if ($slot->qtype === null) { 410 // If the questiontype is missing change the question type. 411 $slot->id = $slot->questionid; 412 $slot->category = 0; 413 $slot->qtype = 'missingtype'; 414 $slot->name = get_string('missingquestion', 'quiz'); 415 $slot->slot = $slot->slot; 416 $slot->maxmark = 0; 417 $slot->questiontext = ' '; 418 $slot->questiontextformat = FORMAT_HTML; 419 $slot->length = 1; 420 421 } else if (!\question_bank::qtype_exists($slot->qtype)) { 422 $slot->qtype = 'missingtype'; 423 } 424 } 425 426 return $slots; 427 } 428 429 /** 430 * Fill in the section ids for each slot. 431 */ 432 public function populate_slots_with_sectionids() { 433 $nextsection = reset($this->sections); 434 foreach ($this->slotsinorder as $slot) { 435 if ($slot->slot == $nextsection->firstslot) { 436 $currentsectionid = $nextsection->id; 437 $nextsection = next($this->sections); 438 if (!$nextsection) { 439 $nextsection = new \stdClass(); 440 $nextsection->firstslot = -1; 441 } 442 } 443 444 $slot->sectionid = $currentsectionid; 445 } 446 } 447 448 /** 449 * Number the questions. 450 */ 451 protected function populate_question_numbers() { 452 $number = 1; 453 foreach ($this->slots as $slot) { 454 $question = $this->questions[$slot->questionid]; 455 if ($question->length == 0) { 456 $question->displayednumber = get_string('infoshort', 'quiz'); 457 } else { 458 $question->displayednumber = $number; 459 $number += 1; 460 } 461 } 462 } 463 464 /** 465 * Move a slot from its current location to a new location. 466 * 467 * After callig this method, this class will be in an invalid state, and 468 * should be discarded if you want to manipulate the structure further. 469 * 470 * @param int $idmove id of slot to be moved 471 * @param int $idbefore id of slot to come before slot being moved 472 * @param int $page new page number of slot being moved 473 * @return void 474 */ 475 public function move_slot($idmove, $idbefore, $page) { 476 global $DB; 477 478 $this->check_can_be_edited(); 479 480 $movingslot = $this->slots[$idmove]; 481 if (empty($movingslot)) { 482 throw new moodle_exception('Bad slot ID ' . $idmove); 483 } 484 $movingslotnumber = (int) $movingslot->slot; 485 486 // Empty target slot means move slot to first. 487 if (empty($idbefore)) { 488 $targetslotnumber = 0; 489 } else { 490 $targetslotnumber = (int) $this->slots[$idbefore]->slot; 491 } 492 493 // Work out how things are being moved. 494 $slotreorder = array(); 495 if ($targetslotnumber > $movingslotnumber) { 496 $slotreorder[$movingslotnumber] = $targetslotnumber; 497 for ($i = $movingslotnumber; $i < $targetslotnumber; $i++) { 498 $slotreorder[$i + 1] = $i; 499 } 500 } else if ($targetslotnumber < $movingslotnumber - 1) { 501 $slotreorder[$movingslotnumber] = $targetslotnumber + 1; 502 for ($i = $targetslotnumber + 1; $i < $movingslotnumber; $i++) { 503 $slotreorder[$i] = $i + 1; 504 } 505 } 506 507 $trans = $DB->start_delegated_transaction(); 508 509 // Slot has moved record new order. 510 if ($slotreorder) { 511 update_field_with_unique_index('quiz_slots', 'slot', $slotreorder, 512 array('quizid' => $this->get_quizid())); 513 } 514 515 // Page has changed. Record it. 516 if (!$page) { 517 $page = 1; 518 } 519 if ($movingslot->page != $page) { 520 $DB->set_field('quiz_slots', 'page', $page, 521 array('id' => $movingslot->id)); 522 } 523 524 $emptypages = $DB->get_fieldset_sql(" 525 SELECT DISTINCT page - 1 526 FROM {quiz_slots} slot 527 WHERE quizid = ? 528 AND page > 1 529 AND NOT EXISTS (SELECT 1 FROM {quiz_slots} WHERE quizid = ? AND page = slot.page - 1) 530 ORDER BY page - 1 DESC 531 ", array($this->get_quizid(), $this->get_quizid())); 532 533 foreach ($emptypages as $page) { 534 $DB->execute(" 535 UPDATE {quiz_slots} 536 SET page = page - 1 537 WHERE quizid = ? 538 AND page > ? 539 ", array($this->get_quizid(), $page)); 540 } 541 542 $trans->allow_commit(); 543 } 544 545 /** 546 * Refresh page numbering of quiz slots. 547 * @param \stdClass $quiz the quiz object. 548 * @param \stdClass[] $slots (optional) array of slot objects. 549 * @return \stdClass[] array of slot objects. 550 */ 551 public function refresh_page_numbers($quiz, $slots=array()) { 552 global $DB; 553 // Get slots ordered by page then slot. 554 if (!count($slots)) { 555 $slots = $DB->get_records('quiz_slots', array('quizid' => $quiz->id), 'slot, page'); 556 } 557 558 // Loop slots. Start Page number at 1 and increment as required. 559 $pagenumbers = array('new' => 0, 'old' => 0); 560 561 foreach ($slots as $slot) { 562 if ($slot->page !== $pagenumbers['old']) { 563 $pagenumbers['old'] = $slot->page; 564 ++$pagenumbers['new']; 565 } 566 567 if ($pagenumbers['new'] == $slot->page) { 568 continue; 569 } 570 $slot->page = $pagenumbers['new']; 571 } 572 573 return $slots; 574 } 575 576 /** 577 * Refresh page numbering of quiz slots and save to the database. 578 * @param \stdClass $quiz the quiz object. 579 * @return \stdClass[] array of slot objects. 580 */ 581 public function refresh_page_numbers_and_update_db($quiz) { 582 global $DB; 583 $this->check_can_be_edited(); 584 585 $slots = $this->refresh_page_numbers($quiz); 586 587 // Record new page order. 588 foreach ($slots as $slot) { 589 $DB->set_field('quiz_slots', 'page', $slot->page, 590 array('id' => $slot->id)); 591 } 592 593 return $slots; 594 } 595 596 /** 597 * Remove a slot from a quiz 598 * @param \stdClass $quiz the quiz object. 599 * @param int $slotnumber The number of the slot to be deleted. 600 */ 601 public function remove_slot($quiz, $slotnumber) { 602 global $DB; 603 604 $this->check_can_be_edited(); 605 606 $slot = $DB->get_record('quiz_slots', array('quizid' => $quiz->id, 'slot' => $slotnumber)); 607 $maxslot = $DB->get_field_sql('SELECT MAX(slot) FROM {quiz_slots} WHERE quizid = ?', array($quiz->id)); 608 if (!$slot) { 609 return; 610 } 611 612 $trans = $DB->start_delegated_transaction(); 613 $DB->delete_records('quiz_slots', array('id' => $slot->id)); 614 for ($i = $slot->slot + 1; $i <= $maxslot; $i++) { 615 $DB->set_field('quiz_slots', 'slot', $i - 1, 616 array('quizid' => $quiz->id, 'slot' => $i)); 617 } 618 619 $qtype = $DB->get_field('question', 'qtype', array('id' => $slot->questionid)); 620 if ($qtype === 'random') { 621 // This function automatically checks if the question is in use, and won't delete if it is. 622 question_delete_question($slot->questionid); 623 } 624 625 unset($this->questions[$slot->questionid]); 626 627 $this->refresh_page_numbers_and_update_db($quiz); 628 629 $trans->allow_commit(); 630 } 631 632 /** 633 * Change the max mark for a slot. 634 * 635 * Saves changes to the question grades in the quiz_slots table and any 636 * corresponding question_attempts. 637 * It does not update 'sumgrades' in the quiz table. 638 * 639 * @param \stdClass $slot row from the quiz_slots table. 640 * @param float $maxmark the new maxmark. 641 * @return bool true if the new grade is different from the old one. 642 */ 643 public function update_slot_maxmark($slot, $maxmark) { 644 global $DB; 645 646 if (abs($maxmark - $slot->maxmark) < 1e-7) { 647 // Grade has not changed. Nothing to do. 648 return false; 649 } 650 651 $trans = $DB->start_delegated_transaction(); 652 $slot->maxmark = $maxmark; 653 $DB->update_record('quiz_slots', $slot); 654 \question_engine::set_max_mark_in_attempts(new \qubaids_for_quiz($slot->quizid), 655 $slot->slot, $maxmark); 656 $trans->allow_commit(); 657 658 return true; 659 } 660 661 /** 662 * Add/Remove a pagebreak. 663 * 664 * Saves changes to the slot page relationship in the quiz_slots table and reorders the paging 665 * for subsequent slots. 666 * 667 * @param \stdClass $quiz the quiz object. 668 * @param int $slotid id of slot. 669 * @param int $type repaginate::LINK or repaginate::UNLINK. 670 * @return \stdClass[] array of slot objects. 671 */ 672 public function update_page_break($quiz, $slotid, $type) { 673 global $DB; 674 675 $this->check_can_be_edited(); 676 677 $quizslots = $DB->get_records('quiz_slots', array('quizid' => $quiz->id), 'slot'); 678 $repaginate = new \mod_quiz\repaginate($quiz->id, $quizslots); 679 $repaginate->repaginate_slots($quizslots[$slotid]->slot, $type); 680 $slots = $this->refresh_page_numbers_and_update_db($quiz); 681 682 return $slots; 683 } 684 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |