[ 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 usage 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 * This class keeps track of a group of questions that are being attempted, 32 * and which state, and so on, each one is currently in. 33 * 34 * A quiz attempt or a lesson attempt could use an instance of this class to 35 * keep track of all the questions in the attempt and process student submissions. 36 * It is basically a collection of {@question_attempt} objects. 37 * 38 * The questions being attempted as part of this usage are identified by an integer 39 * that is passed into many of the methods as $slot. ($question->id is not 40 * used so that the same question can be used more than once in an attempt.) 41 * 42 * Normally, calling code should be able to do everything it needs to be calling 43 * methods of this class. You should not normally need to get individual 44 * {@question_attempt} objects and play around with their inner workind, in code 45 * that it outside the quetsion engine. 46 * 47 * Instances of this class correspond to rows in the question_usages table. 48 * 49 * @copyright 2009 The Open University 50 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 51 */ 52 class question_usage_by_activity { 53 /** 54 * @var integer|string the id for this usage. If this usage was loaded from 55 * the database, then this is the database id. Otherwise a unique random 56 * string is used. 57 */ 58 protected $id = null; 59 60 /** 61 * @var string name of an archetypal behaviour, that should be used 62 * by questions in this usage if possible. 63 */ 64 protected $preferredbehaviour = null; 65 66 /** @var context the context this usage belongs to. */ 67 protected $context; 68 69 /** @var string plugin name of the plugin this usage belongs to. */ 70 protected $owningcomponent; 71 72 /** @var array {@link question_attempt}s that make up this usage. */ 73 protected $questionattempts = array(); 74 75 /** @var question_usage_observer that tracks changes to this usage. */ 76 protected $observer; 77 78 /** 79 * Create a new instance. Normally, calling code should use 80 * {@link question_engine::make_questions_usage_by_activity()} or 81 * {@link question_engine::load_questions_usage_by_activity()} rather than 82 * calling this constructor directly. 83 * 84 * @param string $component the plugin creating this attempt. For example mod_quiz. 85 * @param object $context the context this usage belongs to. 86 */ 87 public function __construct($component, $context) { 88 $this->owningcomponent = $component; 89 $this->context = $context; 90 $this->observer = new question_usage_null_observer(); 91 } 92 93 /** 94 * @param string $behaviour the name of an archetypal behaviour, that should 95 * be used by questions in this usage if possible. 96 */ 97 public function set_preferred_behaviour($behaviour) { 98 $this->preferredbehaviour = $behaviour; 99 $this->observer->notify_modified(); 100 } 101 102 /** @return string the name of the preferred behaviour. */ 103 public function get_preferred_behaviour() { 104 return $this->preferredbehaviour; 105 } 106 107 /** @return context the context this usage belongs to. */ 108 public function get_owning_context() { 109 return $this->context; 110 } 111 112 /** @return string the name of the plugin that owns this attempt. */ 113 public function get_owning_component() { 114 return $this->owningcomponent; 115 } 116 117 /** @return int|string If this usage came from the database, then the id 118 * from the question_usages table is returned. Otherwise a random string is 119 * returned. */ 120 public function get_id() { 121 if (is_null($this->id)) { 122 $this->id = random_string(10); 123 } 124 return $this->id; 125 } 126 127 /** 128 * For internal use only. Used by {@link question_engine_data_mapper} to set 129 * the id when a usage is saved to the database. 130 * @param int $id the newly determined id for this usage. 131 */ 132 public function set_id_from_database($id) { 133 $this->id = $id; 134 foreach ($this->questionattempts as $qa) { 135 $qa->set_usage_id($id); 136 } 137 } 138 139 /** @return question_usage_observer that is tracking changes made to this usage. */ 140 public function get_observer() { 141 return $this->observer; 142 } 143 144 /** 145 * You should almost certainly not call this method from your code. It is for 146 * internal use only. 147 * @param question_usage_observer that should be used to tracking changes made to this usage. 148 */ 149 public function set_observer($observer) { 150 $this->observer = $observer; 151 foreach ($this->questionattempts as $qa) { 152 $qa->set_observer($observer); 153 } 154 } 155 156 /** 157 * Add another question to this usage. 158 * 159 * The added question is not started until you call {@link start_question()} 160 * on it. 161 * 162 * @param question_definition $question the question to add. 163 * @param number $maxmark the maximum this question will be marked out of in 164 * this attempt (optional). If not given, $question->defaultmark is used. 165 * @return int the number used to identify this question within this usage. 166 */ 167 public function add_question(question_definition $question, $maxmark = null) { 168 $qa = new question_attempt($question, $this->get_id(), $this->observer, $maxmark); 169 $qa->set_slot($this->next_slot_number()); 170 $this->questionattempts[$this->next_slot_number()] = $qa; 171 $this->observer->notify_attempt_added($qa); 172 return $qa->get_slot(); 173 } 174 175 /** 176 * Add another question to this usage, in the place of an existing slot. 177 * The question_attempt that was in that slot is moved to the end at a new 178 * slot number, which is returned. 179 * 180 * The added question is not started until you call {@link start_question()} 181 * on it. 182 * 183 * @param int $slot the slot-number of the question to replace. 184 * @param question_definition $question the question to add. 185 * @param number $maxmark the maximum this question will be marked out of in 186 * this attempt (optional). If not given, the max mark from the $qa we 187 * are replacing is used. 188 * @return int the new slot number of the question that was displaced. 189 */ 190 public function add_question_in_place_of_other($slot, question_definition $question, $maxmark = null) { 191 $newslot = $this->next_slot_number(); 192 193 $oldqa = $this->get_question_attempt($slot); 194 $oldqa->set_slot($newslot); 195 $this->questionattempts[$newslot] = $oldqa; 196 197 if ($maxmark === null) { 198 $maxmark = $oldqa->get_max_mark(); 199 } 200 201 $qa = new question_attempt($question, $this->get_id(), $this->observer, $maxmark); 202 $qa->set_slot($slot); 203 $this->questionattempts[$slot] = $qa; 204 205 $this->observer->notify_attempt_moved($oldqa, $slot); 206 $this->observer->notify_attempt_added($qa); 207 208 return $newslot; 209 } 210 211 /** 212 * The slot number that will be allotted to the next question added. 213 */ 214 public function next_slot_number() { 215 return count($this->questionattempts) + 1; 216 } 217 218 /** 219 * Get the question_definition for a question in this attempt. 220 * @param int $slot the number used to identify this question within this usage. 221 * @return question_definition the requested question object. 222 */ 223 public function get_question($slot) { 224 return $this->get_question_attempt($slot)->get_question(); 225 } 226 227 /** @return array all the identifying numbers of all the questions in this usage. */ 228 public function get_slots() { 229 return array_keys($this->questionattempts); 230 } 231 232 /** @return int the identifying number of the first question that was added to this usage. */ 233 public function get_first_question_number() { 234 reset($this->questionattempts); 235 return key($this->questionattempts); 236 } 237 238 /** @return int the number of questions that are currently in this usage. */ 239 public function question_count() { 240 return count($this->questionattempts); 241 } 242 243 /** 244 * Note the part of the {@link question_usage_by_activity} comment that explains 245 * that {@link question_attempt} objects should be considered part of the inner 246 * workings of the question engine, and should not, if possible, be accessed directly. 247 * 248 * @return question_attempt_iterator for iterating over all the questions being 249 * attempted. as part of this usage. 250 */ 251 public function get_attempt_iterator() { 252 return new question_attempt_iterator($this); 253 } 254 255 /** 256 * Check whether $number actually corresponds to a question attempt that is 257 * part of this usage. Throws an exception if not. 258 * 259 * @param int $slot a number allegedly identifying a question within this usage. 260 */ 261 protected function check_slot($slot) { 262 if (!array_key_exists($slot, $this->questionattempts)) { 263 throw new coding_exception('There is no question_attempt number ' . $slot . 264 ' in this attempt.'); 265 } 266 } 267 268 /** 269 * Note the part of the {@link question_usage_by_activity} comment that explains 270 * that {@link question_attempt} objects should be considered part of the inner 271 * workings of the question engine, and should not, if possible, be accessed directly. 272 * 273 * @param int $slot the number used to identify this question within this usage. 274 * @return question_attempt the corresponding {@link question_attempt} object. 275 */ 276 public function get_question_attempt($slot) { 277 $this->check_slot($slot); 278 return $this->questionattempts[$slot]; 279 } 280 281 /** 282 * Get the current state of the attempt at a question. 283 * @param int $slot the number used to identify this question within this usage. 284 * @return question_state. 285 */ 286 public function get_question_state($slot) { 287 return $this->get_question_attempt($slot)->get_state(); 288 } 289 290 /** 291 * @param int $slot the number used to identify this question within this usage. 292 * @param bool $showcorrectness Whether right/partial/wrong states should 293 * be distinguised. 294 * @return string A brief textual description of the current state. 295 */ 296 public function get_question_state_string($slot, $showcorrectness) { 297 return $this->get_question_attempt($slot)->get_state_string($showcorrectness); 298 } 299 300 /** 301 * @param int $slot the number used to identify this question within this usage. 302 * @param bool $showcorrectness Whether right/partial/wrong states should 303 * be distinguised. 304 * @return string a CSS class name for the current state. 305 */ 306 public function get_question_state_class($slot, $showcorrectness) { 307 return $this->get_question_attempt($slot)->get_state_class($showcorrectness); 308 } 309 310 /** 311 * Whether this attempt at a given question could be completed just by the 312 * student interacting with the question, before {@link finish_question()} is called. 313 * 314 * @param int $slot the number used to identify this question within this usage. 315 * @return boolean whether the attempt at the given question can finish naturally. 316 */ 317 public function can_question_finish_during_attempt($slot) { 318 return $this->get_question_attempt($slot)->can_finish_during_attempt(); 319 } 320 321 /** 322 * Get the time of the most recent action performed on a question. 323 * @param int $slot the number used to identify this question within this usage. 324 * @return int timestamp. 325 */ 326 public function get_question_action_time($slot) { 327 return $this->get_question_attempt($slot)->get_last_action_time(); 328 } 329 330 /** 331 * Get the current fraction awarded for the attempt at a question. 332 * @param int $slot the number used to identify this question within this usage. 333 * @return number|null The current fraction for this question, or null if one has 334 * not been assigned yet. 335 */ 336 public function get_question_fraction($slot) { 337 return $this->get_question_attempt($slot)->get_fraction(); 338 } 339 340 /** 341 * Get the current mark awarded for the attempt at a question. 342 * @param int $slot the number used to identify this question within this usage. 343 * @return number|null The current mark for this question, or null if one has 344 * not been assigned yet. 345 */ 346 public function get_question_mark($slot) { 347 return $this->get_question_attempt($slot)->get_mark(); 348 } 349 350 /** 351 * Get the maximum mark possible for the attempt at a question. 352 * @param int $slot the number used to identify this question within this usage. 353 * @return number the available marks for this question. 354 */ 355 public function get_question_max_mark($slot) { 356 return $this->get_question_attempt($slot)->get_max_mark(); 357 } 358 359 /** 360 * Get the total mark for all questions in this usage. 361 * @return number The sum of marks of all the question_attempts in this usage. 362 */ 363 public function get_total_mark() { 364 $mark = 0; 365 foreach ($this->questionattempts as $qa) { 366 if ($qa->get_max_mark() > 0 && $qa->get_state() == question_state::$needsgrading) { 367 return null; 368 } 369 $mark += $qa->get_mark(); 370 } 371 return $mark; 372 } 373 374 /** 375 * Get summary information about this usage. 376 * 377 * Some behaviours may be able to provide interesting summary information 378 * about the attempt as a whole, and this method provides access to that data. 379 * To see how this works, try setting a quiz to one of the CBM behaviours, 380 * and then look at the extra information displayed at the top of the quiz 381 * review page once you have sumitted an attempt. 382 * 383 * In the return value, the array keys are identifiers of the form 384 * qbehaviour_behaviourname_meaningfullkey. For qbehaviour_deferredcbm_highsummary. 385 * The values are arrays with two items, title and content. Each of these 386 * will be either a string, or a renderable. 387 * 388 * @return array as described above. 389 */ 390 public function get_summary_information(question_display_options $options) { 391 return question_engine::get_behaviour_type($this->preferredbehaviour) 392 ->summarise_usage($this, $options); 393 } 394 395 /** 396 * @return string a simple textual summary of the question that was asked. 397 */ 398 public function get_question_summary($slot) { 399 return $this->get_question_attempt($slot)->get_question_summary(); 400 } 401 402 /** 403 * @return string a simple textual summary of response given. 404 */ 405 public function get_response_summary($slot) { 406 return $this->get_question_attempt($slot)->get_response_summary(); 407 } 408 409 /** 410 * @return string a simple textual summary of the correct resonse. 411 */ 412 public function get_right_answer_summary($slot) { 413 return $this->get_question_attempt($slot)->get_right_answer_summary(); 414 } 415 416 /** 417 * Return one of the bits of metadata for a particular question attempt in 418 * this usage. 419 * @param int $slot the slot number of the question of inereest. 420 * @param string $name the name of the metadata variable to return. 421 * @return string the value of that metadata variable. 422 */ 423 public function get_question_attempt_metadata($slot, $name) { 424 return $this->get_question_attempt($slot)->get_metadata($name); 425 } 426 427 /** 428 * Set some metadata for a particular question attempt in this usage. 429 * @param int $slot the slot number of the question of inerest. 430 * @param string $name the name of the metadata variable to return. 431 * @param string $value the value to set that metadata variable to. 432 */ 433 public function set_question_attempt_metadata($slot, $name, $value) { 434 $this->get_question_attempt($slot)->set_metadata($name, $value); 435 } 436 437 /** 438 * Get the {@link core_question_renderer}, in collaboration with appropriate 439 * {@link qbehaviour_renderer} and {@link qtype_renderer} subclasses, to generate the 440 * HTML to display this question. 441 * @param int $slot the number used to identify this question within this usage. 442 * @param question_display_options $options controls how the question is rendered. 443 * @param string|null $number The question number to display. 'i' is a special 444 * value that gets displayed as Information. Null means no number is displayed. 445 * @return string HTML fragment representing the question. 446 */ 447 public function render_question($slot, $options, $number = null) { 448 $options->context = $this->context; 449 return $this->get_question_attempt($slot)->render($options, $number); 450 } 451 452 /** 453 * Generate any bits of HTML that needs to go in the <head> tag when this question 454 * is displayed in the body. 455 * @param int $slot the number used to identify this question within this usage. 456 * @return string HTML fragment. 457 */ 458 public function render_question_head_html($slot) { 459 //$options->context = $this->context; 460 return $this->get_question_attempt($slot)->render_head_html(); 461 } 462 463 /** 464 * Like {@link render_question()} but displays the question at the past step 465 * indicated by $seq, rather than showing the latest step. 466 * 467 * @param int $slot the number used to identify this question within this usage. 468 * @param int $seq the seq number of the past state to display. 469 * @param question_display_options $options controls how the question is rendered. 470 * @param string|null $number The question number to display. 'i' is a special 471 * value that gets displayed as Information. Null means no number is displayed. 472 * @return string HTML fragment representing the question. 473 */ 474 public function render_question_at_step($slot, $seq, $options, $number = null) { 475 $options->context = $this->context; 476 return $this->get_question_attempt($slot)->render_at_step( 477 $seq, $options, $number, $this->preferredbehaviour); 478 } 479 480 /** 481 * Checks whether the users is allow to be served a particular file. 482 * @param int $slot the number used to identify this question within this usage. 483 * @param question_display_options $options the options that control display of the question. 484 * @param string $component the name of the component we are serving files for. 485 * @param string $filearea the name of the file area. 486 * @param array $args the remaining bits of the file path. 487 * @param bool $forcedownload whether the user must be forced to download the file. 488 * @return bool true if the user can access this file. 489 */ 490 public function check_file_access($slot, $options, $component, $filearea, 491 $args, $forcedownload) { 492 return $this->get_question_attempt($slot)->check_file_access( 493 $options, $component, $filearea, $args, $forcedownload); 494 } 495 496 /** 497 * Replace a particular question_attempt with a different one. 498 * 499 * For internal use only. Used when reloading the state of a question from the 500 * database. 501 * 502 * @param array $records Raw records loaded from the database. 503 * @param int $questionattemptid The id of the question_attempt to extract. 504 * @return question_attempt The newly constructed question_attempt_step. 505 */ 506 public function replace_loaded_question_attempt_info($slot, $qa) { 507 $this->check_slot($slot); 508 $this->questionattempts[$slot] = $qa; 509 } 510 511 /** 512 * You should probably not use this method in code outside the question engine. 513 * The main reason for exposing it was for the benefit of unit tests. 514 * @param int $slot the number used to identify this question within this usage. 515 * @return string return the prefix that is pre-pended to field names in the HTML 516 * that is output. 517 */ 518 public function get_field_prefix($slot) { 519 return $this->get_question_attempt($slot)->get_field_prefix(); 520 } 521 522 /** 523 * Get the number of variants available for the question in this slot. 524 * @param int $slot the number used to identify this question within this usage. 525 * @return int the number of variants available. 526 */ 527 public function get_num_variants($slot) { 528 return $this->get_question_attempt($slot)->get_question()->get_num_variants(); 529 } 530 531 /** 532 * Get the variant of the question being used in a given slot. 533 * @param int $slot the number used to identify this question within this usage. 534 * @return int the variant of this question that is being used. 535 */ 536 public function get_variant($slot) { 537 return $this->get_question_attempt($slot)->get_variant(); 538 } 539 540 /** 541 * Start the attempt at a question that has been added to this usage. 542 * @param int $slot the number used to identify this question within this usage. 543 * @param int $variant which variant of the question to use. Must be between 544 * 1 and ->get_num_variants($slot) inclusive. If not give, a variant is 545 * chosen at random. 546 * @param int $timestamp optional, the timstamp to record for this action. Defaults to now. 547 */ 548 public function start_question($slot, $variant = null, $timenow = null) { 549 if (is_null($variant)) { 550 $variant = rand(1, $this->get_num_variants($slot)); 551 } 552 553 $qa = $this->get_question_attempt($slot); 554 $qa->start($this->preferredbehaviour, $variant, array(), $timenow); 555 $this->observer->notify_attempt_modified($qa); 556 } 557 558 /** 559 * Start the attempt at all questions that has been added to this usage. 560 * @param question_variant_selection_strategy how to pick which variant of each question to use. 561 * @param int $timestamp optional, the timstamp to record for this action. Defaults to now. 562 * @param int $userid optional, the user to attribute this action to. Defaults to the current user. 563 */ 564 public function start_all_questions(question_variant_selection_strategy $variantstrategy = null, 565 $timestamp = null, $userid = null) { 566 if (is_null($variantstrategy)) { 567 $variantstrategy = new question_variant_random_strategy(); 568 } 569 570 foreach ($this->questionattempts as $qa) { 571 $qa->start($this->preferredbehaviour, $qa->select_variant($variantstrategy)); 572 $this->observer->notify_attempt_modified($qa); 573 } 574 } 575 576 /** 577 * Start the attempt at a question, starting from the point where the previous 578 * question_attempt $oldqa had reached. This is used by the quiz 'Each attempt 579 * builds on last' mode. 580 * @param int $slot the number used to identify this question within this usage. 581 * @param question_attempt $oldqa a previous attempt at this quetsion that 582 * defines the starting point. 583 */ 584 public function start_question_based_on($slot, question_attempt $oldqa) { 585 $qa = $this->get_question_attempt($slot); 586 $qa->start_based_on($oldqa); 587 $this->observer->notify_attempt_modified($qa); 588 } 589 590 /** 591 * Process all the question actions in the current request. 592 * 593 * If there is a parameter slots included in the post data, then only 594 * those question numbers will be processed, otherwise all questions in this 595 * useage will be. 596 * 597 * This function also does {@link update_question_flags()}. 598 * 599 * @param int $timestamp optional, use this timestamp as 'now'. 600 * @param array $postdata optional, only intended for testing. Use this data 601 * instead of the data from $_POST. 602 */ 603 public function process_all_actions($timestamp = null, $postdata = null) { 604 foreach ($this->get_slots_in_request($postdata) as $slot) { 605 if (!$this->validate_sequence_number($slot, $postdata)) { 606 continue; 607 } 608 $submitteddata = $this->extract_responses($slot, $postdata); 609 $this->process_action($slot, $submitteddata, $timestamp); 610 } 611 $this->update_question_flags($postdata); 612 } 613 614 /** 615 * Process all the question autosave data in the current request. 616 * 617 * If there is a parameter slots included in the post data, then only 618 * those question numbers will be processed, otherwise all questions in this 619 * useage will be. 620 * 621 * This function also does {@link update_question_flags()}. 622 * 623 * @param int $timestamp optional, use this timestamp as 'now'. 624 * @param array $postdata optional, only intended for testing. Use this data 625 * instead of the data from $_POST. 626 */ 627 public function process_all_autosaves($timestamp = null, $postdata = null) { 628 foreach ($this->get_slots_in_request($postdata) as $slot) { 629 if (!$this->is_autosave_required($slot, $postdata)) { 630 continue; 631 } 632 $submitteddata = $this->extract_responses($slot, $postdata); 633 $this->process_autosave($slot, $submitteddata, $timestamp); 634 } 635 $this->update_question_flags($postdata); 636 } 637 638 /** 639 * Get the list of slot numbers that should be processed as part of processing 640 * the current request. 641 * @param array $postdata optional, only intended for testing. Use this data 642 * instead of the data from $_POST. 643 * @return array of slot numbers. 644 */ 645 protected function get_slots_in_request($postdata = null) { 646 // Note: we must not use "question_attempt::get_submitted_var()" because there is no attempt instance!!! 647 if (is_null($postdata)) { 648 $slots = optional_param('slots', null, PARAM_SEQUENCE); 649 } else if (array_key_exists('slots', $postdata)) { 650 $slots = clean_param($postdata['slots'], PARAM_SEQUENCE); 651 } else { 652 $slots = null; 653 } 654 if (is_null($slots)) { 655 $slots = $this->get_slots(); 656 } else if (!$slots) { 657 $slots = array(); 658 } else { 659 $slots = explode(',', $slots); 660 } 661 return $slots; 662 } 663 664 /** 665 * Get the submitted data from the current request that belongs to this 666 * particular question. 667 * 668 * @param int $slot the number used to identify this question within this usage. 669 * @param $postdata optional, only intended for testing. Use this data 670 * instead of the data from $_POST. 671 * @return array submitted data specific to this question. 672 */ 673 public function extract_responses($slot, $postdata = null) { 674 return $this->get_question_attempt($slot)->get_submitted_data($postdata); 675 } 676 677 /** 678 * Transform an array of response data for slots to an array of post data as you would get from quiz attempt form. 679 * 680 * @param $simulatedresponses array keys are slot nos => contains arrays representing student 681 * responses which will be passed to question_definition::prepare_simulated_post_data method 682 * and then have the appropriate prefix added. 683 * @return array simulated post data 684 */ 685 public function prepare_simulated_post_data($simulatedresponses) { 686 $simulatedpostdata = array(); 687 $simulatedpostdata['slots'] = implode(',', array_keys($simulatedresponses)); 688 foreach ($simulatedresponses as $slot => $responsedata) { 689 $slotresponse = array(); 690 691 // Behaviour vars should not be processed by question type, just add prefix. 692 $behaviourvars = $this->get_question_attempt($slot)->get_behaviour()->get_expected_data(); 693 foreach (array_keys($responsedata) as $responsedatakey) { 694 if ($responsedatakey{0} === '-') { 695 $behaviourvarname = substr($responsedatakey, 1); 696 if (isset($behaviourvars[$behaviourvarname])) { 697 // Expected behaviour var found. 698 if ($responsedata[$responsedatakey]) { 699 // Only set the behaviour var if the column value from the cvs file is non zero. 700 // The behaviours only look at whether the var is set or not they don't look at the value. 701 $slotresponse[$responsedatakey] = $responsedata[$responsedatakey]; 702 } 703 } 704 // Remove both expected and unexpected vars from data passed to question type. 705 unset($responsedata[$responsedatakey]); 706 } 707 } 708 709 $slotresponse += $this->get_question($slot)->prepare_simulated_post_data($responsedata); 710 $slotresponse[':sequencecheck'] = $this->get_question_attempt($slot)->get_sequence_check_count(); 711 712 // Add this slot's prefix to slot data. 713 $prefix = $this->get_field_prefix($slot); 714 foreach ($slotresponse as $key => $value) { 715 $simulatedpostdata[$prefix.$key] = $value; 716 } 717 } 718 return $simulatedpostdata; 719 } 720 721 /** 722 * Process a specific action on a specific question. 723 * @param int $slot the number used to identify this question within this usage. 724 * @param $submitteddata the submitted data that constitutes the action. 725 */ 726 public function process_action($slot, $submitteddata, $timestamp = null) { 727 $qa = $this->get_question_attempt($slot); 728 $qa->process_action($submitteddata, $timestamp); 729 $this->observer->notify_attempt_modified($qa); 730 } 731 732 /** 733 * Process an autosave action on a specific question. 734 * @param int $slot the number used to identify this question within this usage. 735 * @param $submitteddata the submitted data that constitutes the action. 736 */ 737 public function process_autosave($slot, $submitteddata, $timestamp = null) { 738 $qa = $this->get_question_attempt($slot); 739 if ($qa->process_autosave($submitteddata, $timestamp)) { 740 $this->observer->notify_attempt_modified($qa); 741 } 742 } 743 744 /** 745 * Check that the sequence number, that detects weird things like the student 746 * clicking back, is OK. If the sequence check variable is not present, returns 747 * false. If the check variable is present and correct, returns true. If the 748 * variable is present and wrong, throws an exception. 749 * @param int $slot the number used to identify this question within this usage. 750 * @param array $submitteddata the submitted data that constitutes the action. 751 * @return bool true if the check variable is present and correct. False if it 752 * is missing. (Throws an exception if the check fails.) 753 */ 754 public function validate_sequence_number($slot, $postdata = null) { 755 $qa = $this->get_question_attempt($slot); 756 $sequencecheck = $qa->get_submitted_var( 757 $qa->get_control_field_name('sequencecheck'), PARAM_INT, $postdata); 758 if (is_null($sequencecheck)) { 759 return false; 760 } else if ($sequencecheck != $qa->get_sequence_check_count()) { 761 throw new question_out_of_sequence_exception($this->id, $slot, $postdata); 762 } else { 763 return true; 764 } 765 } 766 767 /** 768 * Check, based on the sequence number, whether this auto-save is still required. 769 * @param int $slot the number used to identify this question within this usage. 770 * @param array $submitteddata the submitted data that constitutes the action. 771 * @return bool true if the check variable is present and correct, otherwise false. 772 */ 773 public function is_autosave_required($slot, $postdata = null) { 774 $qa = $this->get_question_attempt($slot); 775 $sequencecheck = $qa->get_submitted_var( 776 $qa->get_control_field_name('sequencecheck'), PARAM_INT, $postdata); 777 if (is_null($sequencecheck)) { 778 return false; 779 } else if ($sequencecheck != $qa->get_sequence_check_count()) { 780 return false; 781 } else { 782 return true; 783 } 784 } 785 786 /** 787 * Update the flagged state for all question_attempts in this usage, if their 788 * flagged state was changed in the request. 789 * 790 * @param $postdata optional, only intended for testing. Use this data 791 * instead of the data from $_POST. 792 */ 793 public function update_question_flags($postdata = null) { 794 foreach ($this->questionattempts as $qa) { 795 $flagged = $qa->get_submitted_var( 796 $qa->get_flag_field_name(), PARAM_BOOL, $postdata); 797 if (!is_null($flagged) && $flagged != $qa->is_flagged()) { 798 $qa->set_flagged($flagged); 799 } 800 } 801 } 802 803 /** 804 * Get the correct response to a particular question. Passing the results of 805 * this method to {@link process_action()} will probably result in full marks. 806 * If it is not possible to compute a correct response, this method should return null. 807 * @param int $slot the number used to identify this question within this usage. 808 * @return array that constitutes a correct response to this question. 809 */ 810 public function get_correct_response($slot) { 811 return $this->get_question_attempt($slot)->get_correct_response(); 812 } 813 814 /** 815 * Finish the active phase of an attempt at a question. 816 * 817 * This is an external act of finishing the attempt. Think, for example, of 818 * the 'Submit all and finish' button in the quiz. Some behaviours, 819 * (for example, immediatefeedback) give a way of finishing the active phase 820 * of a question attempt as part of a {@link process_action()} call. 821 * 822 * After the active phase is over, the only changes possible are things like 823 * manual grading, or changing the flag state. 824 * 825 * @param int $slot the number used to identify this question within this usage. 826 */ 827 public function finish_question($slot, $timestamp = null) { 828 $qa = $this->get_question_attempt($slot); 829 $qa->finish($timestamp); 830 $this->observer->notify_attempt_modified($qa); 831 } 832 833 /** 834 * Finish the active phase of an attempt at a question. See {@link finish_question()} 835 * for a fuller description of what 'finish' means. 836 */ 837 public function finish_all_questions($timestamp = null) { 838 foreach ($this->questionattempts as $qa) { 839 $qa->finish($timestamp); 840 $this->observer->notify_attempt_modified($qa); 841 } 842 } 843 844 /** 845 * Perform a manual grading action on a question attempt. 846 * @param int $slot the number used to identify this question within this usage. 847 * @param string $comment the comment being added to the question attempt. 848 * @param number $mark the mark that is being assigned. Can be null to just 849 * add a comment. 850 * @param int $commentformat one of the FORMAT_... constants. The format of $comment. 851 */ 852 public function manual_grade($slot, $comment, $mark, $commentformat = null) { 853 $qa = $this->get_question_attempt($slot); 854 $qa->manual_grade($comment, $mark, $commentformat); 855 $this->observer->notify_attempt_modified($qa); 856 } 857 858 /** 859 * Regrade a question in this usage. This replays the sequence of submitted 860 * actions to recompute the outcomes. 861 * @param int $slot the number used to identify this question within this usage. 862 * @param bool $finished whether the question attempt should be forced to be finished 863 * after the regrade, or whether it may still be in progress (default false). 864 * @param number $newmaxmark (optional) if given, will change the max mark while regrading. 865 */ 866 public function regrade_question($slot, $finished = false, $newmaxmark = null) { 867 $oldqa = $this->get_question_attempt($slot); 868 if (is_null($newmaxmark)) { 869 $newmaxmark = $oldqa->get_max_mark(); 870 } 871 872 $newqa = new question_attempt($oldqa->get_question(), $oldqa->get_usage_id(), 873 $this->observer, $newmaxmark); 874 $newqa->set_database_id($oldqa->get_database_id()); 875 $newqa->set_slot($oldqa->get_slot()); 876 $newqa->regrade($oldqa, $finished); 877 878 $this->questionattempts[$slot] = $newqa; 879 $this->observer->notify_attempt_modified($newqa); 880 } 881 882 /** 883 * Regrade all the questions in this usage (without changing their max mark). 884 * @param bool $finished whether each question should be forced to be finished 885 * after the regrade, or whether it may still be in progress (default false). 886 */ 887 public function regrade_all_questions($finished = false) { 888 foreach ($this->questionattempts as $slot => $notused) { 889 $this->regrade_question($slot, $finished); 890 } 891 } 892 893 /** 894 * Change the max mark for this question_attempt. 895 * @param int $slot the slot number of the question of inerest. 896 * @param float $maxmark the new max mark. 897 */ 898 public function set_max_mark($slot, $maxmark) { 899 $this->get_question_attempt($slot)->set_max_mark($maxmark); 900 } 901 902 /** 903 * Create a question_usage_by_activity from records loaded from the database. 904 * 905 * For internal use only. 906 * 907 * @param Iterator $records Raw records loaded from the database. 908 * @param int $questionattemptid The id of the question_attempt to extract. 909 * @return question_usage_by_activity The newly constructed usage. 910 */ 911 public static function load_from_records($records, $qubaid) { 912 $record = $records->current(); 913 while ($record->qubaid != $qubaid) { 914 $records->next(); 915 if (!$records->valid()) { 916 throw new coding_exception("Question usage {$qubaid} not found in the database."); 917 } 918 $record = $records->current(); 919 } 920 921 $quba = new question_usage_by_activity($record->component, 922 context::instance_by_id($record->contextid, IGNORE_MISSING)); 923 $quba->set_id_from_database($record->qubaid); 924 $quba->set_preferred_behaviour($record->preferredbehaviour); 925 926 $quba->observer = new question_engine_unit_of_work($quba); 927 928 // If slot is null then the current pointer in $records will not be 929 // advanced in the while loop below, and we get stuck in an infinite loop, 930 // since this method is supposed to always consume at least one record. 931 // Therefore, in this case, advance the record here. 932 if (is_null($record->slot)) { 933 $records->next(); 934 } 935 936 while ($record && $record->qubaid == $qubaid && !is_null($record->slot)) { 937 $quba->questionattempts[$record->slot] = 938 question_attempt::load_from_records($records, 939 $record->questionattemptid, $quba->observer, 940 $quba->get_preferred_behaviour()); 941 if ($records->valid()) { 942 $record = $records->current(); 943 } else { 944 $record = false; 945 } 946 } 947 948 return $quba; 949 } 950 } 951 952 953 /** 954 * A class abstracting access to the 955 * {@link question_usage_by_activity::$questionattempts} array. 956 * 957 * This class snapshots the list of {@link question_attempts} to iterate over 958 * when it is created. If a question is added to the usage mid-iteration, it 959 * will now show up. 960 * 961 * To create an instance of this class, use 962 * {@link question_usage_by_activity::get_attempt_iterator()} 963 * 964 * @copyright 2009 The Open University 965 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 966 */ 967 class question_attempt_iterator implements Iterator, ArrayAccess { 968 /** @var question_usage_by_activity that we are iterating over. */ 969 protected $quba; 970 /** @var array of question numbers. */ 971 protected $slots; 972 973 /** 974 * To create an instance of this class, use 975 * {@link question_usage_by_activity::get_attempt_iterator()}. 976 * @param $quba the usage to iterate over. 977 */ 978 public function __construct(question_usage_by_activity $quba) { 979 $this->quba = $quba; 980 $this->slots = $quba->get_slots(); 981 $this->rewind(); 982 } 983 984 /** @return question_attempt_step */ 985 public function current() { 986 return $this->offsetGet(current($this->slots)); 987 } 988 /** @return int */ 989 public function key() { 990 return current($this->slots); 991 } 992 public function next() { 993 next($this->slots); 994 } 995 public function rewind() { 996 reset($this->slots); 997 } 998 /** @return bool */ 999 public function valid() { 1000 return current($this->slots) !== false; 1001 } 1002 1003 /** @return bool */ 1004 public function offsetExists($slot) { 1005 return in_array($slot, $this->slots); 1006 } 1007 /** @return question_attempt_step */ 1008 public function offsetGet($slot) { 1009 return $this->quba->get_question_attempt($slot); 1010 } 1011 public function offsetSet($slot, $value) { 1012 throw new coding_exception('You are only allowed read-only access to ' . 1013 'question_attempt::states through a question_attempt_step_iterator. Cannot set.'); 1014 } 1015 public function offsetUnset($slot) { 1016 throw new coding_exception('You are only allowed read-only access to ' . 1017 'question_attempt::states through a question_attempt_step_iterator. Cannot unset.'); 1018 } 1019 } 1020 1021 1022 /** 1023 * Interface for things that want to be notified of signficant changes to a 1024 * {@link question_usage_by_activity}. 1025 * 1026 * A question behaviour controls the flow of actions a student can 1027 * take as they work through a question, and later, as a teacher manually grades it. 1028 * 1029 * @copyright 2009 The Open University 1030 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1031 */ 1032 interface question_usage_observer { 1033 /** Called when a field of the question_usage_by_activity is changed. */ 1034 public function notify_modified(); 1035 1036 /** 1037 * Called when a new question attempt is added to this usage. 1038 * @param question_attempt $qa the newly added question attempt. 1039 */ 1040 public function notify_attempt_added(question_attempt $qa); 1041 1042 /** 1043 * Called when the fields of a question attempt in this usage are modified. 1044 * @param question_attempt $qa the newly added question attempt. 1045 */ 1046 public function notify_attempt_modified(question_attempt $qa); 1047 1048 /** 1049 * Called when a question_attempt has been moved to a new slot. 1050 * @param question_attempt $qa The question attempt that was moved. 1051 * @param int $oldslot The previous slot number of that attempt. 1052 */ 1053 public function notify_attempt_moved(question_attempt $qa, $oldslot); 1054 1055 /** 1056 * Called when a new step is added to a question attempt in this usage. 1057 * @param question_attempt_step $step the new step. 1058 * @param question_attempt $qa the usage it is being added to. 1059 * @param int $seq the sequence number of the new step. 1060 */ 1061 public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq); 1062 1063 /** 1064 * Called when a new step is updated in a question attempt in this usage. 1065 * @param question_attempt_step $step the step that was updated. 1066 * @param question_attempt $qa the usage it is being added to. 1067 * @param int $seq the sequence number of the new step. 1068 */ 1069 public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq); 1070 1071 /** 1072 * Called when a new step is updated in a question attempt in this usage. 1073 * @param question_attempt_step $step the step to delete. 1074 * @param question_attempt $qa the usage it is being added to. 1075 */ 1076 public function notify_step_deleted(question_attempt_step $step, question_attempt $qa); 1077 1078 /** 1079 * Called when a new metadata variable is set on a question attempt in this usage. 1080 * @param question_attempt $qa the question attempt the metadata is being added to. 1081 * @param int $name the name of the metadata variable added. 1082 */ 1083 public function notify_metadata_added(question_attempt $qa, $name); 1084 1085 /** 1086 * Called when a metadata variable on a question attempt in this usage is updated. 1087 * @param question_attempt $qa the question attempt where the metadata is being modified. 1088 * @param int $name the name of the metadata variable modified. 1089 */ 1090 public function notify_metadata_modified(question_attempt $qa, $name); 1091 } 1092 1093 1094 /** 1095 * Null implmentation of the {@link question_usage_watcher} interface. 1096 * Does nothing. 1097 * 1098 * @copyright 2009 The Open University 1099 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1100 */ 1101 class question_usage_null_observer implements question_usage_observer { 1102 public function notify_modified() { 1103 } 1104 public function notify_attempt_added(question_attempt $qa) { 1105 } 1106 public function notify_attempt_modified(question_attempt $qa) { 1107 } 1108 public function notify_attempt_moved(question_attempt $qa, $oldslot) { 1109 } 1110 public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) { 1111 } 1112 public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) { 1113 } 1114 public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) { 1115 } 1116 public function notify_metadata_added(question_attempt $qa, $name) { 1117 } 1118 public function notify_metadata_modified(question_attempt $qa, $name) { 1119 } 1120 }
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 |