[ Index ] |
PHP Cross Reference of Unnamed Project |
[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 displayed question number (or 'i') for a given slot. 123 * @param int $slotnumber the index of the slot in question. 124 * @return string the question number ot display for this slot. 125 */ 126 public function get_displayed_number_for_slot($slotnumber) { 127 return $this->slotsinorder[$slotnumber]->displayednumber; 128 } 129 130 /** 131 * Get the page a given slot is on. 132 * @param int $slotnumber the index of the slot in question. 133 * @return int the page number of the page that slot is on. 134 */ 135 public function get_page_number_for_slot($slotnumber) { 136 return $this->slotsinorder[$slotnumber]->page; 137 } 138 139 /** 140 * Get the slot id of a given slot slot. 141 * @param int $slotnumber the index of the slot in question. 142 * @return int the page number of the page that slot is on. 143 */ 144 public function get_slot_id_for_slot($slotnumber) { 145 return $this->slotsinorder[$slotnumber]->id; 146 } 147 148 /** 149 * Get the question type in a given slot. 150 * @param int $slotnumber the index of the slot in question. 151 * @return string the question type (e.g. multichoice). 152 */ 153 public function get_question_type_for_slot($slotnumber) { 154 return $this->questions[$this->slotsinorder[$slotnumber]->questionid]->qtype; 155 } 156 157 /** 158 * Whether it would be possible, given the question types, etc. for the 159 * question in the given slot to require that the previous question had been 160 * answered before this one is displayed. 161 * @param int $slotnumber the index of the slot in question. 162 * @return bool can this question require the previous one. 163 */ 164 public function can_question_depend_on_previous_slot($slotnumber) { 165 return $slotnumber > 1 && $this->can_finish_during_the_attempt($slotnumber - 1); 166 } 167 168 /** 169 * Whether it is possible for another question to depend on this one finishing. 170 * Note that the answer is not exact, because of random questions, and sometimes 171 * questions cannot be depended upon because of quiz options. 172 * @param int $slotnumber the index of the slot in question. 173 * @return bool can this question finish naturally during the attempt? 174 */ 175 public function can_finish_during_the_attempt($slotnumber) { 176 if ($this->quizobj->get_navigation_method() == QUIZ_NAVMETHOD_SEQ) { 177 return false; 178 } 179 180 if ($this->slotsinorder[$slotnumber]->section->shufflequestions) { 181 return false; 182 } 183 184 if (in_array($this->get_question_type_for_slot($slotnumber), array('random', 'missingtype'))) { 185 return \question_engine::can_questions_finish_during_the_attempt( 186 $this->quizobj->get_quiz()->preferredbehaviour); 187 } 188 189 if (isset($this->slotsinorder[$slotnumber]->canfinish)) { 190 return $this->slotsinorder[$slotnumber]->canfinish; 191 } 192 193 try { 194 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context()); 195 $tempslot = $quba->add_question(\question_bank::load_question( 196 $this->slotsinorder[$slotnumber]->questionid)); 197 $quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour); 198 $quba->start_all_questions(); 199 200 $this->slotsinorder[$slotnumber]->canfinish = $quba->can_question_finish_during_attempt($tempslot); 201 return $this->slotsinorder[$slotnumber]->canfinish; 202 } catch (\Exception $e) { 203 // If the question fails to start, this should not block editing. 204 return false; 205 } 206 } 207 208 /** 209 * Whether it would be possible, given the question types, etc. for the 210 * question in the given slot to require that the previous question had been 211 * answered before this one is displayed. 212 * @param int $slotnumber the index of the slot in question. 213 * @return bool can this question require the previous one. 214 */ 215 public function is_question_dependent_on_previous_slot($slotnumber) { 216 return $this->slotsinorder[$slotnumber]->requireprevious; 217 } 218 219 /** 220 * Is a particular question in this attempt a real question, or something like a description. 221 * @param int $slotnumber the index of the slot in question. 222 * @return bool whether that question is a real question. 223 */ 224 public function is_real_question($slotnumber) { 225 return $this->get_question_in_slot($slotnumber)->length != 0; 226 } 227 228 /** 229 * Get the course id that the quiz belongs to. 230 * @return int the course.id for the quiz. 231 */ 232 public function get_courseid() { 233 return $this->quizobj->get_courseid(); 234 } 235 236 /** 237 * Get the course module id of the quiz. 238 * @return int the course_modules.id for the quiz. 239 */ 240 public function get_cmid() { 241 return $this->quizobj->get_cmid(); 242 } 243 244 /** 245 * Get id of the quiz. 246 * @return int the quiz.id for the quiz. 247 */ 248 public function get_quizid() { 249 return $this->quizobj->get_quizid(); 250 } 251 252 /** 253 * Get the quiz object. 254 * @return \stdClass the quiz settings row from the database. 255 */ 256 public function get_quiz() { 257 return $this->quizobj->get_quiz(); 258 } 259 260 /** 261 * Quizzes can only be repaginated if they have not been attempted, the 262 * questions are not shuffled, and there are two or more questions. 263 * @return bool whether this quiz can be repaginated. 264 */ 265 public function can_be_repaginated() { 266 return $this->can_be_edited() && $this->get_question_count() >= 2; 267 } 268 269 /** 270 * Quizzes can only be edited if they have not been attempted. 271 * @return bool whether the quiz can be edited. 272 */ 273 public function can_be_edited() { 274 if ($this->canbeedited === null) { 275 $this->canbeedited = !quiz_has_attempts($this->quizobj->get_quizid()); 276 } 277 return $this->canbeedited; 278 } 279 280 /** 281 * This quiz can only be edited if they have not been attempted. 282 * Throw an exception if this is not the case. 283 */ 284 public function check_can_be_edited() { 285 if (!$this->can_be_edited()) { 286 $reportlink = quiz_attempt_summary_link_to_reports($this->get_quiz(), 287 $this->quizobj->get_cm(), $this->quizobj->get_context()); 288 throw new \moodle_exception('cannoteditafterattempts', 'quiz', 289 new \moodle_url('/mod/quiz/edit.php', array('cmid' => $this->get_cmid())), $reportlink); 290 } 291 } 292 293 /** 294 * How many questions are allowed per page in the quiz. 295 * This setting controls how frequently extra page-breaks should be inserted 296 * automatically when questions are added to the quiz. 297 * @return int the number of questions that should be on each page of the 298 * quiz by default. 299 */ 300 public function get_questions_per_page() { 301 return $this->quizobj->get_quiz()->questionsperpage; 302 } 303 304 /** 305 * Get quiz slots. 306 * @return \stdClass[] the slots in this quiz. 307 */ 308 public function get_slots() { 309 return $this->slots; 310 } 311 312 /** 313 * Is this slot the first one on its page? 314 * @param int $slotnumber the index of the slot in question. 315 * @return bool whether this slot the first one on its page. 316 */ 317 public function is_first_slot_on_page($slotnumber) { 318 if ($slotnumber == 1) { 319 return true; 320 } 321 return $this->slotsinorder[$slotnumber]->page != $this->slotsinorder[$slotnumber - 1]->page; 322 } 323 324 /** 325 * Is this slot the last one on its page? 326 * @param int $slotnumber the index of the slot in question. 327 * @return bool whether this slot the last one on its page. 328 */ 329 public function is_last_slot_on_page($slotnumber) { 330 if (!isset($this->slotsinorder[$slotnumber + 1])) { 331 return true; 332 } 333 return $this->slotsinorder[$slotnumber]->page != $this->slotsinorder[$slotnumber + 1]->page; 334 } 335 336 /** 337 * Is this slot the last one in its section? 338 * @param int $slotnumber the index of the slot in question. 339 * @return bool whether this slot the last one on its section. 340 */ 341 public function is_last_slot_in_section($slotnumber) { 342 return $slotnumber == $this->slotsinorder[$slotnumber]->section->lastslot; 343 } 344 345 /** 346 * Is this slot the only one in its section? 347 * @param int $slotnumber the index of the slot in question. 348 * @return bool whether this slot the only one on its section. 349 */ 350 public function is_only_slot_in_section($slotnumber) { 351 return $this->slotsinorder[$slotnumber]->section->firstslot == 352 $this->slotsinorder[$slotnumber]->section->lastslot; 353 } 354 355 /** 356 * Is this slot the last one in the quiz? 357 * @param int $slotnumber the index of the slot in question. 358 * @return bool whether this slot the last one in the quiz. 359 */ 360 public function is_last_slot_in_quiz($slotnumber) { 361 end($this->slotsinorder); 362 return $slotnumber == key($this->slotsinorder); 363 } 364 365 /** 366 * Is this the first section in the quiz? 367 * @param \stdClass $section the quiz_sections row. 368 * @return bool whether this is first section in the quiz. 369 */ 370 public function is_first_section($section) { 371 return $section->firstslot == 1; 372 } 373 374 /** 375 * Is this the last section in the quiz? 376 * @param \stdClass $section the quiz_sections row. 377 * @return bool whether this is first section in the quiz. 378 */ 379 public function is_last_section($section) { 380 return $section->id == end($this->sections)->id; 381 } 382 383 /** 384 * Does this section only contain one slot? 385 * @param \stdClass $section the quiz_sections row. 386 * @return bool whether this section contains only one slot. 387 */ 388 public function is_only_one_slot_in_section($section) { 389 return $section->firstslot == $section->lastslot; 390 } 391 392 /** 393 * Get the final slot in the quiz. 394 * @return \stdClass the quiz_slots for for the final slot in the quiz. 395 */ 396 public function get_last_slot() { 397 return end($this->slotsinorder); 398 } 399 400 /** 401 * Get a slot by it's id. Throws an exception if it is missing. 402 * @param int $slotid the slot id. 403 * @return \stdClass the requested quiz_slots row. 404 */ 405 public function get_slot_by_id($slotid) { 406 if (!array_key_exists($slotid, $this->slots)) { 407 throw new \coding_exception('The \'slotid\' could not be found.'); 408 } 409 return $this->slots[$slotid]; 410 } 411 412 /** 413 * Check whether adding a section heading is possible 414 * @param int $pagenumber the number of the page. 415 * @return boolean 416 */ 417 public function can_add_section_heading($pagenumber) { 418 // There is a default section heading on this page, 419 // do not show adding new section heading in the Add menu. 420 if ($pagenumber == 1) { 421 return false; 422 } 423 // Get an array of firstslots. 424 $firstslots = array(); 425 foreach ($this->sections as $section) { 426 $firstslots[] = $section->firstslot; 427 } 428 foreach ($this->slotsinorder as $slot) { 429 if ($slot->page == $pagenumber) { 430 if (in_array($slot->slot, $firstslots)) { 431 return false; 432 } 433 } 434 } 435 // Do not show the adding section heading on the last add menu. 436 if ($pagenumber == 0) { 437 return false; 438 } 439 return true; 440 } 441 442 /** 443 * Get all the slots in a section of the quiz. 444 * @param int $sectionid the section id. 445 * @return int[] slot numbers. 446 */ 447 public function get_slots_in_section($sectionid) { 448 $slots = array(); 449 foreach ($this->slotsinorder as $slot) { 450 if ($slot->section->id == $sectionid) { 451 $slots[] = $slot->slot; 452 } 453 } 454 return $slots; 455 } 456 457 /** 458 * Get all the sections of the quiz. 459 * @return \stdClass[] the sections in this quiz. 460 */ 461 public function get_sections() { 462 return $this->sections; 463 } 464 465 /** 466 * Get a particular section by id. 467 * @return \stdClass the section. 468 */ 469 public function get_section_by_id($sectionid) { 470 return $this->sections[$sectionid]; 471 } 472 473 /** 474 * Get the number of questions in the quiz. 475 * @return int the number of questions in the quiz. 476 */ 477 public function get_section_count() { 478 return count($this->sections); 479 } 480 481 /** 482 * Get the overall quiz grade formatted for display. 483 * @return string the maximum grade for this quiz. 484 */ 485 public function formatted_quiz_grade() { 486 return quiz_format_grade($this->get_quiz(), $this->get_quiz()->grade); 487 } 488 489 /** 490 * Get the maximum mark for a question, formatted for display. 491 * @param int $slotnumber the index of the slot in question. 492 * @return string the maximum mark for the question in this slot. 493 */ 494 public function formatted_question_grade($slotnumber) { 495 return quiz_format_question_grade($this->get_quiz(), $this->slotsinorder[$slotnumber]->maxmark); 496 } 497 498 /** 499 * Get the number of decimal places for displyaing overall quiz grades or marks. 500 * @return int the number of decimal places. 501 */ 502 public function get_decimal_places_for_grades() { 503 return $this->get_quiz()->decimalpoints; 504 } 505 506 /** 507 * Get the number of decimal places for displyaing question marks. 508 * @return int the number of decimal places. 509 */ 510 public function get_decimal_places_for_question_marks() { 511 return quiz_get_grade_format($this->get_quiz()); 512 } 513 514 /** 515 * Get any warnings to show at the top of the edit page. 516 * @return string[] array of strings. 517 */ 518 public function get_edit_page_warnings() { 519 $warnings = array(); 520 521 if (quiz_has_attempts($this->quizobj->get_quizid())) { 522 $reviewlink = quiz_attempt_summary_link_to_reports($this->quizobj->get_quiz(), 523 $this->quizobj->get_cm(), $this->quizobj->get_context()); 524 $warnings[] = get_string('cannoteditafterattempts', 'quiz', $reviewlink); 525 } 526 527 return $warnings; 528 } 529 530 /** 531 * Get the date information about the current state of the quiz. 532 * @return string[] array of two strings. First a short summary, then a longer 533 * explanation of the current state, e.g. for a tool-tip. 534 */ 535 public function get_dates_summary() { 536 $timenow = time(); 537 $quiz = $this->quizobj->get_quiz(); 538 539 // Exact open and close dates for the tool-tip. 540 $dates = array(); 541 if ($quiz->timeopen > 0) { 542 if ($timenow > $quiz->timeopen) { 543 $dates[] = get_string('quizopenedon', 'quiz', userdate($quiz->timeopen)); 544 } else { 545 $dates[] = get_string('quizwillopen', 'quiz', userdate($quiz->timeopen)); 546 } 547 } 548 if ($quiz->timeclose > 0) { 549 if ($timenow > $quiz->timeclose) { 550 $dates[] = get_string('quizclosed', 'quiz', userdate($quiz->timeclose)); 551 } else { 552 $dates[] = get_string('quizcloseson', 'quiz', userdate($quiz->timeclose)); 553 } 554 } 555 if (empty($dates)) { 556 $dates[] = get_string('alwaysavailable', 'quiz'); 557 } 558 $explanation = implode(', ', $dates); 559 560 // Brief summary on the page. 561 if ($timenow < $quiz->timeopen) { 562 $currentstatus = get_string('quizisclosedwillopen', 'quiz', 563 userdate($quiz->timeopen, get_string('strftimedatetimeshort', 'langconfig'))); 564 } else if ($quiz->timeclose && $timenow <= $quiz->timeclose) { 565 $currentstatus = get_string('quizisopenwillclose', 'quiz', 566 userdate($quiz->timeclose, get_string('strftimedatetimeshort', 'langconfig'))); 567 } else if ($quiz->timeclose && $timenow > $quiz->timeclose) { 568 $currentstatus = get_string('quizisclosed', 'quiz'); 569 } else { 570 $currentstatus = get_string('quizisopen', 'quiz'); 571 } 572 573 return array($currentstatus, $explanation); 574 } 575 576 /** 577 * Set up this class with the structure for a given quiz. 578 * @param \stdClass $quiz the quiz settings. 579 */ 580 public function populate_structure($quiz) { 581 global $DB; 582 583 $slots = $DB->get_records_sql(" 584 SELECT slot.id AS slotid, slot.slot, slot.questionid, slot.page, slot.maxmark, 585 slot.requireprevious, q.*, qc.contextid 586 FROM {quiz_slots} slot 587 LEFT JOIN {question} q ON q.id = slot.questionid 588 LEFT JOIN {question_categories} qc ON qc.id = q.category 589 WHERE slot.quizid = ? 590 ORDER BY slot.slot", array($quiz->id)); 591 592 $slots = $this->populate_missing_questions($slots); 593 594 $this->questions = array(); 595 $this->slots = array(); 596 $this->slotsinorder = array(); 597 foreach ($slots as $slotdata) { 598 $this->questions[$slotdata->questionid] = $slotdata; 599 600 $slot = new \stdClass(); 601 $slot->id = $slotdata->slotid; 602 $slot->slot = $slotdata->slot; 603 $slot->quizid = $quiz->id; 604 $slot->page = $slotdata->page; 605 $slot->questionid = $slotdata->questionid; 606 $slot->maxmark = $slotdata->maxmark; 607 $slot->requireprevious = $slotdata->requireprevious; 608 609 $this->slots[$slot->id] = $slot; 610 $this->slotsinorder[$slot->slot] = $slot; 611 } 612 613 // Get quiz sections in ascending order of the firstslot. 614 $this->sections = $DB->get_records('quiz_sections', array('quizid' => $quiz->id), 'firstslot ASC'); 615 $this->populate_slots_with_sections(); 616 $this->populate_question_numbers(); 617 } 618 619 /** 620 * Used by populate. Make up fake data for any missing questions. 621 * @param \stdClass[] $slots the data about the slots and questions in the quiz. 622 * @return \stdClass[] updated $slots array. 623 */ 624 protected function populate_missing_questions($slots) { 625 // Address missing question types. 626 foreach ($slots as $slot) { 627 if ($slot->qtype === null) { 628 // If the questiontype is missing change the question type. 629 $slot->id = $slot->questionid; 630 $slot->category = 0; 631 $slot->qtype = 'missingtype'; 632 $slot->name = get_string('missingquestion', 'quiz'); 633 $slot->slot = $slot->slot; 634 $slot->maxmark = 0; 635 $slot->requireprevious = 0; 636 $slot->questiontext = ' '; 637 $slot->questiontextformat = FORMAT_HTML; 638 $slot->length = 1; 639 640 } else if (!\question_bank::qtype_exists($slot->qtype)) { 641 $slot->qtype = 'missingtype'; 642 } 643 } 644 645 return $slots; 646 } 647 648 /** 649 * Fill in the section ids for each slot. 650 */ 651 public function populate_slots_with_sections() { 652 $sections = array_values($this->sections); 653 foreach ($sections as $i => $section) { 654 if (isset($sections[$i + 1])) { 655 $section->lastslot = $sections[$i + 1]->firstslot - 1; 656 } else { 657 $section->lastslot = count($this->slotsinorder); 658 } 659 for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { 660 $this->slotsinorder[$slot]->section = $section; 661 } 662 } 663 } 664 665 /** 666 * Number the questions. 667 */ 668 protected function populate_question_numbers() { 669 $number = 1; 670 foreach ($this->slots as $slot) { 671 if ($this->questions[$slot->questionid]->length == 0) { 672 $slot->displayednumber = get_string('infoshort', 'quiz'); 673 } else { 674 $slot->displayednumber = $number; 675 $number += 1; 676 } 677 } 678 } 679 680 /** 681 * Move a slot from its current location to a new location. 682 * 683 * After callig this method, this class will be in an invalid state, and 684 * should be discarded if you want to manipulate the structure further. 685 * 686 * @param int $idmove id of slot to be moved 687 * @param int $idmoveafter id of slot to come before slot being moved 688 * @param int $page new page number of slot being moved 689 * @param bool $insection if the question is moving to a place where a new 690 * section starts, include it in that section. 691 * @return void 692 */ 693 public function move_slot($idmove, $idmoveafter, $page) { 694 global $DB; 695 696 $this->check_can_be_edited(); 697 698 $movingslot = $this->slots[$idmove]; 699 if (empty($movingslot)) { 700 throw new \moodle_exception('Bad slot ID ' . $idmove); 701 } 702 $movingslotnumber = (int) $movingslot->slot; 703 704 // Empty target slot means move slot to first. 705 if (empty($idmoveafter)) { 706 $moveafterslotnumber = 0; 707 } else { 708 $moveafterslotnumber = (int) $this->slots[$idmoveafter]->slot; 709 } 710 711 // If the action came in as moving a slot to itself, normalise this to 712 // moving the slot to after the previous slot. 713 if ($moveafterslotnumber == $movingslotnumber) { 714 $moveafterslotnumber = $moveafterslotnumber - 1; 715 } 716 717 $followingslotnumber = $moveafterslotnumber + 1; 718 if ($followingslotnumber == $movingslotnumber) { 719 $followingslotnumber += 1; 720 } 721 722 // Check the target page number is OK. 723 if ($page == 0) { 724 $page = 1; 725 } 726 if (($moveafterslotnumber > 0 && $page < $this->get_page_number_for_slot($moveafterslotnumber)) || 727 $page < 1) { 728 throw new \coding_exception('The target page number is too small.'); 729 } else if (!$this->is_last_slot_in_quiz($moveafterslotnumber) && 730 $page > $this->get_page_number_for_slot($followingslotnumber)) { 731 throw new \coding_exception('The target page number is too large.'); 732 } 733 734 // Work out how things are being moved. 735 $slotreorder = array(); 736 if ($moveafterslotnumber > $movingslotnumber) { 737 // Moving down. 738 $slotreorder[$movingslotnumber] = $moveafterslotnumber; 739 for ($i = $movingslotnumber; $i < $moveafterslotnumber; $i++) { 740 $slotreorder[$i + 1] = $i; 741 } 742 743 $headingmoveafter = $movingslotnumber; 744 if ($this->is_last_slot_in_quiz($moveafterslotnumber) || 745 $page == $this->get_page_number_for_slot($moveafterslotnumber + 1)) { 746 // We are moving to the start of a section, so that heading needs 747 // to be included in the ones that move up. 748 $headingmovebefore = $moveafterslotnumber + 1; 749 } else { 750 $headingmovebefore = $moveafterslotnumber; 751 } 752 $headingmovedirection = -1; 753 754 } else if ($moveafterslotnumber < $movingslotnumber - 1) { 755 // Moving up. 756 $slotreorder[$movingslotnumber] = $moveafterslotnumber + 1; 757 for ($i = $moveafterslotnumber + 1; $i < $movingslotnumber; $i++) { 758 $slotreorder[$i] = $i + 1; 759 } 760 761 if ($page == $this->get_page_number_for_slot($moveafterslotnumber + 1)) { 762 // Moving to the start of a section, don't move that section. 763 $headingmoveafter = $moveafterslotnumber + 1; 764 } else { 765 // Moving tot the end of the previous section, so move the heading down too. 766 $headingmoveafter = $moveafterslotnumber; 767 } 768 $headingmovebefore = $movingslotnumber + 1; 769 $headingmovedirection = 1; 770 } else { 771 // Staying in the same place, but possibly changing page/section. 772 if ($page > $movingslot->page) { 773 $headingmoveafter = $movingslotnumber; 774 $headingmovebefore = $movingslotnumber + 2; 775 $headingmovedirection = -1; 776 } else if ($page < $movingslot->page) { 777 $headingmoveafter = $movingslotnumber - 1; 778 $headingmovebefore = $movingslotnumber + 1; 779 $headingmovedirection = 1; 780 } else { 781 return; // Nothing to do. 782 } 783 } 784 785 if ($this->is_only_slot_in_section($movingslotnumber)) { 786 throw new \coding_exception('You cannot remove the last slot in a section.'); 787 } 788 789 $trans = $DB->start_delegated_transaction(); 790 791 // Slot has moved record new order. 792 if ($slotreorder) { 793 update_field_with_unique_index('quiz_slots', 'slot', $slotreorder, 794 array('quizid' => $this->get_quizid())); 795 } 796 797 // Page has changed. Record it. 798 if ($movingslot->page != $page) { 799 $DB->set_field('quiz_slots', 'page', $page, 800 array('id' => $movingslot->id)); 801 } 802 803 // Update section fist slots. 804 $DB->execute(" 805 UPDATE {quiz_sections} 806 SET firstslot = firstslot + ? 807 WHERE quizid = ? 808 AND firstslot > ? 809 AND firstslot < ? 810 ", array($headingmovedirection, $this->get_quizid(), 811 $headingmoveafter, $headingmovebefore)); 812 813 // If any pages are now empty, remove them. 814 $emptypages = $DB->get_fieldset_sql(" 815 SELECT DISTINCT page - 1 816 FROM {quiz_slots} slot 817 WHERE quizid = ? 818 AND page > 1 819 AND NOT EXISTS (SELECT 1 FROM {quiz_slots} WHERE quizid = ? AND page = slot.page - 1) 820 ORDER BY page - 1 DESC 821 ", array($this->get_quizid(), $this->get_quizid())); 822 823 foreach ($emptypages as $page) { 824 $DB->execute(" 825 UPDATE {quiz_slots} 826 SET page = page - 1 827 WHERE quizid = ? 828 AND page > ? 829 ", array($this->get_quizid(), $page)); 830 } 831 832 $trans->allow_commit(); 833 } 834 835 /** 836 * Refresh page numbering of quiz slots. 837 * @param \stdClass[] $slots (optional) array of slot objects. 838 * @return \stdClass[] array of slot objects. 839 */ 840 public function refresh_page_numbers($slots = array()) { 841 global $DB; 842 // Get slots ordered by page then slot. 843 if (!count($slots)) { 844 $slots = $DB->get_records('quiz_slots', array('quizid' => $this->get_quizid()), 'slot, page'); 845 } 846 847 // Loop slots. Start Page number at 1 and increment as required. 848 $pagenumbers = array('new' => 0, 'old' => 0); 849 850 foreach ($slots as $slot) { 851 if ($slot->page !== $pagenumbers['old']) { 852 $pagenumbers['old'] = $slot->page; 853 ++$pagenumbers['new']; 854 } 855 856 if ($pagenumbers['new'] == $slot->page) { 857 continue; 858 } 859 $slot->page = $pagenumbers['new']; 860 } 861 862 return $slots; 863 } 864 865 /** 866 * Refresh page numbering of quiz slots and save to the database. 867 * @param \stdClass $quiz the quiz object. 868 * @return \stdClass[] array of slot objects. 869 */ 870 public function refresh_page_numbers_and_update_db() { 871 global $DB; 872 $this->check_can_be_edited(); 873 874 $slots = $this->refresh_page_numbers(); 875 876 // Record new page order. 877 foreach ($slots as $slot) { 878 $DB->set_field('quiz_slots', 'page', $slot->page, 879 array('id' => $slot->id)); 880 } 881 882 return $slots; 883 } 884 885 /** 886 * Remove a slot from a quiz 887 * @param int $slotnumber The number of the slot to be deleted. 888 */ 889 public function remove_slot($slotnumber) { 890 global $DB; 891 892 $this->check_can_be_edited(); 893 894 if ($this->is_only_slot_in_section($slotnumber) && $this->get_section_count() > 1) { 895 throw new \coding_exception('You cannot remove the last slot in a section.'); 896 } 897 898 $slot = $DB->get_record('quiz_slots', array('quizid' => $this->get_quizid(), 'slot' => $slotnumber)); 899 if (!$slot) { 900 return; 901 } 902 $maxslot = $DB->get_field_sql('SELECT MAX(slot) FROM {quiz_slots} WHERE quizid = ?', array($this->get_quizid())); 903 904 $trans = $DB->start_delegated_transaction(); 905 $DB->delete_records('quiz_slots', array('id' => $slot->id)); 906 for ($i = $slot->slot + 1; $i <= $maxslot; $i++) { 907 $DB->set_field('quiz_slots', 'slot', $i - 1, 908 array('quizid' => $this->get_quizid(), 'slot' => $i)); 909 } 910 911 $qtype = $DB->get_field('question', 'qtype', array('id' => $slot->questionid)); 912 if ($qtype === 'random') { 913 // This function automatically checks if the question is in use, and won't delete if it is. 914 question_delete_question($slot->questionid); 915 } 916 917 $DB->execute(" 918 UPDATE {quiz_sections} 919 SET firstslot = firstslot - 1 920 WHERE quizid = ? 921 AND firstslot > ? 922 ", array($this->get_quizid(), $slotnumber)); 923 unset($this->questions[$slot->questionid]); 924 925 $this->refresh_page_numbers_and_update_db(); 926 927 $trans->allow_commit(); 928 } 929 930 /** 931 * Change the max mark for a slot. 932 * 933 * Saves changes to the question grades in the quiz_slots table and any 934 * corresponding question_attempts. 935 * It does not update 'sumgrades' in the quiz table. 936 * 937 * @param \stdClass $slot row from the quiz_slots table. 938 * @param float $maxmark the new maxmark. 939 * @return bool true if the new grade is different from the old one. 940 */ 941 public function update_slot_maxmark($slot, $maxmark) { 942 global $DB; 943 944 if (abs($maxmark - $slot->maxmark) < 1e-7) { 945 // Grade has not changed. Nothing to do. 946 return false; 947 } 948 949 $trans = $DB->start_delegated_transaction(); 950 $slot->maxmark = $maxmark; 951 $DB->update_record('quiz_slots', $slot); 952 \question_engine::set_max_mark_in_attempts(new \qubaids_for_quiz($slot->quizid), 953 $slot->slot, $maxmark); 954 $trans->allow_commit(); 955 956 return true; 957 } 958 959 /** 960 * Set whether the question in a particular slot requires the previous one. 961 * @param int $slotid id of slot. 962 * @param bool $requireprevious if true, set this question to require the previous one. 963 */ 964 public function update_question_dependency($slotid, $requireprevious) { 965 global $DB; 966 $DB->set_field('quiz_slots', 'requireprevious', $requireprevious, array('id' => $slotid)); 967 } 968 969 /** 970 * Add/Remove a pagebreak. 971 * 972 * Saves changes to the slot page relationship in the quiz_slots table and reorders the paging 973 * for subsequent slots. 974 * 975 * @param int $slotid id of slot. 976 * @param int $type repaginate::LINK or repaginate::UNLINK. 977 * @return \stdClass[] array of slot objects. 978 */ 979 public function update_page_break($slotid, $type) { 980 global $DB; 981 982 $this->check_can_be_edited(); 983 984 $quizslots = $DB->get_records('quiz_slots', array('quizid' => $this->get_quizid()), 'slot'); 985 $repaginate = new \mod_quiz\repaginate($this->get_quizid(), $quizslots); 986 $repaginate->repaginate_slots($quizslots[$slotid]->slot, $type); 987 $slots = $this->refresh_page_numbers_and_update_db(); 988 989 return $slots; 990 } 991 992 /** 993 * Add a section heading on a given page and return the sectionid 994 * @param int $pagenumber the number of the page where the section heading begins. 995 * @param string $heading the heading to add. 996 */ 997 public function add_section_heading($pagenumber, $heading = 'Section heading ...') { 998 global $DB; 999 $section = new \stdClass(); 1000 $section->heading = $heading; 1001 $section->quizid = $this->get_quizid(); 1002 $slotsonpage = $DB->get_records('quiz_slots', array('quizid' => $this->get_quizid(), 'page' => $pagenumber), 'slot DESC'); 1003 $section->firstslot = end($slotsonpage)->slot; 1004 $section->shufflequestions = 0; 1005 return $DB->insert_record('quiz_sections', $section); 1006 } 1007 1008 /** 1009 * Change the heading for a section. 1010 * @param int $id the id of the section to change. 1011 * @param string $newheading the new heading for this section. 1012 */ 1013 public function set_section_heading($id, $newheading) { 1014 global $DB; 1015 $section = $DB->get_record('quiz_sections', array('id' => $id), '*', MUST_EXIST); 1016 $section->heading = $newheading; 1017 $DB->update_record('quiz_sections', $section); 1018 } 1019 1020 /** 1021 * Change the shuffle setting for a section. 1022 * @param int $id the id of the section to change. 1023 * @param bool $shuffle whether this section should be shuffled. 1024 */ 1025 public function set_section_shuffle($id, $shuffle) { 1026 global $DB; 1027 $section = $DB->get_record('quiz_sections', array('id' => $id), '*', MUST_EXIST); 1028 $section->shufflequestions = $shuffle; 1029 $DB->update_record('quiz_sections', $section); 1030 } 1031 1032 /** 1033 * Remove the section heading with the given id 1034 * @param int $sectionid the section to remove. 1035 */ 1036 public function remove_section_heading($sectionid) { 1037 global $DB; 1038 $section = $DB->get_record('quiz_sections', array('id' => $sectionid), '*', MUST_EXIST); 1039 if ($section->firstslot == 1) { 1040 throw new \coding_exception('Cannot remove the first section in a quiz.'); 1041 } 1042 $DB->delete_records('quiz_sections', array('id' => $sectionid)); 1043 } 1044 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |