[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * 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 * The slot number that will be allotted to the next question added. 177 */ 178 public function next_slot_number() { 179 return count($this->questionattempts) + 1; 180 } 181 182 /** 183 * Get the question_definition for a question in this attempt. 184 * @param int $slot the number used to identify this question within this usage. 185 * @return question_definition the requested question object. 186 */ 187 public function get_question($slot) { 188 return $this->get_question_attempt($slot)->get_question(); 189 } 190 191 /** @return array all the identifying numbers of all the questions in this usage. */ 192 public function get_slots() { 193 return array_keys($this->questionattempts); 194 } 195 196 /** @return int the identifying number of the first question that was added to this usage. */ 197 public function get_first_question_number() { 198 reset($this->questionattempts); 199 return key($this->questionattempts); 200 } 201 202 /** @return int the number of questions that are currently in this usage. */ 203 public function question_count() { 204 return count($this->questionattempts); 205 } 206 207 /** 208 * Note the part of the {@link question_usage_by_activity} comment that explains 209 * that {@link question_attempt} objects should be considered part of the inner 210 * workings of the question engine, and should not, if possible, be accessed directly. 211 * 212 * @return question_attempt_iterator for iterating over all the questions being 213 * attempted. as part of this usage. 214 */ 215 public function get_attempt_iterator() { 216 return new question_attempt_iterator($this); 217 } 218 219 /** 220 * Check whether $number actually corresponds to a question attempt that is 221 * part of this usage. Throws an exception if not. 222 * 223 * @param int $slot a number allegedly identifying a question within this usage. 224 */ 225 protected function check_slot($slot) { 226 if (!array_key_exists($slot, $this->questionattempts)) { 227 throw new coding_exception('There is no question_attempt number ' . $slot . 228 ' in this attempt.'); 229 } 230 } 231 232 /** 233 * Note the part of the {@link question_usage_by_activity} comment that explains 234 * that {@link question_attempt} objects should be considered part of the inner 235 * workings of the question engine, and should not, if possible, be accessed directly. 236 * 237 * @param int $slot the number used to identify this question within this usage. 238 * @return question_attempt the corresponding {@link question_attempt} object. 239 */ 240 public function get_question_attempt($slot) { 241 $this->check_slot($slot); 242 return $this->questionattempts[$slot]; 243 } 244 245 /** 246 * Get the current state of the attempt at a question. 247 * @param int $slot the number used to identify this question within this usage. 248 * @return question_state. 249 */ 250 public function get_question_state($slot) { 251 return $this->get_question_attempt($slot)->get_state(); 252 } 253 254 /** 255 * @param int $slot the number used to identify this question within this usage. 256 * @param bool $showcorrectness Whether right/partial/wrong states should 257 * be distinguised. 258 * @return string A brief textual description of the current state. 259 */ 260 public function get_question_state_string($slot, $showcorrectness) { 261 return $this->get_question_attempt($slot)->get_state_string($showcorrectness); 262 } 263 264 /** 265 * @param int $slot the number used to identify this question within this usage. 266 * @param bool $showcorrectness Whether right/partial/wrong states should 267 * be distinguised. 268 * @return string a CSS class name for the current state. 269 */ 270 public function get_question_state_class($slot, $showcorrectness) { 271 return $this->get_question_attempt($slot)->get_state_class($showcorrectness); 272 } 273 274 /** 275 * Get the time of the most recent action performed on a question. 276 * @param int $slot the number used to identify this question within this usage. 277 * @return int timestamp. 278 */ 279 public function get_question_action_time($slot) { 280 return $this->get_question_attempt($slot)->get_last_action_time(); 281 } 282 283 /** 284 * Get the current fraction awarded for the attempt at a question. 285 * @param int $slot the number used to identify this question within this usage. 286 * @return number|null The current fraction for this question, or null if one has 287 * not been assigned yet. 288 */ 289 public function get_question_fraction($slot) { 290 return $this->get_question_attempt($slot)->get_fraction(); 291 } 292 293 /** 294 * Get the current mark awarded for the attempt at a question. 295 * @param int $slot the number used to identify this question within this usage. 296 * @return number|null The current mark for this question, or null if one has 297 * not been assigned yet. 298 */ 299 public function get_question_mark($slot) { 300 return $this->get_question_attempt($slot)->get_mark(); 301 } 302 303 /** 304 * Get the maximum mark possible for the attempt at a question. 305 * @param int $slot the number used to identify this question within this usage. 306 * @return number the available marks for this question. 307 */ 308 public function get_question_max_mark($slot) { 309 return $this->get_question_attempt($slot)->get_max_mark(); 310 } 311 312 /** 313 * Get the total mark for all questions in this usage. 314 * @return number The sum of marks of all the question_attempts in this usage. 315 */ 316 public function get_total_mark() { 317 $mark = 0; 318 foreach ($this->questionattempts as $qa) { 319 if ($qa->get_max_mark() > 0 && $qa->get_state() == question_state::$needsgrading) { 320 return null; 321 } 322 $mark += $qa->get_mark(); 323 } 324 return $mark; 325 } 326 327 /** 328 * Get summary information about this usage. 329 * 330 * Some behaviours may be able to provide interesting summary information 331 * about the attempt as a whole, and this method provides access to that data. 332 * To see how this works, try setting a quiz to one of the CBM behaviours, 333 * and then look at the extra information displayed at the top of the quiz 334 * review page once you have sumitted an attempt. 335 * 336 * In the return value, the array keys are identifiers of the form 337 * qbehaviour_behaviourname_meaningfullkey. For qbehaviour_deferredcbm_highsummary. 338 * The values are arrays with two items, title and content. Each of these 339 * will be either a string, or a renderable. 340 * 341 * @return array as described above. 342 */ 343 public function get_summary_information(question_display_options $options) { 344 return question_engine::get_behaviour_type($this->preferredbehaviour) 345 ->summarise_usage($this, $options); 346 } 347 348 /** 349 * @return string a simple textual summary of the question that was asked. 350 */ 351 public function get_question_summary($slot) { 352 return $this->get_question_attempt($slot)->get_question_summary(); 353 } 354 355 /** 356 * @return string a simple textual summary of response given. 357 */ 358 public function get_response_summary($slot) { 359 return $this->get_question_attempt($slot)->get_response_summary(); 360 } 361 362 /** 363 * @return string a simple textual summary of the correct resonse. 364 */ 365 public function get_right_answer_summary($slot) { 366 return $this->get_question_attempt($slot)->get_right_answer_summary(); 367 } 368 369 /** 370 * Get the {@link core_question_renderer}, in collaboration with appropriate 371 * {@link qbehaviour_renderer} and {@link qtype_renderer} subclasses, to generate the 372 * HTML to display this question. 373 * @param int $slot the number used to identify this question within this usage. 374 * @param question_display_options $options controls how the question is rendered. 375 * @param string|null $number The question number to display. 'i' is a special 376 * value that gets displayed as Information. Null means no number is displayed. 377 * @return string HTML fragment representing the question. 378 */ 379 public function render_question($slot, $options, $number = null) { 380 $options->context = $this->context; 381 return $this->get_question_attempt($slot)->render($options, $number); 382 } 383 384 /** 385 * Generate any bits of HTML that needs to go in the <head> tag when this question 386 * is displayed in the body. 387 * @param int $slot the number used to identify this question within this usage. 388 * @return string HTML fragment. 389 */ 390 public function render_question_head_html($slot) { 391 //$options->context = $this->context; 392 return $this->get_question_attempt($slot)->render_head_html(); 393 } 394 395 /** 396 * Like {@link render_question()} but displays the question at the past step 397 * indicated by $seq, rather than showing the latest step. 398 * 399 * @param int $slot the number used to identify this question within this usage. 400 * @param int $seq the seq number of the past state to display. 401 * @param question_display_options $options controls how the question is rendered. 402 * @param string|null $number The question number to display. 'i' is a special 403 * value that gets displayed as Information. Null means no number is displayed. 404 * @return string HTML fragment representing the question. 405 */ 406 public function render_question_at_step($slot, $seq, $options, $number = null) { 407 $options->context = $this->context; 408 return $this->get_question_attempt($slot)->render_at_step( 409 $seq, $options, $number, $this->preferredbehaviour); 410 } 411 412 /** 413 * Checks whether the users is allow to be served a particular file. 414 * @param int $slot the number used to identify this question within this usage. 415 * @param question_display_options $options the options that control display of the question. 416 * @param string $component the name of the component we are serving files for. 417 * @param string $filearea the name of the file area. 418 * @param array $args the remaining bits of the file path. 419 * @param bool $forcedownload whether the user must be forced to download the file. 420 * @return bool true if the user can access this file. 421 */ 422 public function check_file_access($slot, $options, $component, $filearea, 423 $args, $forcedownload) { 424 return $this->get_question_attempt($slot)->check_file_access( 425 $options, $component, $filearea, $args, $forcedownload); 426 } 427 428 /** 429 * Replace a particular question_attempt with a different one. 430 * 431 * For internal use only. Used when reloading the state of a question from the 432 * database. 433 * 434 * @param array $records Raw records loaded from the database. 435 * @param int $questionattemptid The id of the question_attempt to extract. 436 * @return question_attempt The newly constructed question_attempt_step. 437 */ 438 public function replace_loaded_question_attempt_info($slot, $qa) { 439 $this->check_slot($slot); 440 $this->questionattempts[$slot] = $qa; 441 } 442 443 /** 444 * You should probably not use this method in code outside the question engine. 445 * The main reason for exposing it was for the benefit of unit tests. 446 * @param int $slot the number used to identify this question within this usage. 447 * @return string return the prefix that is pre-pended to field names in the HTML 448 * that is output. 449 */ 450 public function get_field_prefix($slot) { 451 return $this->get_question_attempt($slot)->get_field_prefix(); 452 } 453 454 /** 455 * Get the number of variants available for the question in this slot. 456 * @param int $slot the number used to identify this question within this usage. 457 * @return int the number of variants available. 458 */ 459 public function get_num_variants($slot) { 460 return $this->get_question_attempt($slot)->get_question()->get_num_variants(); 461 } 462 463 /** 464 * Get the variant of the question being used in a given slot. 465 * @param int $slot the number used to identify this question within this usage. 466 * @return int the variant of this question that is being used. 467 */ 468 public function get_variant($slot) { 469 return $this->get_question_attempt($slot)->get_variant(); 470 } 471 472 /** 473 * Start the attempt at a question that has been added to this usage. 474 * @param int $slot the number used to identify this question within this usage. 475 * @param int $variant which variant of the question to use. Must be between 476 * 1 and ->get_num_variants($slot) inclusive. If not give, a variant is 477 * chosen at random. 478 */ 479 public function start_question($slot, $variant = null) { 480 if (is_null($variant)) { 481 $variant = rand(1, $this->get_num_variants($slot)); 482 } 483 484 $qa = $this->get_question_attempt($slot); 485 $qa->start($this->preferredbehaviour, $variant); 486 $this->observer->notify_attempt_modified($qa); 487 } 488 489 /** 490 * Start the attempt at all questions that has been added to this usage. 491 * @param question_variant_selection_strategy how to pick which variant of each question to use. 492 * @param int $timestamp optional, the timstamp to record for this action. Defaults to now. 493 * @param int $userid optional, the user to attribute this action to. Defaults to the current user. 494 */ 495 public function start_all_questions(question_variant_selection_strategy $variantstrategy = null, 496 $timestamp = null, $userid = null) { 497 if (is_null($variantstrategy)) { 498 $variantstrategy = new question_variant_random_strategy(); 499 } 500 501 foreach ($this->questionattempts as $qa) { 502 $qa->start($this->preferredbehaviour, $qa->select_variant($variantstrategy)); 503 $this->observer->notify_attempt_modified($qa); 504 } 505 } 506 507 /** 508 * Start the attempt at a question, starting from the point where the previous 509 * question_attempt $oldqa had reached. This is used by the quiz 'Each attempt 510 * builds on last' mode. 511 * @param int $slot the number used to identify this question within this usage. 512 * @param question_attempt $oldqa a previous attempt at this quetsion that 513 * defines the starting point. 514 */ 515 public function start_question_based_on($slot, question_attempt $oldqa) { 516 $qa = $this->get_question_attempt($slot); 517 $qa->start_based_on($oldqa); 518 $this->observer->notify_attempt_modified($qa); 519 } 520 521 /** 522 * Process all the question actions in the current request. 523 * 524 * If there is a parameter slots included in the post data, then only 525 * those question numbers will be processed, otherwise all questions in this 526 * useage will be. 527 * 528 * This function also does {@link update_question_flags()}. 529 * 530 * @param int $timestamp optional, use this timestamp as 'now'. 531 * @param array $postdata optional, only intended for testing. Use this data 532 * instead of the data from $_POST. 533 */ 534 public function process_all_actions($timestamp = null, $postdata = null) { 535 foreach ($this->get_slots_in_request($postdata) as $slot) { 536 if (!$this->validate_sequence_number($slot, $postdata)) { 537 continue; 538 } 539 $submitteddata = $this->extract_responses($slot, $postdata); 540 $this->process_action($slot, $submitteddata, $timestamp); 541 } 542 $this->update_question_flags($postdata); 543 } 544 545 /** 546 * Process all the question autosave data in the current request. 547 * 548 * If there is a parameter slots included in the post data, then only 549 * those question numbers will be processed, otherwise all questions in this 550 * useage will be. 551 * 552 * This function also does {@link update_question_flags()}. 553 * 554 * @param int $timestamp optional, use this timestamp as 'now'. 555 * @param array $postdata optional, only intended for testing. Use this data 556 * instead of the data from $_POST. 557 */ 558 public function process_all_autosaves($timestamp = null, $postdata = null) { 559 foreach ($this->get_slots_in_request($postdata) as $slot) { 560 if (!$this->is_autosave_required($slot, $postdata)) { 561 continue; 562 } 563 $submitteddata = $this->extract_responses($slot, $postdata); 564 $this->process_autosave($slot, $submitteddata, $timestamp); 565 } 566 $this->update_question_flags($postdata); 567 } 568 569 /** 570 * Get the list of slot numbers that should be processed as part of processing 571 * the current request. 572 * @param array $postdata optional, only intended for testing. Use this data 573 * instead of the data from $_POST. 574 * @return array of slot numbers. 575 */ 576 protected function get_slots_in_request($postdata = null) { 577 // Note: we must not use "question_attempt::get_submitted_var()" because there is no attempt instance!!! 578 if (is_null($postdata)) { 579 $slots = optional_param('slots', null, PARAM_SEQUENCE); 580 } else if (array_key_exists('slots', $postdata)) { 581 $slots = clean_param($postdata['slots'], PARAM_SEQUENCE); 582 } else { 583 $slots = null; 584 } 585 if (is_null($slots)) { 586 $slots = $this->get_slots(); 587 } else if (!$slots) { 588 $slots = array(); 589 } else { 590 $slots = explode(',', $slots); 591 } 592 return $slots; 593 } 594 595 /** 596 * Get the submitted data from the current request that belongs to this 597 * particular question. 598 * 599 * @param int $slot the number used to identify this question within this usage. 600 * @param $postdata optional, only intended for testing. Use this data 601 * instead of the data from $_POST. 602 * @return array submitted data specific to this question. 603 */ 604 public function extract_responses($slot, $postdata = null) { 605 return $this->get_question_attempt($slot)->get_submitted_data($postdata); 606 } 607 608 /** 609 * Transform an array of response data for slots to an array of post data as you would get from quiz attempt form. 610 * 611 * @param $simulatedresponses array keys are slot nos => contains arrays representing student 612 * responses which will be passed to question_definition::prepare_simulated_post_data method 613 * and then have the appropriate prefix added. 614 * @return array simulated post data 615 */ 616 public function prepare_simulated_post_data($simulatedresponses) { 617 $simulatedpostdata = array(); 618 $simulatedpostdata['slots'] = implode(',', array_keys($simulatedresponses)); 619 foreach ($simulatedresponses as $slot => $responsedata) { 620 $slotresponse = array(); 621 622 // Behaviour vars should not be processed by question type, just add prefix. 623 $behaviourvars = $this->get_question_attempt($slot)->get_behaviour()->get_expected_data(); 624 foreach (array_keys($responsedata) as $responsedatakey) { 625 if ($responsedatakey{0} === '-') { 626 $behaviourvarname = substr($responsedatakey, 1); 627 if (isset($behaviourvars[$behaviourvarname])) { 628 // Expected behaviour var found. 629 if ($responsedata[$responsedatakey]) { 630 // Only set the behaviour var if the column value from the cvs file is non zero. 631 // The behaviours only look at whether the var is set or not they don't look at the value. 632 $slotresponse[$responsedatakey] = $responsedata[$responsedatakey]; 633 } 634 } 635 // Remove both expected and unexpected vars from data passed to question type. 636 unset($responsedata[$responsedatakey]); 637 } 638 } 639 640 $slotresponse += $this->get_question($slot)->prepare_simulated_post_data($responsedata); 641 $slotresponse[':sequencecheck'] = $this->get_question_attempt($slot)->get_sequence_check_count(); 642 643 // Add this slot's prefix to slot data. 644 $prefix = $this->get_field_prefix($slot); 645 foreach ($slotresponse as $key => $value) { 646 $simulatedpostdata[$prefix.$key] = $value; 647 } 648 } 649 return $simulatedpostdata; 650 } 651 652 /** 653 * Process a specific action on a specific question. 654 * @param int $slot the number used to identify this question within this usage. 655 * @param $submitteddata the submitted data that constitutes the action. 656 */ 657 public function process_action($slot, $submitteddata, $timestamp = null) { 658 $qa = $this->get_question_attempt($slot); 659 $qa->process_action($submitteddata, $timestamp); 660 $this->observer->notify_attempt_modified($qa); 661 } 662 663 /** 664 * Process an autosave action on a specific question. 665 * @param int $slot the number used to identify this question within this usage. 666 * @param $submitteddata the submitted data that constitutes the action. 667 */ 668 public function process_autosave($slot, $submitteddata, $timestamp = null) { 669 $qa = $this->get_question_attempt($slot); 670 if ($qa->process_autosave($submitteddata, $timestamp)) { 671 $this->observer->notify_attempt_modified($qa); 672 } 673 } 674 675 /** 676 * Check that the sequence number, that detects weird things like the student 677 * clicking back, is OK. If the sequence check variable is not present, returns 678 * false. If the check variable is present and correct, returns true. If the 679 * variable is present and wrong, throws an exception. 680 * @param int $slot the number used to identify this question within this usage. 681 * @param array $submitteddata the submitted data that constitutes the action. 682 * @return bool true if the check variable is present and correct. False if it 683 * is missing. (Throws an exception if the check fails.) 684 */ 685 public function validate_sequence_number($slot, $postdata = null) { 686 $qa = $this->get_question_attempt($slot); 687 $sequencecheck = $qa->get_submitted_var( 688 $qa->get_control_field_name('sequencecheck'), PARAM_INT, $postdata); 689 if (is_null($sequencecheck)) { 690 return false; 691 } else if ($sequencecheck != $qa->get_sequence_check_count()) { 692 throw new question_out_of_sequence_exception($this->id, $slot, $postdata); 693 } else { 694 return true; 695 } 696 } 697 698 /** 699 * Check, based on the sequence number, whether this auto-save is still required. 700 * @param int $slot the number used to identify this question within this usage. 701 * @param array $submitteddata the submitted data that constitutes the action. 702 * @return bool true if the check variable is present and correct, otherwise false. 703 */ 704 public function is_autosave_required($slot, $postdata = null) { 705 $qa = $this->get_question_attempt($slot); 706 $sequencecheck = $qa->get_submitted_var( 707 $qa->get_control_field_name('sequencecheck'), PARAM_INT, $postdata); 708 if (is_null($sequencecheck)) { 709 return false; 710 } else if ($sequencecheck != $qa->get_sequence_check_count()) { 711 return false; 712 } else { 713 return true; 714 } 715 } 716 717 /** 718 * Update the flagged state for all question_attempts in this usage, if their 719 * flagged state was changed in the request. 720 * 721 * @param $postdata optional, only intended for testing. Use this data 722 * instead of the data from $_POST. 723 */ 724 public function update_question_flags($postdata = null) { 725 foreach ($this->questionattempts as $qa) { 726 $flagged = $qa->get_submitted_var( 727 $qa->get_flag_field_name(), PARAM_BOOL, $postdata); 728 if (!is_null($flagged) && $flagged != $qa->is_flagged()) { 729 $qa->set_flagged($flagged); 730 } 731 } 732 } 733 734 /** 735 * Get the correct response to a particular question. Passing the results of 736 * this method to {@link process_action()} will probably result in full marks. 737 * If it is not possible to compute a correct response, this method should return null. 738 * @param int $slot the number used to identify this question within this usage. 739 * @return array that constitutes a correct response to this question. 740 */ 741 public function get_correct_response($slot) { 742 return $this->get_question_attempt($slot)->get_correct_response(); 743 } 744 745 /** 746 * Finish the active phase of an attempt at a question. 747 * 748 * This is an external act of finishing the attempt. Think, for example, of 749 * the 'Submit all and finish' button in the quiz. Some behaviours, 750 * (for example, immediatefeedback) give a way of finishing the active phase 751 * of a question attempt as part of a {@link process_action()} call. 752 * 753 * After the active phase is over, the only changes possible are things like 754 * manual grading, or changing the flag state. 755 * 756 * @param int $slot the number used to identify this question within this usage. 757 */ 758 public function finish_question($slot, $timestamp = null) { 759 $qa = $this->get_question_attempt($slot); 760 $qa->finish($timestamp); 761 $this->observer->notify_attempt_modified($qa); 762 } 763 764 /** 765 * Finish the active phase of an attempt at a question. See {@link finish_question()} 766 * for a fuller description of what 'finish' means. 767 */ 768 public function finish_all_questions($timestamp = null) { 769 foreach ($this->questionattempts as $qa) { 770 $qa->finish($timestamp); 771 $this->observer->notify_attempt_modified($qa); 772 } 773 } 774 775 /** 776 * Perform a manual grading action on a question attempt. 777 * @param int $slot the number used to identify this question within this usage. 778 * @param string $comment the comment being added to the question attempt. 779 * @param number $mark the mark that is being assigned. Can be null to just 780 * add a comment. 781 * @param int $commentformat one of the FORMAT_... constants. The format of $comment. 782 */ 783 public function manual_grade($slot, $comment, $mark, $commentformat = null) { 784 $qa = $this->get_question_attempt($slot); 785 $qa->manual_grade($comment, $mark, $commentformat); 786 $this->observer->notify_attempt_modified($qa); 787 } 788 789 /** 790 * Regrade a question in this usage. This replays the sequence of submitted 791 * actions to recompute the outcomes. 792 * @param int $slot the number used to identify this question within this usage. 793 * @param bool $finished whether the question attempt should be forced to be finished 794 * after the regrade, or whether it may still be in progress (default false). 795 * @param number $newmaxmark (optional) if given, will change the max mark while regrading. 796 */ 797 public function regrade_question($slot, $finished = false, $newmaxmark = null) { 798 $oldqa = $this->get_question_attempt($slot); 799 if (is_null($newmaxmark)) { 800 $newmaxmark = $oldqa->get_max_mark(); 801 } 802 803 $newqa = new question_attempt($oldqa->get_question(), $oldqa->get_usage_id(), 804 $this->observer, $newmaxmark); 805 $newqa->set_database_id($oldqa->get_database_id()); 806 $newqa->set_slot($oldqa->get_slot()); 807 $newqa->regrade($oldqa, $finished); 808 809 $this->questionattempts[$slot] = $newqa; 810 $this->observer->notify_attempt_modified($newqa); 811 } 812 813 /** 814 * Regrade all the questions in this usage (without changing their max mark). 815 * @param bool $finished whether each question should be forced to be finished 816 * after the regrade, or whether it may still be in progress (default false). 817 */ 818 public function regrade_all_questions($finished = false) { 819 foreach ($this->questionattempts as $slot => $notused) { 820 $this->regrade_question($slot, $finished); 821 } 822 } 823 824 /** 825 * Create a question_usage_by_activity from records loaded from the database. 826 * 827 * For internal use only. 828 * 829 * @param Iterator $records Raw records loaded from the database. 830 * @param int $questionattemptid The id of the question_attempt to extract. 831 * @return question_usage_by_activity The newly constructed usage. 832 */ 833 public static function load_from_records($records, $qubaid) { 834 $record = $records->current(); 835 while ($record->qubaid != $qubaid) { 836 $records->next(); 837 if (!$records->valid()) { 838 throw new coding_exception("Question usage {$qubaid} not found in the database."); 839 } 840 $record = $records->current(); 841 } 842 843 $quba = new question_usage_by_activity($record->component, 844 context::instance_by_id($record->contextid, IGNORE_MISSING)); 845 $quba->set_id_from_database($record->qubaid); 846 $quba->set_preferred_behaviour($record->preferredbehaviour); 847 848 $quba->observer = new question_engine_unit_of_work($quba); 849 850 // If slot is null then the current pointer in $records will not be 851 // advanced in the while loop below, and we get stuck in an infinite loop, 852 // since this method is supposed to always consume at least one record. 853 // Therefore, in this case, advance the record here. 854 if (is_null($record->slot)) { 855 $records->next(); 856 } 857 858 while ($record && $record->qubaid == $qubaid && !is_null($record->slot)) { 859 $quba->questionattempts[$record->slot] = 860 question_attempt::load_from_records($records, 861 $record->questionattemptid, $quba->observer, 862 $quba->get_preferred_behaviour()); 863 if ($records->valid()) { 864 $record = $records->current(); 865 } else { 866 $record = false; 867 } 868 } 869 870 return $quba; 871 } 872 } 873 874 875 /** 876 * A class abstracting access to the 877 * {@link question_usage_by_activity::$questionattempts} array. 878 * 879 * This class snapshots the list of {@link question_attempts} to iterate over 880 * when it is created. If a question is added to the usage mid-iteration, it 881 * will now show up. 882 * 883 * To create an instance of this class, use 884 * {@link question_usage_by_activity::get_attempt_iterator()} 885 * 886 * @copyright 2009 The Open University 887 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 888 */ 889 class question_attempt_iterator implements Iterator, ArrayAccess { 890 /** @var question_usage_by_activity that we are iterating over. */ 891 protected $quba; 892 /** @var array of question numbers. */ 893 protected $slots; 894 895 /** 896 * To create an instance of this class, use 897 * {@link question_usage_by_activity::get_attempt_iterator()}. 898 * @param $quba the usage to iterate over. 899 */ 900 public function __construct(question_usage_by_activity $quba) { 901 $this->quba = $quba; 902 $this->slots = $quba->get_slots(); 903 $this->rewind(); 904 } 905 906 /** @return question_attempt_step */ 907 public function current() { 908 return $this->offsetGet(current($this->slots)); 909 } 910 /** @return int */ 911 public function key() { 912 return current($this->slots); 913 } 914 public function next() { 915 next($this->slots); 916 } 917 public function rewind() { 918 reset($this->slots); 919 } 920 /** @return bool */ 921 public function valid() { 922 return current($this->slots) !== false; 923 } 924 925 /** @return bool */ 926 public function offsetExists($slot) { 927 return in_array($slot, $this->slots); 928 } 929 /** @return question_attempt_step */ 930 public function offsetGet($slot) { 931 return $this->quba->get_question_attempt($slot); 932 } 933 public function offsetSet($slot, $value) { 934 throw new coding_exception('You are only allowed read-only access to ' . 935 'question_attempt::states through a question_attempt_step_iterator. Cannot set.'); 936 } 937 public function offsetUnset($slot) { 938 throw new coding_exception('You are only allowed read-only access to ' . 939 'question_attempt::states through a question_attempt_step_iterator. Cannot unset.'); 940 } 941 } 942 943 944 /** 945 * Interface for things that want to be notified of signficant changes to a 946 * {@link question_usage_by_activity}. 947 * 948 * A question behaviour controls the flow of actions a student can 949 * take as they work through a question, and later, as a teacher manually grades it. 950 * 951 * @copyright 2009 The Open University 952 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 953 */ 954 interface question_usage_observer { 955 /** Called when a field of the question_usage_by_activity is changed. */ 956 public function notify_modified(); 957 958 /** 959 * Called when the fields of a question attempt in this usage are modified. 960 * @param question_attempt $qa the newly added question attempt. 961 */ 962 public function notify_attempt_modified(question_attempt $qa); 963 964 /** 965 * Called when a new question attempt is added to this usage. 966 * @param question_attempt $qa the newly added question attempt. 967 */ 968 public function notify_attempt_added(question_attempt $qa); 969 970 /** 971 * Called when a new step is added to a question attempt in this usage. 972 * @param question_attempt_step $step the new step. 973 * @param question_attempt $qa the usage it is being added to. 974 * @param int $seq the sequence number of the new step. 975 */ 976 public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq); 977 978 /** 979 * Called when a new step is updated in a question attempt in this usage. 980 * @param question_attempt_step $step the step that was updated. 981 * @param question_attempt $qa the usage it is being added to. 982 * @param int $seq the sequence number of the new step. 983 */ 984 public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq); 985 986 /** 987 * Called when a new step is updated in a question attempt in this usage. 988 * @param question_attempt_step $step the step to delete. 989 * @param question_attempt $qa the usage it is being added to. 990 */ 991 public function notify_step_deleted(question_attempt_step $step, question_attempt $qa); 992 993 } 994 995 996 /** 997 * Null implmentation of the {@link question_usage_watcher} interface. 998 * Does nothing. 999 * 1000 * @copyright 2009 The Open University 1001 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1002 */ 1003 class question_usage_null_observer implements question_usage_observer { 1004 public function notify_modified() { 1005 } 1006 public function notify_attempt_modified(question_attempt $qa) { 1007 } 1008 public function notify_attempt_added(question_attempt $qa) { 1009 } 1010 public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) { 1011 } 1012 public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) { 1013 } 1014 public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) { 1015 } 1016 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |