[ 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 * This file defines the question attempt class, and a few related classes. 19 * 20 * @package moodlecore 21 * @subpackage questionengine 22 * @copyright 2009 The Open University 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 30 /** 31 * Tracks an attempt at one particular question in a {@link question_usage_by_activity}. 32 * 33 * Most calling code should need to access objects of this class. They should be 34 * able to do everything through the usage interface. This class is an internal 35 * implementation detail of the question engine. 36 * 37 * Instances of this class correspond to rows in the question_attempts table, and 38 * a collection of {@link question_attempt_steps}. Question inteaction models and 39 * question types do work with question_attempt objects. 40 * 41 * @copyright 2009 The Open University 42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 43 */ 44 class question_attempt { 45 /** 46 * @var string this is a magic value that question types can return from 47 * {@link question_definition::get_expected_data()}. 48 */ 49 const USE_RAW_DATA = 'use raw data'; 50 51 /** 52 * @var string Should not longer be used. 53 * @deprecated since Moodle 3.0 54 */ 55 const PARAM_MARK = PARAM_RAW_TRIMMED; 56 57 /** 58 * @var string special value to indicate a response variable that is uploaded 59 * files. 60 */ 61 const PARAM_FILES = 'paramfiles'; 62 63 /** 64 * @var string special value to indicate a response variable that is uploaded 65 * files. 66 */ 67 const PARAM_RAW_FILES = 'paramrawfiles'; 68 69 /** 70 * @var string means first try at a question during an attempt by a user. 71 */ 72 const FIRST_TRY = 'firsttry'; 73 74 /** 75 * @var string means last try at a question during an attempt by a user. 76 */ 77 const LAST_TRY = 'lasttry'; 78 79 /** 80 * @var string means all tries at a question during an attempt by a user. 81 */ 82 const ALL_TRIES = 'alltries'; 83 84 /** @var integer if this attempts is stored in the question_attempts table, the id of that row. */ 85 protected $id = null; 86 87 /** @var integer|string the id of the question_usage_by_activity we belong to. */ 88 protected $usageid; 89 90 /** @var integer the number used to identify this question_attempt within the usage. */ 91 protected $slot = null; 92 93 /** 94 * @var question_behaviour the behaviour controlling this attempt. 95 * null until {@link start()} is called. 96 */ 97 protected $behaviour = null; 98 99 /** @var question_definition the question this is an attempt at. */ 100 protected $question; 101 102 /** @var int which variant of the question to use. */ 103 protected $variant; 104 105 /** 106 * @var float the maximum mark that can be scored at this question. 107 * Actually, this is only really a nominal maximum. It might be better thought 108 * of as the question weight. 109 */ 110 protected $maxmark; 111 112 /** 113 * @var float the minimum fraction that can be scored at this question, so 114 * the minimum mark is $this->minfraction * $this->maxmark. 115 */ 116 protected $minfraction = null; 117 118 /** 119 * @var float the maximum fraction that can be scored at this question, so 120 * the maximum mark is $this->maxfraction * $this->maxmark. 121 */ 122 protected $maxfraction = null; 123 124 /** 125 * @var string plain text summary of the variant of the question the 126 * student saw. Intended for reporting purposes. 127 */ 128 protected $questionsummary = null; 129 130 /** 131 * @var string plain text summary of the response the student gave. 132 * Intended for reporting purposes. 133 */ 134 protected $responsesummary = null; 135 136 /** 137 * @var string plain text summary of the correct response to this question 138 * variant the student saw. The format should be similar to responsesummary. 139 * Intended for reporting purposes. 140 */ 141 protected $rightanswer = null; 142 143 /** @var array of {@link question_attempt_step}s. The steps in this attempt. */ 144 protected $steps = array(); 145 146 /** 147 * @var question_attempt_step if, when we loaded the step from the DB, there was 148 * an autosaved step, we save a pointer to it here. (It is also added to the $steps array.) 149 */ 150 protected $autosavedstep = null; 151 152 /** @var boolean whether the user has flagged this attempt within the usage. */ 153 protected $flagged = false; 154 155 /** @var question_usage_observer tracks changes to the useage this attempt is part of.*/ 156 protected $observer; 157 158 /**#@+ 159 * Constants used by the intereaction models to indicate whether the current 160 * pending step should be kept or discarded. 161 */ 162 const KEEP = true; 163 const DISCARD = false; 164 /**#@-*/ 165 166 /** 167 * Create a new {@link question_attempt}. Normally you should create question_attempts 168 * indirectly, by calling {@link question_usage_by_activity::add_question()}. 169 * 170 * @param question_definition $question the question this is an attempt at. 171 * @param int|string $usageid The id of the 172 * {@link question_usage_by_activity} we belong to. Used by {@link get_field_prefix()}. 173 * @param question_usage_observer $observer tracks changes to the useage this 174 * attempt is part of. (Optional, a {@link question_usage_null_observer} is 175 * used if one is not passed. 176 * @param number $maxmark the maximum grade for this question_attempt. If not 177 * passed, $question->defaultmark is used. 178 */ 179 public function __construct(question_definition $question, $usageid, 180 question_usage_observer $observer = null, $maxmark = null) { 181 $this->question = $question; 182 $this->usageid = $usageid; 183 if (is_null($observer)) { 184 $observer = new question_usage_null_observer(); 185 } 186 $this->observer = $observer; 187 if (!is_null($maxmark)) { 188 $this->maxmark = $maxmark; 189 } else { 190 $this->maxmark = $question->defaultmark; 191 } 192 } 193 194 /** 195 * This method exists so that {@link question_attempt_with_restricted_history} 196 * can override it. You should not normally need to call it. 197 * @return question_attempt return ourself. 198 */ 199 public function get_full_qa() { 200 return $this; 201 } 202 203 /** @return question_definition the question this is an attempt at. */ 204 public function get_question() { 205 return $this->question; 206 } 207 208 /** 209 * Get the variant of the question being used in a given slot. 210 * @return int the variant number. 211 */ 212 public function get_variant() { 213 return $this->variant; 214 } 215 216 /** 217 * Set the number used to identify this question_attempt within the usage. 218 * For internal use only. 219 * @param int $slot 220 */ 221 public function set_slot($slot) { 222 $this->slot = $slot; 223 } 224 225 /** @return int the number used to identify this question_attempt within the usage. */ 226 public function get_slot() { 227 return $this->slot; 228 } 229 230 /** 231 * @return int the id of row for this question_attempt, if it is stored in the 232 * database. null if not. 233 */ 234 public function get_database_id() { 235 return $this->id; 236 } 237 238 /** 239 * For internal use only. Set the id of the corresponding database row. 240 * @param int $id the id of row for this question_attempt, if it is 241 * stored in the database. 242 */ 243 public function set_database_id($id) { 244 $this->id = $id; 245 } 246 247 /** 248 * You should almost certainly not call this method from your code. It is for 249 * internal use only. 250 * @param question_usage_observer that should be used to tracking changes made to this qa. 251 */ 252 public function set_observer($observer) { 253 $this->observer = $observer; 254 } 255 256 /** @return int|string the id of the {@link question_usage_by_activity} we belong to. */ 257 public function get_usage_id() { 258 return $this->usageid; 259 } 260 261 /** 262 * Set the id of the {@link question_usage_by_activity} we belong to. 263 * For internal use only. 264 * @param int|string the new id. 265 */ 266 public function set_usage_id($usageid) { 267 $this->usageid = $usageid; 268 } 269 270 /** @return string the name of the behaviour that is controlling this attempt. */ 271 public function get_behaviour_name() { 272 return $this->behaviour->get_name(); 273 } 274 275 /** 276 * For internal use only. 277 * @return question_behaviour the behaviour that is controlling this attempt. 278 */ 279 public function get_behaviour() { 280 return $this->behaviour; 281 } 282 283 /** 284 * Set the flagged state of this question. 285 * @param bool $flagged the new state. 286 */ 287 public function set_flagged($flagged) { 288 $this->flagged = $flagged; 289 $this->observer->notify_attempt_modified($this); 290 } 291 292 /** @return bool whether this question is currently flagged. */ 293 public function is_flagged() { 294 return $this->flagged; 295 } 296 297 /** 298 * Get the name (in the sense a HTML name="" attribute, or a $_POST variable 299 * name) to use for the field that indicates whether this question is flagged. 300 * 301 * @return string The field name to use. 302 */ 303 public function get_flag_field_name() { 304 return $this->get_control_field_name('flagged'); 305 } 306 307 /** 308 * Get the name (in the sense a HTML name="" attribute, or a $_POST variable 309 * name) to use for a question_type variable belonging to this question_attempt. 310 * 311 * See the comment on {@link question_attempt_step} for an explanation of 312 * question type and behaviour variables. 313 * 314 * @param $varname The short form of the variable name. 315 * @return string The field name to use. 316 */ 317 public function get_qt_field_name($varname) { 318 return $this->get_field_prefix() . $varname; 319 } 320 321 /** 322 * Get the name (in the sense a HTML name="" attribute, or a $_POST variable 323 * name) to use for a question_type variable belonging to this question_attempt. 324 * 325 * See the comment on {@link question_attempt_step} for an explanation of 326 * question type and behaviour variables. 327 * 328 * @param $varname The short form of the variable name. 329 * @return string The field name to use. 330 */ 331 public function get_behaviour_field_name($varname) { 332 return $this->get_field_prefix() . '-' . $varname; 333 } 334 335 /** 336 * Get the name (in the sense a HTML name="" attribute, or a $_POST variable 337 * name) to use for a control variables belonging to this question_attempt. 338 * 339 * Examples are :sequencecheck and :flagged 340 * 341 * @param $varname The short form of the variable name. 342 * @return string The field name to use. 343 */ 344 public function get_control_field_name($varname) { 345 return $this->get_field_prefix() . ':' . $varname; 346 } 347 348 /** 349 * Get the prefix added to variable names to give field names for this 350 * question attempt. 351 * 352 * You should not use this method directly. This is an implementation detail 353 * anyway, but if you must access it, use {@link question_usage_by_activity::get_field_prefix()}. 354 * 355 * @param $varname The short form of the variable name. 356 * @return string The field name to use. 357 */ 358 public function get_field_prefix() { 359 return 'q' . $this->usageid . ':' . $this->slot . '_'; 360 } 361 362 /** 363 * Get one of the steps in this attempt. 364 * 365 * @param int $i the step number, which counts from 0. 366 * @return question_attempt_step 367 */ 368 public function get_step($i) { 369 if ($i < 0 || $i >= count($this->steps)) { 370 throw new coding_exception('Index out of bounds in question_attempt::get_step.'); 371 } 372 return $this->steps[$i]; 373 } 374 375 /** 376 * Get the number of real steps in this attempt. 377 * This is put as a hidden field in the HTML, so that when we receive some 378 * data to process, then we can check that it came from the question 379 * in the state we are now it. 380 * @return int a number that summarises the current state of this question attempt. 381 */ 382 public function get_sequence_check_count() { 383 $numrealsteps = $this->get_num_steps(); 384 if ($this->has_autosaved_step()) { 385 $numrealsteps -= 1; 386 } 387 return $numrealsteps; 388 } 389 390 /** 391 * Get the number of steps in this attempt. 392 * For internal/test code use only. 393 * @return int the number of steps we currently have. 394 */ 395 public function get_num_steps() { 396 return count($this->steps); 397 } 398 399 /** 400 * Return the latest step in this question_attempt. 401 * For internal/test code use only. 402 * @return question_attempt_step 403 */ 404 public function get_last_step() { 405 if (count($this->steps) == 0) { 406 return new question_null_step(); 407 } 408 return end($this->steps); 409 } 410 411 /** 412 * @return boolean whether this question_attempt has autosaved data from 413 * some time in the past. 414 */ 415 public function has_autosaved_step() { 416 return !is_null($this->autosavedstep); 417 } 418 419 /** 420 * @return question_attempt_step_iterator for iterating over the steps in 421 * this attempt, in order. 422 */ 423 public function get_step_iterator() { 424 return new question_attempt_step_iterator($this); 425 } 426 427 /** 428 * The same as {@link get_step_iterator()}. However, for a 429 * {@link question_attempt_with_restricted_history} this returns the full 430 * list of steps, while {@link get_step_iterator()} returns only the 431 * limited history. 432 * @return question_attempt_step_iterator for iterating over the steps in 433 * this attempt, in order. 434 */ 435 public function get_full_step_iterator() { 436 return $this->get_step_iterator(); 437 } 438 439 /** 440 * @return question_attempt_reverse_step_iterator for iterating over the steps in 441 * this attempt, in reverse order. 442 */ 443 public function get_reverse_step_iterator() { 444 return new question_attempt_reverse_step_iterator($this); 445 } 446 447 /** 448 * Get the qt data from the latest step that has any qt data. Return $default 449 * array if it is no step has qt data. 450 * 451 * @param string $name the name of the variable to get. 452 * @param mixed default the value to return no step has qt data. 453 * (Optional, defaults to an empty array.) 454 * @return array|mixed the data, or $default if there is not any. 455 */ 456 public function get_last_qt_data($default = array()) { 457 foreach ($this->get_reverse_step_iterator() as $step) { 458 $response = $step->get_qt_data(); 459 if (!empty($response)) { 460 return $response; 461 } 462 } 463 return $default; 464 } 465 466 /** 467 * Get the last step with a particular question type varialbe set. 468 * @param string $name the name of the variable to get. 469 * @return question_attempt_step the last step, or a step with no variables 470 * if there was not a real step. 471 */ 472 public function get_last_step_with_qt_var($name) { 473 foreach ($this->get_reverse_step_iterator() as $step) { 474 if ($step->has_qt_var($name)) { 475 return $step; 476 } 477 } 478 return new question_attempt_step_read_only(); 479 } 480 481 /** 482 * Get the last step with a particular behaviour variable set. 483 * @param string $name the name of the variable to get. 484 * @return question_attempt_step the last step, or a step with no variables 485 * if there was not a real step. 486 */ 487 public function get_last_step_with_behaviour_var($name) { 488 foreach ($this->get_reverse_step_iterator() as $step) { 489 if ($step->has_behaviour_var($name)) { 490 return $step; 491 } 492 } 493 return new question_attempt_step_read_only(); 494 } 495 496 /** 497 * Get the latest value of a particular question type variable. That is, get 498 * the value from the latest step that has it set. Return null if it is not 499 * set in any step. 500 * 501 * @param string $name the name of the variable to get. 502 * @param mixed default the value to return in the variable has never been set. 503 * (Optional, defaults to null.) 504 * @return mixed string value, or $default if it has never been set. 505 */ 506 public function get_last_qt_var($name, $default = null) { 507 $step = $this->get_last_step_with_qt_var($name); 508 if ($step->has_qt_var($name)) { 509 return $step->get_qt_var($name); 510 } else { 511 return $default; 512 } 513 } 514 515 /** 516 * Get the latest set of files for a particular question type variable of 517 * type question_attempt::PARAM_FILES. 518 * 519 * @param string $name the name of the associated variable. 520 * @return array of {@link stored_files}. 521 */ 522 public function get_last_qt_files($name, $contextid) { 523 foreach ($this->get_reverse_step_iterator() as $step) { 524 if ($step->has_qt_var($name)) { 525 return $step->get_qt_files($name, $contextid); 526 } 527 } 528 return array(); 529 } 530 531 /** 532 * Get the URL of a file that belongs to a response variable of this 533 * question_attempt. 534 * @param stored_file $file the file to link to. 535 * @return string the URL of that file. 536 */ 537 public function get_response_file_url(stored_file $file) { 538 return file_encode_url(new moodle_url('/pluginfile.php'), '/' . implode('/', array( 539 $file->get_contextid(), 540 $file->get_component(), 541 $file->get_filearea(), 542 $this->usageid, 543 $this->slot, 544 $file->get_itemid())) . 545 $file->get_filepath() . $file->get_filename(), true); 546 } 547 548 /** 549 * Prepare a draft file are for the files belonging the a response variable 550 * of this question attempt. The draft area is populated with the files from 551 * the most recent step having files. 552 * 553 * @param string $name the variable name the files belong to. 554 * @param int $contextid the id of the context the quba belongs to. 555 * @return int the draft itemid. 556 */ 557 public function prepare_response_files_draft_itemid($name, $contextid) { 558 foreach ($this->get_reverse_step_iterator() as $step) { 559 if ($step->has_qt_var($name)) { 560 return $step->prepare_response_files_draft_itemid($name, $contextid); 561 } 562 } 563 564 // No files yet. 565 $draftid = 0; // Will be filled in by file_prepare_draft_area. 566 file_prepare_draft_area($draftid, $contextid, 'question', 'response_' . $name, null); 567 return $draftid; 568 } 569 570 /** 571 * Get the latest value of a particular behaviour variable. That is, 572 * get the value from the latest step that has it set. Return null if it is 573 * not set in any step. 574 * 575 * @param string $name the name of the variable to get. 576 * @param mixed default the value to return in the variable has never been set. 577 * (Optional, defaults to null.) 578 * @return mixed string value, or $default if it has never been set. 579 */ 580 public function get_last_behaviour_var($name, $default = null) { 581 foreach ($this->get_reverse_step_iterator() as $step) { 582 if ($step->has_behaviour_var($name)) { 583 return $step->get_behaviour_var($name); 584 } 585 } 586 return $default; 587 } 588 589 /** 590 * Get the current state of this question attempt. That is, the state of the 591 * latest step. 592 * @return question_state 593 */ 594 public function get_state() { 595 return $this->get_last_step()->get_state(); 596 } 597 598 /** 599 * @param bool $showcorrectness Whether right/partial/wrong states should 600 * be distinguised. 601 * @return string A brief textual description of the current state. 602 */ 603 public function get_state_string($showcorrectness) { 604 // Special case when attempt is based on previous one, see MDL-31226. 605 if ($this->get_num_steps() == 1 && $this->get_state() == question_state::$complete) { 606 return get_string('notchanged', 'question'); 607 } 608 return $this->behaviour->get_state_string($showcorrectness); 609 } 610 611 /** 612 * @param bool $showcorrectness Whether right/partial/wrong states should 613 * be distinguised. 614 * @return string a CSS class name for the current state. 615 */ 616 public function get_state_class($showcorrectness) { 617 return $this->get_state()->get_state_class($showcorrectness); 618 } 619 620 /** 621 * @return int the timestamp of the most recent step in this question attempt. 622 */ 623 public function get_last_action_time() { 624 return $this->get_last_step()->get_timecreated(); 625 } 626 627 /** 628 * Get the current fraction of this question attempt. That is, the fraction 629 * of the latest step, or null if this question has not yet been graded. 630 * @return number the current fraction. 631 */ 632 public function get_fraction() { 633 return $this->get_last_step()->get_fraction(); 634 } 635 636 /** @return bool whether this question attempt has a non-zero maximum mark. */ 637 public function has_marks() { 638 // Since grades are stored in the database as NUMBER(12,7). 639 return $this->maxmark >= 0.00000005; 640 } 641 642 /** 643 * @return number the current mark for this question. 644 * {@link get_fraction()} * {@link get_max_mark()}. 645 */ 646 public function get_mark() { 647 return $this->fraction_to_mark($this->get_fraction()); 648 } 649 650 /** 651 * This is used by the manual grading code, particularly in association with 652 * validation. If there is a mark submitted in the request, then use that, 653 * otherwise use the latest mark for this question. 654 * @return number the current manual mark for this question, formatted for display. 655 */ 656 public function get_current_manual_mark() { 657 $mark = $this->get_submitted_var($this->get_behaviour_field_name('mark'), PARAM_RAW_TRIMMED); 658 if (is_null($mark)) { 659 return format_float($this->get_mark(), 7, true, true); 660 } else { 661 return $mark; 662 } 663 } 664 665 /** 666 * @param number|null $fraction a fraction. 667 * @return number|null the corresponding mark. 668 */ 669 public function fraction_to_mark($fraction) { 670 if (is_null($fraction)) { 671 return null; 672 } 673 return $fraction * $this->maxmark; 674 } 675 676 /** 677 * @return float the maximum mark possible for this question attempt. 678 * In fact, this is not strictly the maximum, becuase get_max_fraction may 679 * return a number greater than 1. It might be better to think of this as a 680 * question weight. 681 */ 682 public function get_max_mark() { 683 return $this->maxmark; 684 } 685 686 /** @return float the maximum mark possible for this question attempt. */ 687 public function get_min_fraction() { 688 if (is_null($this->minfraction)) { 689 throw new coding_exception('This question_attempt has not been started yet, the min fraction is not yet known.'); 690 } 691 return $this->minfraction; 692 } 693 694 /** @return float the maximum mark possible for this question attempt. */ 695 public function get_max_fraction() { 696 if (is_null($this->maxfraction)) { 697 throw new coding_exception('This question_attempt has not been started yet, the max fraction is not yet known.'); 698 } 699 return $this->maxfraction; 700 } 701 702 /** 703 * The current mark, formatted to the stated number of decimal places. Uses 704 * {@link format_float()} to format floats according to the current locale. 705 * @param int $dp number of decimal places. 706 * @return string formatted mark. 707 */ 708 public function format_mark($dp) { 709 return $this->format_fraction_as_mark($this->get_fraction(), $dp); 710 } 711 712 /** 713 * The current mark, formatted to the stated number of decimal places. Uses 714 * {@link format_float()} to format floats according to the current locale. 715 * @param int $dp number of decimal places. 716 * @return string formatted mark. 717 */ 718 public function format_fraction_as_mark($fraction, $dp) { 719 return format_float($this->fraction_to_mark($fraction), $dp); 720 } 721 722 /** 723 * The maximum mark for this question attempt, formatted to the stated number 724 * of decimal places. Uses {@link format_float()} to format floats according 725 * to the current locale. 726 * @param int $dp number of decimal places. 727 * @return string formatted maximum mark. 728 */ 729 public function format_max_mark($dp) { 730 return format_float($this->maxmark, $dp); 731 } 732 733 /** 734 * Return the hint that applies to the question in its current state, or null. 735 * @return question_hint|null 736 */ 737 public function get_applicable_hint() { 738 return $this->behaviour->get_applicable_hint(); 739 } 740 741 /** 742 * Produce a plain-text summary of what the user did during a step. 743 * @param question_attempt_step $step the step in quetsion. 744 * @return string a summary of what was done during that step. 745 */ 746 public function summarise_action(question_attempt_step $step) { 747 return $this->behaviour->summarise_action($step); 748 } 749 750 /** 751 * Return one of the bits of metadata for a this question attempt. 752 * @param string $name the name of the metadata variable to return. 753 * @return string the value of that metadata variable. 754 */ 755 public function get_metadata($name) { 756 return $this->get_step(0)->get_metadata_var($name); 757 } 758 759 /** 760 * Set some metadata for this question attempt. 761 * @param string $name the name of the metadata variable to return. 762 * @param string $value the value to set that metadata variable to. 763 */ 764 public function set_metadata($name, $value) { 765 $firststep = $this->get_step(0); 766 if (!$firststep->has_metadata_var($name)) { 767 $this->observer->notify_metadata_added($this, $name); 768 } else if ($value !== $firststep->get_metadata_var($name)) { 769 $this->observer->notify_metadata_modified($this, $name); 770 } 771 $firststep->set_metadata_var($name, $value); 772 } 773 774 /** 775 * Helper function used by {@link rewrite_pluginfile_urls()} and 776 * {@link rewrite_response_pluginfile_urls()}. 777 * @return array ids that need to go into the file paths. 778 */ 779 protected function extra_file_path_components() { 780 return array($this->get_usage_id(), $this->get_slot()); 781 } 782 783 /** 784 * Calls {@link question_rewrite_question_urls()} with appropriate parameters 785 * for content belonging to this question. 786 * @param string $text the content to output. 787 * @param string $component the component name (normally 'question' or 'qtype_...') 788 * @param string $filearea the name of the file area. 789 * @param int $itemid the item id. 790 * @return srting the content with the URLs rewritten. 791 */ 792 public function rewrite_pluginfile_urls($text, $component, $filearea, $itemid) { 793 return question_rewrite_question_urls($text, 'pluginfile.php', 794 $this->question->contextid, $component, $filearea, 795 $this->extra_file_path_components(), $itemid); 796 } 797 798 /** 799 * Calls {@link question_rewrite_question_urls()} with appropriate parameters 800 * for content belonging to responses to this question. 801 * 802 * @param string $text the text to update the URLs in. 803 * @param int $contextid the id of the context the quba belongs to. 804 * @param string $name the variable name the files belong to. 805 * @param question_attempt_step $step the step the response is coming from. 806 * @return srting the content with the URLs rewritten. 807 */ 808 public function rewrite_response_pluginfile_urls($text, $contextid, $name, 809 question_attempt_step $step) { 810 return $step->rewrite_response_pluginfile_urls($text, $contextid, $name, 811 $this->extra_file_path_components()); 812 } 813 814 /** 815 * Get the {@link core_question_renderer}, in collaboration with appropriate 816 * {@link qbehaviour_renderer} and {@link qtype_renderer} subclasses, to generate the 817 * HTML to display this question attempt in its current state. 818 * @param question_display_options $options controls how the question is rendered. 819 * @param string|null $number The question number to display. 820 * @return string HTML fragment representing the question. 821 */ 822 public function render($options, $number, $page = null) { 823 if (is_null($page)) { 824 global $PAGE; 825 $page = $PAGE; 826 } 827 $qoutput = $page->get_renderer('core', 'question'); 828 $qtoutput = $this->question->get_renderer($page); 829 return $this->behaviour->render($options, $number, $qoutput, $qtoutput); 830 } 831 832 /** 833 * Generate any bits of HTML that needs to go in the <head> tag when this question 834 * attempt is displayed in the body. 835 * @return string HTML fragment. 836 */ 837 public function render_head_html($page = null) { 838 if (is_null($page)) { 839 global $PAGE; 840 $page = $PAGE; 841 } 842 // TODO go via behaviour. 843 return $this->question->get_renderer($page)->head_code($this) . 844 $this->behaviour->get_renderer($page)->head_code($this); 845 } 846 847 /** 848 * Like {@link render_question()} but displays the question at the past step 849 * indicated by $seq, rather than showing the latest step. 850 * 851 * @param int $seq the seq number of the past state to display. 852 * @param question_display_options $options controls how the question is rendered. 853 * @param string|null $number The question number to display. 'i' is a special 854 * value that gets displayed as Information. Null means no number is displayed. 855 * @return string HTML fragment representing the question. 856 */ 857 public function render_at_step($seq, $options, $number, $preferredbehaviour) { 858 $restrictedqa = new question_attempt_with_restricted_history($this, $seq, $preferredbehaviour); 859 return $restrictedqa->render($options, $number); 860 } 861 862 /** 863 * Checks whether the users is allow to be served a particular file. 864 * @param question_display_options $options the options that control display of the question. 865 * @param string $component the name of the component we are serving files for. 866 * @param string $filearea the name of the file area. 867 * @param array $args the remaining bits of the file path. 868 * @param bool $forcedownload whether the user must be forced to download the file. 869 * @return bool true if the user can access this file. 870 */ 871 public function check_file_access($options, $component, $filearea, $args, $forcedownload) { 872 return $this->behaviour->check_file_access($options, $component, $filearea, $args, $forcedownload); 873 } 874 875 /** 876 * Add a step to this question attempt. 877 * @param question_attempt_step $step the new step. 878 */ 879 protected function add_step(question_attempt_step $step) { 880 $this->steps[] = $step; 881 end($this->steps); 882 $this->observer->notify_step_added($step, $this, key($this->steps)); 883 } 884 885 /** 886 * Add an auto-saved step to this question attempt. We mark auto-saved steps by 887 * changing saving the step number with a - sign. 888 * @param question_attempt_step $step the new step. 889 */ 890 protected function add_autosaved_step(question_attempt_step $step) { 891 $this->steps[] = $step; 892 $this->autosavedstep = $step; 893 end($this->steps); 894 $this->observer->notify_step_added($step, $this, -key($this->steps)); 895 } 896 897 /** 898 * Discard any auto-saved data belonging to this question attempt. 899 */ 900 public function discard_autosaved_step() { 901 if (!$this->has_autosaved_step()) { 902 return; 903 } 904 905 $autosaved = array_pop($this->steps); 906 $this->autosavedstep = null; 907 $this->observer->notify_step_deleted($autosaved, $this); 908 } 909 910 /** 911 * If there is an autosaved step, convert it into a real save, so that it 912 * is preserved. 913 */ 914 protected function convert_autosaved_step_to_real_step() { 915 if ($this->autosavedstep === null) { 916 return; 917 } 918 919 $laststep = end($this->steps); 920 if ($laststep !== $this->autosavedstep) { 921 throw new coding_exception('Cannot convert autosaved step to real step, since other steps have been added.'); 922 } 923 924 $this->observer->notify_step_modified($this->autosavedstep, $this, key($this->steps)); 925 $this->autosavedstep = null; 926 } 927 928 /** 929 * Use a strategy to pick a variant. 930 * @param question_variant_selection_strategy $variantstrategy a strategy. 931 * @return int the selected variant. 932 */ 933 public function select_variant(question_variant_selection_strategy $variantstrategy) { 934 return $variantstrategy->choose_variant($this->get_question()->get_num_variants(), 935 $this->get_question()->get_variants_selection_seed()); 936 } 937 938 /** 939 * Start this question attempt. 940 * 941 * You should not call this method directly. Call 942 * {@link question_usage_by_activity::start_question()} instead. 943 * 944 * @param string|question_behaviour $preferredbehaviour the name of the 945 * desired archetypal behaviour, or an actual model instance. 946 * @param int $variant the variant of the question to start. Between 1 and 947 * $this->get_question()->get_num_variants() inclusive. 948 * @param array $submitteddata optional, used when re-starting to keep the same initial state. 949 * @param int $timestamp optional, the timstamp to record for this action. Defaults to now. 950 * @param int $userid optional, the user to attribute this action to. Defaults to the current user. 951 * @param int $existingstepid optional, if this step is going to replace an existing step 952 * (for example, during a regrade) this is the id of the previous step we are replacing. 953 */ 954 public function start($preferredbehaviour, $variant, $submitteddata = array(), 955 $timestamp = null, $userid = null, $existingstepid = null) { 956 957 if ($this->get_num_steps() > 0) { 958 throw new coding_exception('Cannot start a question that is already started.'); 959 } 960 961 // Initialise the behaviour. 962 $this->variant = $variant; 963 if (is_string($preferredbehaviour)) { 964 $this->behaviour = 965 $this->question->make_behaviour($this, $preferredbehaviour); 966 } else { 967 $class = get_class($preferredbehaviour); 968 $this->behaviour = new $class($this, $preferredbehaviour); 969 } 970 971 // Record the minimum and maximum fractions. 972 $this->minfraction = $this->behaviour->get_min_fraction(); 973 $this->maxfraction = $this->behaviour->get_max_fraction(); 974 975 // Initialise the first step. 976 $firststep = new question_attempt_step($submitteddata, $timestamp, $userid, $existingstepid); 977 if ($submitteddata) { 978 $firststep->set_state(question_state::$complete); 979 $this->behaviour->apply_attempt_state($firststep); 980 } else { 981 $this->behaviour->init_first_step($firststep, $variant); 982 } 983 $this->add_step($firststep); 984 985 // Record questionline and correct answer. 986 $this->questionsummary = $this->behaviour->get_question_summary(); 987 $this->rightanswer = $this->behaviour->get_right_answer_summary(); 988 } 989 990 /** 991 * Start this question attempt, starting from the point that the previous 992 * attempt $oldqa had reached. 993 * 994 * You should not call this method directly. Call 995 * {@link question_usage_by_activity::start_question_based_on()} instead. 996 * 997 * @param question_attempt $oldqa a previous attempt at this quetsion that 998 * defines the starting point. 999 */ 1000 public function start_based_on(question_attempt $oldqa) { 1001 $this->start($oldqa->behaviour, $oldqa->get_variant(), $oldqa->get_resume_data()); 1002 } 1003 1004 /** 1005 * Used by {@link start_based_on()} to get the data needed to start a new 1006 * attempt from the point this attempt has go to. 1007 * @return array name => value pairs. 1008 */ 1009 protected function get_resume_data() { 1010 $resumedata = $this->behaviour->get_resume_data(); 1011 foreach ($resumedata as $name => $value) { 1012 if ($value instanceof question_file_loader) { 1013 $resumedata[$name] = $value->get_question_file_saver(); 1014 } 1015 } 1016 return $resumedata; 1017 } 1018 1019 /** 1020 * Get a particular parameter from the current request. A wrapper round 1021 * {@link optional_param()}, except that the results is returned without 1022 * slashes. 1023 * @param string $name the paramter name. 1024 * @param int $type one of the standard PARAM_... constants, or one of the 1025 * special extra constands defined by this class. 1026 * @param array $postdata (optional, only inteded for testing use) take the 1027 * data from this array, instead of from $_POST. 1028 * @return mixed the requested value. 1029 */ 1030 public function get_submitted_var($name, $type, $postdata = null) { 1031 switch ($type) { 1032 1033 case self::PARAM_FILES: 1034 return $this->process_response_files($name, $name, $postdata); 1035 1036 case self::PARAM_RAW_FILES: 1037 $var = $this->get_submitted_var($name, PARAM_RAW, $postdata); 1038 return $this->process_response_files($name, $name . ':itemid', $postdata, $var); 1039 1040 default: 1041 if (is_null($postdata)) { 1042 $var = optional_param($name, null, $type); 1043 } else if (array_key_exists($name, $postdata)) { 1044 $var = clean_param($postdata[$name], $type); 1045 } else { 1046 $var = null; 1047 } 1048 1049 return $var; 1050 } 1051 } 1052 1053 /** 1054 * Validate the manual mark for a question. 1055 * @param unknown $currentmark the user input (e.g. '1,0', '1,0' or 'invalid'. 1056 * @return string any errors with the value, or '' if it is OK. 1057 */ 1058 public function validate_manual_mark($currentmark) { 1059 if ($currentmark === null || $currentmark === '') { 1060 return ''; 1061 } 1062 1063 $mark = question_utils::clean_param_mark($currentmark); 1064 if ($mark === null) { 1065 return get_string('manualgradeinvalidformat', 'question'); 1066 } 1067 1068 $maxmark = $this->get_max_mark(); 1069 if ($mark > $maxmark * $this->get_max_fraction() || $mark < $maxmark * $this->get_min_fraction()) { 1070 return get_string('manualgradeoutofrange', 'question'); 1071 } 1072 1073 return ''; 1074 } 1075 1076 /** 1077 * Handle a submitted variable representing uploaded files. 1078 * @param string $name the field name. 1079 * @param string $draftidname the field name holding the draft file area id. 1080 * @param array $postdata (optional, only inteded for testing use) take the 1081 * data from this array, instead of from $_POST. At the moment, this 1082 * behaves as if there were no files. 1083 * @param string $text optional reponse text. 1084 * @return question_file_saver that can be used to save the files later. 1085 */ 1086 protected function process_response_files($name, $draftidname, $postdata = null, $text = null) { 1087 if ($postdata) { 1088 // For simulated posts, get the draft itemid from there. 1089 $draftitemid = $this->get_submitted_var($draftidname, PARAM_INT, $postdata); 1090 } else { 1091 $draftitemid = file_get_submitted_draft_itemid($draftidname); 1092 } 1093 1094 if (!$draftitemid) { 1095 return null; 1096 } 1097 1098 return new question_file_saver($draftitemid, 'question', 'response_' . 1099 str_replace($this->get_field_prefix(), '', $name), $text); 1100 } 1101 1102 /** 1103 * Get any data from the request that matches the list of expected params. 1104 * @param array $expected variable name => PARAM_... constant. 1105 * @param string $extraprefix '-' or ''. 1106 * @return array name => value. 1107 */ 1108 protected function get_expected_data($expected, $postdata, $extraprefix) { 1109 $submitteddata = array(); 1110 foreach ($expected as $name => $type) { 1111 $value = $this->get_submitted_var( 1112 $this->get_field_prefix() . $extraprefix . $name, $type, $postdata); 1113 if (!is_null($value)) { 1114 $submitteddata[$extraprefix . $name] = $value; 1115 } 1116 } 1117 return $submitteddata; 1118 } 1119 1120 /** 1121 * Get all the submitted question type data for this question, whithout checking 1122 * that it is valid or cleaning it in any way. 1123 * @return array name => value. 1124 */ 1125 public function get_all_submitted_qt_vars($postdata) { 1126 if (is_null($postdata)) { 1127 $postdata = $_POST; 1128 } 1129 1130 $pattern = '/^' . preg_quote($this->get_field_prefix(), '/') . '[^-:]/'; 1131 $prefixlen = strlen($this->get_field_prefix()); 1132 1133 $submitteddata = array(); 1134 foreach ($postdata as $name => $value) { 1135 if (preg_match($pattern, $name)) { 1136 $submitteddata[substr($name, $prefixlen)] = $value; 1137 } 1138 } 1139 1140 return $submitteddata; 1141 } 1142 1143 /** 1144 * Get all the sumbitted data belonging to this question attempt from the 1145 * current request. 1146 * @param array $postdata (optional, only inteded for testing use) take the 1147 * data from this array, instead of from $_POST. 1148 * @return array name => value pairs that could be passed to {@link process_action()}. 1149 */ 1150 public function get_submitted_data($postdata = null) { 1151 $submitteddata = $this->get_expected_data( 1152 $this->behaviour->get_expected_data(), $postdata, '-'); 1153 1154 $expected = $this->behaviour->get_expected_qt_data(); 1155 if ($expected === self::USE_RAW_DATA) { 1156 $submitteddata += $this->get_all_submitted_qt_vars($postdata); 1157 } else { 1158 $submitteddata += $this->get_expected_data($expected, $postdata, ''); 1159 } 1160 return $submitteddata; 1161 } 1162 1163 /** 1164 * Get a set of response data for this question attempt that would get the 1165 * best possible mark. If it is not possible to compute a correct 1166 * response, this method should return null. 1167 * @return array|null name => value pairs that could be passed to {@link process_action()}. 1168 */ 1169 public function get_correct_response() { 1170 $response = $this->question->get_correct_response(); 1171 if (is_null($response)) { 1172 return null; 1173 } 1174 $imvars = $this->behaviour->get_correct_response(); 1175 foreach ($imvars as $name => $value) { 1176 $response['-' . $name] = $value; 1177 } 1178 return $response; 1179 } 1180 1181 /** 1182 * Change the quetsion summary. Note, that this is almost never necessary. 1183 * This method was only added to work around a limitation of the Opaque 1184 * protocol, which only sends questionLine at the end of an attempt. 1185 * @param $questionsummary the new summary to set. 1186 */ 1187 public function set_question_summary($questionsummary) { 1188 $this->questionsummary = $questionsummary; 1189 $this->observer->notify_attempt_modified($this); 1190 } 1191 1192 /** 1193 * @return string a simple textual summary of the question that was asked. 1194 */ 1195 public function get_question_summary() { 1196 return $this->questionsummary; 1197 } 1198 1199 /** 1200 * @return string a simple textual summary of response given. 1201 */ 1202 public function get_response_summary() { 1203 return $this->responsesummary; 1204 } 1205 1206 /** 1207 * @return string a simple textual summary of the correct resonse. 1208 */ 1209 public function get_right_answer_summary() { 1210 return $this->rightanswer; 1211 } 1212 1213 /** 1214 * Whether this attempt at this question could be completed just by the 1215 * student interacting with the question, before {@link finish()} is called. 1216 * 1217 * @return boolean whether this attempt can finish naturally. 1218 */ 1219 public function can_finish_during_attempt() { 1220 return $this->behaviour->can_finish_during_attempt(); 1221 } 1222 1223 /** 1224 * Perform the action described by $submitteddata. 1225 * @param array $submitteddata the submitted data the determines the action. 1226 * @param int $timestamp the time to record for the action. (If not given, use now.) 1227 * @param int $userid the user to attribute the action to. (If not given, use the current user.) 1228 * @param int $existingstepid used by the regrade code. 1229 */ 1230 public function process_action($submitteddata, $timestamp = null, $userid = null, $existingstepid = null) { 1231 $pendingstep = new question_attempt_pending_step($submitteddata, $timestamp, $userid, $existingstepid); 1232 $this->discard_autosaved_step(); 1233 if ($this->behaviour->process_action($pendingstep) == self::KEEP) { 1234 $this->add_step($pendingstep); 1235 if ($pendingstep->response_summary_changed()) { 1236 $this->responsesummary = $pendingstep->get_new_response_summary(); 1237 } 1238 if ($pendingstep->variant_number_changed()) { 1239 $this->variant = $pendingstep->get_new_variant_number(); 1240 } 1241 } 1242 } 1243 1244 /** 1245 * Process an autosave. 1246 * @param array $submitteddata the submitted data the determines the action. 1247 * @param int $timestamp the time to record for the action. (If not given, use now.) 1248 * @param int $userid the user to attribute the action to. (If not given, use the current user.) 1249 * @return bool whether anything was saved. 1250 */ 1251 public function process_autosave($submitteddata, $timestamp = null, $userid = null) { 1252 $pendingstep = new question_attempt_pending_step($submitteddata, $timestamp, $userid); 1253 if ($this->behaviour->process_autosave($pendingstep) == self::KEEP) { 1254 $this->add_autosaved_step($pendingstep); 1255 return true; 1256 } 1257 return false; 1258 } 1259 1260 /** 1261 * Perform a finish action on this question attempt. This corresponds to an 1262 * external finish action, for example the user pressing Submit all and finish 1263 * in the quiz, rather than using one of the controls that is part of the 1264 * question. 1265 * 1266 * @param int $timestamp the time to record for the action. (If not given, use now.) 1267 * @param int $userid the user to attribute the aciton to. (If not given, use the current user.) 1268 */ 1269 public function finish($timestamp = null, $userid = null) { 1270 $this->convert_autosaved_step_to_real_step(); 1271 $this->process_action(array('-finish' => 1), $timestamp, $userid); 1272 } 1273 1274 /** 1275 * Perform a regrade. This replays all the actions from $oldqa into this 1276 * attempt. 1277 * @param question_attempt $oldqa the attempt to regrade. 1278 * @param bool $finished whether the question attempt should be forced to be finished 1279 * after the regrade, or whether it may still be in progress (default false). 1280 */ 1281 public function regrade(question_attempt $oldqa, $finished) { 1282 $first = true; 1283 foreach ($oldqa->get_step_iterator() as $step) { 1284 $this->observer->notify_step_deleted($step, $this); 1285 1286 if ($first) { 1287 // First step of the attempt. 1288 $first = false; 1289 $this->start($oldqa->behaviour, $oldqa->get_variant(), $step->get_all_data(), 1290 $step->get_timecreated(), $step->get_user_id(), $step->get_id()); 1291 1292 } else if ($step->has_behaviour_var('finish') && count($step->get_submitted_data()) > 1) { 1293 // This case relates to MDL-32062. The upgrade code from 2.0 1294 // generates attempts where the final submit of the question 1295 // data, and the finish action, are in the same step. The system 1296 // cannot cope with that, so convert the single old step into 1297 // two new steps. 1298 $submitteddata = $step->get_submitted_data(); 1299 unset($submitteddata['-finish']); 1300 $this->process_action($submitteddata, 1301 $step->get_timecreated(), $step->get_user_id(), $step->get_id()); 1302 $this->finish($step->get_timecreated(), $step->get_user_id()); 1303 1304 } else { 1305 // This is the normal case. Replay the next step of the attempt. 1306 $this->process_action($step->get_submitted_data(), 1307 $step->get_timecreated(), $step->get_user_id(), $step->get_id()); 1308 } 1309 } 1310 1311 if ($finished) { 1312 $this->finish(); 1313 } 1314 } 1315 1316 /** 1317 * Change the max mark for this question_attempt. 1318 * @param float $maxmark the new max mark. 1319 */ 1320 public function set_max_mark($maxmark) { 1321 $this->maxmark = $maxmark; 1322 $this->observer->notify_attempt_modified($this); 1323 } 1324 1325 /** 1326 * Perform a manual grading action on this attempt. 1327 * @param string $comment the comment being added. 1328 * @param float $mark the new mark. If null, then only a comment is added. 1329 * @param int $commentformat the FORMAT_... for $comment. Must be given. 1330 * @param int $timestamp the time to record for the action. (If not given, use now.) 1331 * @param int $userid the user to attribute the aciton to. (If not given, use the current user.) 1332 */ 1333 public function manual_grade($comment, $mark, $commentformat = null, $timestamp = null, $userid = null) { 1334 $submitteddata = array('-comment' => $comment); 1335 if (is_null($commentformat)) { 1336 debugging('You should pass $commentformat to manual_grade.', DEBUG_DEVELOPER); 1337 $commentformat = FORMAT_HTML; 1338 } 1339 $submitteddata['-commentformat'] = $commentformat; 1340 if (!is_null($mark)) { 1341 $submitteddata['-mark'] = $mark; 1342 $submitteddata['-maxmark'] = $this->maxmark; 1343 } 1344 $this->process_action($submitteddata, $timestamp, $userid); 1345 } 1346 1347 /** @return bool Whether this question attempt has had a manual comment added. */ 1348 public function has_manual_comment() { 1349 foreach ($this->steps as $step) { 1350 if ($step->has_behaviour_var('comment')) { 1351 return true; 1352 } 1353 } 1354 return false; 1355 } 1356 1357 /** 1358 * @return array(string, int) the most recent manual comment that was added 1359 * to this question, and the FORMAT_... it is. 1360 */ 1361 public function get_manual_comment() { 1362 foreach ($this->get_reverse_step_iterator() as $step) { 1363 if ($step->has_behaviour_var('comment')) { 1364 return array($step->get_behaviour_var('comment'), 1365 $step->get_behaviour_var('commentformat')); 1366 } 1367 } 1368 return array(null, null); 1369 } 1370 1371 /** 1372 * This is used by the manual grading code, particularly in association with 1373 * validation. If there is a comment submitted in the request, then use that, 1374 * otherwise use the latest comment for this question. 1375 * @return number the current mark for this question. 1376 * {@link get_fraction()} * {@link get_max_mark()}. 1377 */ 1378 public function get_current_manual_comment() { 1379 $comment = $this->get_submitted_var($this->get_behaviour_field_name('comment'), PARAM_RAW); 1380 if (is_null($comment)) { 1381 return $this->get_manual_comment(); 1382 } else { 1383 $commentformat = $this->get_submitted_var( 1384 $this->get_behaviour_field_name('commentformat'), PARAM_INT); 1385 if ($commentformat === null) { 1386 $commentformat = FORMAT_HTML; 1387 } 1388 return array($comment, $commentformat); 1389 } 1390 } 1391 1392 /** 1393 * Break down a student response by sub part and classification. See also {@link question::classify_response}. 1394 * Used for response analysis. 1395 * 1396 * @param string $whichtries which tries to analyse for response analysis. Will be one of 1397 * question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES. 1398 * Defaults to question_attempt::LAST_TRY. 1399 * @return (question_classified_response|array)[] If $whichtries is question_attempt::FIRST_TRY or LAST_TRY index is subpartid 1400 * and values are question_classified_response instances. 1401 * If $whichtries is question_attempt::ALL_TRIES then first key is submitted response no 1402 * and the second key is subpartid. 1403 */ 1404 public function classify_response($whichtries = self::LAST_TRY) { 1405 return $this->behaviour->classify_response($whichtries); 1406 } 1407 1408 /** 1409 * Create a question_attempt_step from records loaded from the database. 1410 * 1411 * For internal use only. 1412 * 1413 * @param Iterator $records Raw records loaded from the database. 1414 * @param int $questionattemptid The id of the question_attempt to extract. 1415 * @return question_attempt The newly constructed question_attempt. 1416 */ 1417 public static function load_from_records($records, $questionattemptid, 1418 question_usage_observer $observer, $preferredbehaviour) { 1419 $record = $records->current(); 1420 while ($record->questionattemptid != $questionattemptid) { 1421 $record = $records->next(); 1422 if (!$records->valid()) { 1423 throw new coding_exception("Question attempt {$questionattemptid} not found in the database."); 1424 } 1425 $record = $records->current(); 1426 } 1427 1428 try { 1429 $question = question_bank::load_question($record->questionid); 1430 } catch (Exception $e) { 1431 // The question must have been deleted somehow. Create a missing 1432 // question to use in its place. 1433 $question = question_bank::get_qtype('missingtype')->make_deleted_instance( 1434 $record->questionid, $record->maxmark + 0); 1435 } 1436 1437 $qa = new question_attempt($question, $record->questionusageid, 1438 null, $record->maxmark + 0); 1439 $qa->set_database_id($record->questionattemptid); 1440 $qa->set_slot($record->slot); 1441 $qa->variant = $record->variant + 0; 1442 $qa->minfraction = $record->minfraction + 0; 1443 $qa->maxfraction = $record->maxfraction + 0; 1444 $qa->set_flagged($record->flagged); 1445 $qa->questionsummary = $record->questionsummary; 1446 $qa->rightanswer = $record->rightanswer; 1447 $qa->responsesummary = $record->responsesummary; 1448 $qa->timemodified = $record->timemodified; 1449 1450 $qa->behaviour = question_engine::make_behaviour( 1451 $record->behaviour, $qa, $preferredbehaviour); 1452 $qa->observer = $observer; 1453 1454 // If attemptstepid is null (which should not happen, but has happened 1455 // due to corrupt data, see MDL-34251) then the current pointer in $records 1456 // will not be advanced in the while loop below, and we get stuck in an 1457 // infinite loop, since this method is supposed to always consume at 1458 // least one record. Therefore, in this case, advance the record here. 1459 if (is_null($record->attemptstepid)) { 1460 $records->next(); 1461 } 1462 1463 $i = 0; 1464 $autosavedstep = null; 1465 $autosavedsequencenumber = null; 1466 while ($record && $record->questionattemptid == $questionattemptid && !is_null($record->attemptstepid)) { 1467 $sequencenumber = $record->sequencenumber; 1468 $nextstep = question_attempt_step::load_from_records($records, $record->attemptstepid, $qa->get_question()->get_type_name()); 1469 1470 if ($sequencenumber < 0) { 1471 if (!$autosavedstep) { 1472 $autosavedstep = $nextstep; 1473 $autosavedsequencenumber = -$sequencenumber; 1474 } else { 1475 // Old redundant data. Mark it for deletion. 1476 $qa->observer->notify_step_deleted($nextstep, $qa); 1477 } 1478 } else { 1479 $qa->steps[$i] = $nextstep; 1480 if ($i == 0) { 1481 $question->apply_attempt_state($qa->steps[0]); 1482 } 1483 $i++; 1484 } 1485 1486 if ($records->valid()) { 1487 $record = $records->current(); 1488 } else { 1489 $record = false; 1490 } 1491 } 1492 1493 if ($autosavedstep) { 1494 if ($autosavedsequencenumber >= $i) { 1495 $qa->autosavedstep = $autosavedstep; 1496 $qa->steps[$i] = $qa->autosavedstep; 1497 } else { 1498 $qa->observer->notify_step_deleted($autosavedstep, $qa); 1499 } 1500 } 1501 1502 return $qa; 1503 } 1504 1505 /** 1506 * Allow access to steps with responses submitted by students for grading in a question attempt. 1507 * 1508 * @return question_attempt_steps_with_submitted_response_iterator to access all steps with submitted data for questions that 1509 * allow multiple submissions that count towards grade, per attempt. 1510 */ 1511 public function get_steps_with_submitted_response_iterator() { 1512 return new question_attempt_steps_with_submitted_response_iterator($this); 1513 } 1514 } 1515 1516 1517 /** 1518 * This subclass of question_attempt pretends that only part of the step history 1519 * exists. It is used for rendering the question in past states. 1520 * 1521 * All methods that try to modify the question_attempt throw exceptions. 1522 * 1523 * @copyright 2010 The Open University 1524 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1525 */ 1526 class question_attempt_with_restricted_history extends question_attempt { 1527 /** 1528 * @var question_attempt the underlying question_attempt. 1529 */ 1530 protected $baseqa; 1531 1532 /** 1533 * Create a question_attempt_with_restricted_history 1534 * @param question_attempt $baseqa The question_attempt to make a restricted version of. 1535 * @param int $lastseq the index of the last step to include. 1536 * @param string $preferredbehaviour the preferred behaviour. It is slightly 1537 * annoyting that this needs to be passed, but unavoidable for now. 1538 */ 1539 public function __construct(question_attempt $baseqa, $lastseq, $preferredbehaviour) { 1540 $this->baseqa = $baseqa->get_full_qa(); 1541 1542 if ($lastseq < 0 || $lastseq >= $this->baseqa->get_num_steps()) { 1543 throw new coding_exception('$lastseq out of range', $lastseq); 1544 } 1545 1546 $this->steps = array_slice($this->baseqa->steps, 0, $lastseq + 1); 1547 $this->observer = new question_usage_null_observer(); 1548 1549 // This should be a straight copy of all the remaining fields. 1550 $this->id = $this->baseqa->id; 1551 $this->usageid = $this->baseqa->usageid; 1552 $this->slot = $this->baseqa->slot; 1553 $this->question = $this->baseqa->question; 1554 $this->maxmark = $this->baseqa->maxmark; 1555 $this->minfraction = $this->baseqa->minfraction; 1556 $this->maxfraction = $this->baseqa->maxfraction; 1557 $this->questionsummary = $this->baseqa->questionsummary; 1558 $this->responsesummary = $this->baseqa->responsesummary; 1559 $this->rightanswer = $this->baseqa->rightanswer; 1560 $this->flagged = $this->baseqa->flagged; 1561 1562 // Except behaviour, where we need to create a new one. 1563 $this->behaviour = question_engine::make_behaviour( 1564 $this->baseqa->get_behaviour_name(), $this, $preferredbehaviour); 1565 } 1566 1567 public function get_full_qa() { 1568 return $this->baseqa; 1569 } 1570 1571 public function get_full_step_iterator() { 1572 return $this->baseqa->get_step_iterator(); 1573 } 1574 1575 protected function add_step(question_attempt_step $step) { 1576 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 1577 } 1578 public function process_action($submitteddata, $timestamp = null, $userid = null, $existingstepid = null) { 1579 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 1580 } 1581 public function start($preferredbehaviour, $variant, $submitteddata = array(), $timestamp = null, $userid = null, $existingstepid = null) { 1582 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 1583 } 1584 1585 public function set_database_id($id) { 1586 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 1587 } 1588 public function set_flagged($flagged) { 1589 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 1590 } 1591 public function set_slot($slot) { 1592 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 1593 } 1594 public function set_question_summary($questionsummary) { 1595 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 1596 } 1597 public function set_usage_id($usageid) { 1598 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 1599 } 1600 } 1601 1602 1603 /** 1604 * A class abstracting access to the {@link question_attempt::$states} array. 1605 * 1606 * This is actively linked to question_attempt. If you add an new step 1607 * mid-iteration, then it will be included. 1608 * 1609 * @copyright 2009 The Open University 1610 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1611 */ 1612 class question_attempt_step_iterator implements Iterator, ArrayAccess { 1613 /** @var question_attempt the question_attempt being iterated over. */ 1614 protected $qa; 1615 /** @var integer records the current position in the iteration. */ 1616 protected $i; 1617 1618 /** 1619 * Do not call this constructor directly. 1620 * Use {@link question_attempt::get_step_iterator()}. 1621 * @param question_attempt $qa the attempt to iterate over. 1622 */ 1623 public function __construct(question_attempt $qa) { 1624 $this->qa = $qa; 1625 $this->rewind(); 1626 } 1627 1628 /** @return question_attempt_step */ 1629 public function current() { 1630 return $this->offsetGet($this->i); 1631 } 1632 /** @return int */ 1633 public function key() { 1634 return $this->i; 1635 } 1636 public function next() { 1637 ++$this->i; 1638 } 1639 public function rewind() { 1640 $this->i = 0; 1641 } 1642 /** @return bool */ 1643 public function valid() { 1644 return $this->offsetExists($this->i); 1645 } 1646 1647 /** @return bool */ 1648 public function offsetExists($i) { 1649 return $i >= 0 && $i < $this->qa->get_num_steps(); 1650 } 1651 /** @return question_attempt_step */ 1652 public function offsetGet($i) { 1653 return $this->qa->get_step($i); 1654 } 1655 public function offsetSet($offset, $value) { 1656 throw new coding_exception('You are only allowed read-only access to question_attempt::states through a question_attempt_step_iterator. Cannot set.'); 1657 } 1658 public function offsetUnset($offset) { 1659 throw new coding_exception('You are only allowed read-only access to question_attempt::states through a question_attempt_step_iterator. Cannot unset.'); 1660 } 1661 } 1662 1663 1664 /** 1665 * A variant of {@link question_attempt_step_iterator} that iterates through the 1666 * steps in reverse order. 1667 * 1668 * @copyright 2009 The Open University 1669 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1670 */ 1671 class question_attempt_reverse_step_iterator extends question_attempt_step_iterator { 1672 public function next() { 1673 --$this->i; 1674 } 1675 1676 public function rewind() { 1677 $this->i = $this->qa->get_num_steps() - 1; 1678 } 1679 } 1680 1681 /** 1682 * A variant of {@link question_attempt_step_iterator} that iterates through the 1683 * steps with submitted tries. 1684 * 1685 * @copyright 2014 The Open University 1686 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1687 */ 1688 class question_attempt_steps_with_submitted_response_iterator extends question_attempt_step_iterator implements Countable { 1689 1690 /** @var question_attempt the question_attempt being iterated over. */ 1691 protected $qa; 1692 1693 /** @var integer records the current position in the iteration. */ 1694 protected $submittedresponseno; 1695 1696 /** 1697 * Index is the submitted response number and value is the step no. 1698 * 1699 * @var int[] 1700 */ 1701 protected $stepswithsubmittedresponses; 1702 1703 /** 1704 * Do not call this constructor directly. 1705 * Use {@link question_attempt::get_submission_step_iterator()}. 1706 * @param question_attempt $qa the attempt to iterate over. 1707 */ 1708 public function __construct(question_attempt $qa) { 1709 $this->qa = $qa; 1710 $this->find_steps_with_submitted_response(); 1711 $this->rewind(); 1712 } 1713 1714 /** 1715 * Find the step nos in which a student has submitted a response. Including any step with a response that is saved before 1716 * the question attempt finishes. 1717 * 1718 * Called from constructor, should not be called from elsewhere. 1719 * 1720 */ 1721 protected function find_steps_with_submitted_response() { 1722 $stepnos = array(); 1723 $lastsavedstep = null; 1724 foreach ($this->qa->get_step_iterator() as $stepno => $step) { 1725 if ($this->qa->get_behaviour()->step_has_a_submitted_response($step)) { 1726 $stepnos[] = $stepno; 1727 $lastsavedstep = null; 1728 } else { 1729 $qtdata = $step->get_qt_data(); 1730 if (count($qtdata)) { 1731 $lastsavedstep = $stepno; 1732 } 1733 } 1734 } 1735 1736 if (!is_null($lastsavedstep)) { 1737 $stepnos[] = $lastsavedstep; 1738 } 1739 if (empty($stepnos)) { 1740 $this->stepswithsubmittedresponses = array(); 1741 } else { 1742 // Re-index array so index starts with 1. 1743 $this->stepswithsubmittedresponses = array_combine(range(1, count($stepnos)), $stepnos); 1744 } 1745 } 1746 1747 /** @return question_attempt_step */ 1748 public function current() { 1749 return $this->offsetGet($this->submittedresponseno); 1750 } 1751 /** @return int */ 1752 public function key() { 1753 return $this->submittedresponseno; 1754 } 1755 public function next() { 1756 ++$this->submittedresponseno; 1757 } 1758 public function rewind() { 1759 $this->submittedresponseno = 1; 1760 } 1761 /** @return bool */ 1762 public function valid() { 1763 return $this->submittedresponseno >= 1 && $this->submittedresponseno <= count($this->stepswithsubmittedresponses); 1764 } 1765 1766 /** 1767 * @param int $submittedresponseno 1768 * @return bool 1769 */ 1770 public function offsetExists($submittedresponseno) { 1771 return $submittedresponseno >= 1; 1772 } 1773 1774 /** 1775 * @param int $submittedresponseno 1776 * @return question_attempt_step 1777 */ 1778 public function offsetGet($submittedresponseno) { 1779 if ($submittedresponseno > count($this->stepswithsubmittedresponses)) { 1780 return null; 1781 } else { 1782 return $this->qa->get_step($this->step_no_for_try($submittedresponseno)); 1783 } 1784 } 1785 1786 /** 1787 * @return int the count of steps with tries. 1788 */ 1789 public function count() { 1790 return count($this->stepswithsubmittedresponses); 1791 } 1792 1793 /** 1794 * @param int $submittedresponseno 1795 * @throws coding_exception 1796 * @return int|null the step number or null if there is no such submitted response. 1797 */ 1798 public function step_no_for_try($submittedresponseno) { 1799 if (isset($this->stepswithsubmittedresponses[$submittedresponseno])) { 1800 return $this->stepswithsubmittedresponses[$submittedresponseno]; 1801 } else if ($submittedresponseno > count($this->stepswithsubmittedresponses)) { 1802 return null; 1803 } else { 1804 throw new coding_exception('Try number not found. It should be 1 or more.'); 1805 } 1806 } 1807 1808 public function offsetSet($offset, $value) { 1809 throw new coding_exception('You are only allowed read-only access to question_attempt::states '. 1810 'through a question_attempt_step_iterator. Cannot set.'); 1811 } 1812 public function offsetUnset($offset) { 1813 throw new coding_exception('You are only allowed read-only access to question_attempt::states '. 1814 'through a question_attempt_step_iterator. Cannot unset.'); 1815 } 1816 1817 }
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 |