|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00027 defined('MOODLE_INTERNAL') || die(); 00028 00029 00044 class question_attempt { 00049 const USE_RAW_DATA = 'use raw data'; 00050 00055 const PARAM_MARK = 'parammark'; 00056 00061 const PARAM_FILES = 'paramfiles'; 00062 00067 const PARAM_CLEANHTML_FILES = 'paramcleanhtmlfiles'; 00068 00070 protected $id = null; 00071 00073 protected $usageid; 00074 00076 protected $slot = null; 00077 00082 protected $behaviour = null; 00083 00085 protected $question; 00086 00088 protected $variant; 00089 00091 protected $maxmark; 00092 00097 protected $minfraction = null; 00098 00103 protected $questionsummary = null; 00104 00109 protected $responsesummary = null; 00110 00116 protected $rightanswer = null; 00117 00119 protected $steps = array(); 00120 00122 protected $flagged = false; 00123 00125 protected $observer; 00126 00131 const KEEP = true; 00132 const DISCARD = false; 00148 public function __construct(question_definition $question, $usageid, 00149 question_usage_observer $observer = null, $maxmark = null) { 00150 $this->question = $question; 00151 $this->usageid = $usageid; 00152 if (is_null($observer)) { 00153 $observer = new question_usage_null_observer(); 00154 } 00155 $this->observer = $observer; 00156 if (!is_null($maxmark)) { 00157 $this->maxmark = $maxmark; 00158 } else { 00159 $this->maxmark = $question->defaultmark; 00160 } 00161 } 00162 00168 public function get_full_qa() { 00169 return $this; 00170 } 00171 00173 public function get_question() { 00174 return $this->question; 00175 } 00176 00181 public function get_variant() { 00182 return $this->variant; 00183 } 00184 00190 public function set_slot($slot) { 00191 $this->slot = $slot; 00192 } 00193 00195 public function get_slot() { 00196 return $this->slot; 00197 } 00198 00203 public function get_database_id() { 00204 return $this->id; 00205 } 00206 00212 public function set_database_id($id) { 00213 $this->id = $id; 00214 } 00215 00221 public function set_observer($observer) { 00222 $this->observer = $observer; 00223 } 00224 00226 public function get_usage_id() { 00227 return $this->usageid; 00228 } 00229 00235 public function set_usage_id($usageid) { 00236 $this->usageid = $usageid; 00237 } 00238 00240 public function get_behaviour_name() { 00241 return $this->behaviour->get_name(); 00242 } 00243 00248 public function get_behaviour() { 00249 return $this->behaviour; 00250 } 00251 00256 public function set_flagged($flagged) { 00257 $this->flagged = $flagged; 00258 $this->observer->notify_attempt_modified($this); 00259 } 00260 00262 public function is_flagged() { 00263 return $this->flagged; 00264 } 00265 00272 public function get_flag_field_name() { 00273 return $this->get_control_field_name('flagged'); 00274 } 00275 00286 public function get_qt_field_name($varname) { 00287 return $this->get_field_prefix() . $varname; 00288 } 00289 00300 public function get_behaviour_field_name($varname) { 00301 return $this->get_field_prefix() . '-' . $varname; 00302 } 00303 00313 public function get_control_field_name($varname) { 00314 return $this->get_field_prefix() . ':' . $varname; 00315 } 00316 00327 public function get_field_prefix() { 00328 return 'q' . $this->usageid . ':' . $this->slot . '_'; 00329 } 00330 00337 public function get_step($i) { 00338 if ($i < 0 || $i >= count($this->steps)) { 00339 throw new coding_exception('Index out of bounds in question_attempt::get_step.'); 00340 } 00341 return $this->steps[$i]; 00342 } 00343 00349 public function get_num_steps() { 00350 return count($this->steps); 00351 } 00352 00358 public function get_last_step() { 00359 if (count($this->steps) == 0) { 00360 return new question_null_step(); 00361 } 00362 return end($this->steps); 00363 } 00364 00369 public function get_step_iterator() { 00370 return new question_attempt_step_iterator($this); 00371 } 00372 00381 public function get_full_step_iterator() { 00382 return $this->get_step_iterator(); 00383 } 00384 00389 public function get_reverse_step_iterator() { 00390 return new question_attempt_reverse_step_iterator($this); 00391 } 00392 00402 public function get_last_qt_data($default = array()) { 00403 foreach ($this->get_reverse_step_iterator() as $step) { 00404 $response = $step->get_qt_data(); 00405 if (!empty($response)) { 00406 return $response; 00407 } 00408 } 00409 return $default; 00410 } 00411 00418 public function get_last_step_with_qt_var($name) { 00419 foreach ($this->get_reverse_step_iterator() as $step) { 00420 if ($step->has_qt_var($name)) { 00421 return $step; 00422 } 00423 } 00424 return new question_attempt_step_read_only(); 00425 } 00426 00433 public function get_last_step_with_behaviour_var($name) { 00434 foreach ($this->get_reverse_step_iterator() as $step) { 00435 if ($step->has_behaviour_var($name)) { 00436 return $step; 00437 } 00438 } 00439 return new question_attempt_step_read_only(); 00440 } 00441 00452 public function get_last_qt_var($name, $default = null) { 00453 $step = $this->get_last_step_with_qt_var($name); 00454 if ($step->has_qt_var($name)) { 00455 return $step->get_qt_var($name); 00456 } else { 00457 return $default; 00458 } 00459 } 00460 00468 public function get_last_qt_files($name, $contextid) { 00469 foreach ($this->get_reverse_step_iterator() as $step) { 00470 if ($step->has_qt_var($name)) { 00471 return $step->get_qt_files($name, $contextid); 00472 } 00473 } 00474 return array(); 00475 } 00476 00483 public function get_response_file_url(stored_file $file) { 00484 return file_encode_url(new moodle_url('/pluginfile.php'), '/' . implode('/', array( 00485 $file->get_contextid(), 00486 $file->get_component(), 00487 $file->get_filearea(), 00488 $this->usageid, 00489 $this->slot, 00490 $file->get_itemid())) . 00491 $file->get_filepath() . $file->get_filename(), true); 00492 } 00493 00503 public function prepare_response_files_draft_itemid($name, $contextid) { 00504 foreach ($this->get_reverse_step_iterator() as $step) { 00505 if ($step->has_qt_var($name)) { 00506 return $step->prepare_response_files_draft_itemid($name, $contextid); 00507 } 00508 } 00509 00510 // No files yet. 00511 $draftid = 0; // Will be filled in by file_prepare_draft_area. 00512 file_prepare_draft_area($draftid, $contextid, 'question', 'response_' . $name, null); 00513 return $draftid; 00514 } 00515 00526 public function get_last_behaviour_var($name, $default = null) { 00527 foreach ($this->get_reverse_step_iterator() as $step) { 00528 if ($step->has_behaviour_var($name)) { 00529 return $step->get_behaviour_var($name); 00530 } 00531 } 00532 return $default; 00533 } 00534 00540 public function get_state() { 00541 return $this->get_last_step()->get_state(); 00542 } 00543 00549 public function get_state_string($showcorrectness) { 00550 return $this->behaviour->get_state_string($showcorrectness); 00551 } 00552 00558 public function get_state_class($showcorrectness) { 00559 return $this->get_state()->get_state_class($showcorrectness); 00560 } 00561 00565 public function get_last_action_time() { 00566 return $this->get_last_step()->get_timecreated(); 00567 } 00568 00574 public function get_fraction() { 00575 return $this->get_last_step()->get_fraction(); 00576 } 00577 00579 public function has_marks() { 00580 // Since grades are stored in the database as NUMBER(12,7). 00581 return $this->maxmark >= 0.00000005; 00582 } 00583 00588 public function get_mark() { 00589 return $this->fraction_to_mark($this->get_fraction()); 00590 } 00591 00599 public function get_current_manual_mark() { 00600 $mark = $this->get_submitted_var($this->get_behaviour_field_name('mark'), question_attempt::PARAM_MARK); 00601 if (is_null($mark)) { 00602 return $this->get_mark(); 00603 } else { 00604 return $mark; 00605 } 00606 } 00607 00612 public function fraction_to_mark($fraction) { 00613 if (is_null($fraction)) { 00614 return null; 00615 } 00616 return $fraction * $this->maxmark; 00617 } 00618 00620 public function get_max_mark() { 00621 return $this->maxmark; 00622 } 00623 00625 public function get_min_fraction() { 00626 if (is_null($this->minfraction)) { 00627 throw new coding_exception('This question_attempt has not been started yet, the min fraction is not yet konwn.'); 00628 } 00629 return $this->minfraction; 00630 } 00631 00638 public function format_mark($dp) { 00639 return $this->format_fraction_as_mark($this->get_fraction(), $dp); 00640 } 00641 00648 public function format_fraction_as_mark($fraction, $dp) { 00649 return format_float($this->fraction_to_mark($fraction), $dp); 00650 } 00651 00659 public function format_max_mark($dp) { 00660 return format_float($this->maxmark, $dp); 00661 } 00662 00667 public function get_applicable_hint() { 00668 return $this->behaviour->get_applicable_hint(); 00669 } 00670 00676 public function summarise_action(question_attempt_step $step) { 00677 return $this->behaviour->summarise_action($step); 00678 } 00679 00685 protected function extra_file_path_components() { 00686 return array($this->get_usage_id(), $this->get_slot()); 00687 } 00688 00698 public function rewrite_pluginfile_urls($text, $component, $filearea, $itemid) { 00699 return question_rewrite_question_urls($text, 'pluginfile.php', 00700 $this->question->contextid, $component, $filearea, 00701 $this->extra_file_path_components(), $itemid); 00702 } 00703 00714 public function rewrite_response_pluginfile_urls($text, $contextid, $name, 00715 question_attempt_step $step) { 00716 return $step->rewrite_response_pluginfile_urls($text, $contextid, $name, 00717 $this->extra_file_path_components()); 00718 } 00719 00728 public function render($options, $number, $page = null) { 00729 if (is_null($page)) { 00730 global $PAGE; 00731 $page = $PAGE; 00732 } 00733 $qoutput = $page->get_renderer('core', 'question'); 00734 $qtoutput = $this->question->get_renderer($page); 00735 return $this->behaviour->render($options, $number, $qoutput, $qtoutput); 00736 } 00737 00743 public function render_head_html($page = null) { 00744 if (is_null($page)) { 00745 global $PAGE; 00746 $page = $PAGE; 00747 } 00748 // TODO go via behaviour. 00749 return $this->question->get_renderer($page)->head_code($this) . 00750 $this->behaviour->get_renderer($page)->head_code($this); 00751 } 00752 00763 public function render_at_step($seq, $options, $number, $preferredbehaviour) { 00764 $restrictedqa = new question_attempt_with_restricted_history($this, $seq, $preferredbehaviour); 00765 return $restrictedqa->render($options, $number); 00766 } 00767 00777 public function check_file_access($options, $component, $filearea, $args, $forcedownload) { 00778 return $this->behaviour->check_file_access($options, $component, $filearea, $args, $forcedownload); 00779 } 00780 00785 protected function add_step(question_attempt_step $step) { 00786 $this->steps[] = $step; 00787 end($this->steps); 00788 $this->observer->notify_step_added($step, $this, key($this->steps)); 00789 } 00790 00796 public function select_variant(question_variant_selection_strategy $variantstrategy) { 00797 return $variantstrategy->choose_variant($this->get_question()->get_num_variants(), 00798 $this->get_question()->get_variants_selection_seed()); 00799 } 00800 00817 public function start($preferredbehaviour, $variant, $submitteddata = array(), 00818 $timestamp = null, $userid = null, $existingstepid = null) { 00819 00820 // Initialise the behaviour. 00821 $this->variant = $variant; 00822 if (is_string($preferredbehaviour)) { 00823 $this->behaviour = 00824 $this->question->make_behaviour($this, $preferredbehaviour); 00825 } else { 00826 $class = get_class($preferredbehaviour); 00827 $this->behaviour = new $class($this, $preferredbehaviour); 00828 } 00829 00830 // Record the minimum fraction. 00831 $this->minfraction = $this->behaviour->get_min_fraction(); 00832 00833 // Initialise the first step. 00834 $firststep = new question_attempt_step($submitteddata, $timestamp, $userid, $existingstepid); 00835 $firststep->set_state(question_state::$todo); 00836 if ($submitteddata) { 00837 $this->question->apply_attempt_state($firststep); 00838 } else { 00839 $this->behaviour->init_first_step($firststep, $variant); 00840 } 00841 $this->add_step($firststep); 00842 00843 // Record questionline and correct answer. 00844 $this->questionsummary = $this->behaviour->get_question_summary(); 00845 $this->rightanswer = $this->behaviour->get_right_answer_summary(); 00846 } 00847 00858 public function start_based_on(question_attempt $oldqa) { 00859 $this->start($oldqa->behaviour, $oldqa->get_variant(), $oldqa->get_resume_data()); 00860 } 00861 00867 protected function get_resume_data() { 00868 return $this->behaviour->get_resume_data(); 00869 } 00870 00882 public function get_submitted_var($name, $type, $postdata = null) { 00883 switch ($type) { 00884 case self::PARAM_MARK: 00885 // Special case to work around PARAM_NUMBER converting '' to 0. 00886 $mark = $this->get_submitted_var($name, PARAM_RAW_TRIMMED, $postdata); 00887 if ($mark === '') { 00888 return $mark; 00889 } else { 00890 return $this->get_submitted_var($name, PARAM_NUMBER, $postdata); 00891 } 00892 00893 case self::PARAM_FILES: 00894 return $this->process_response_files($name, $name, $postdata); 00895 00896 case self::PARAM_CLEANHTML_FILES: 00897 $var = $this->get_submitted_var($name, PARAM_CLEANHTML, $postdata); 00898 return $this->process_response_files($name, $name . ':itemid', $postdata, $var); 00899 00900 default: 00901 if (is_null($postdata)) { 00902 $var = optional_param($name, null, $type); 00903 } else if (array_key_exists($name, $postdata)) { 00904 $var = clean_param($postdata[$name], $type); 00905 } else { 00906 $var = null; 00907 } 00908 00909 return $var; 00910 } 00911 } 00912 00923 protected function process_response_files($name, $draftidname, $postdata = null, $text = null) { 00924 if ($postdata) { 00925 // There can be no files with test data (at the moment). 00926 return null; 00927 } 00928 00929 $draftitemid = file_get_submitted_draft_itemid($draftidname); 00930 if (!$draftitemid) { 00931 return null; 00932 } 00933 00934 return new question_file_saver($draftitemid, 'question', 'response_' . 00935 str_replace($this->get_field_prefix(), '', $name), $text); 00936 } 00937 00944 protected function get_expected_data($expected, $postdata, $extraprefix) { 00945 $submitteddata = array(); 00946 foreach ($expected as $name => $type) { 00947 $value = $this->get_submitted_var( 00948 $this->get_field_prefix() . $extraprefix . $name, $type, $postdata); 00949 if (!is_null($value)) { 00950 $submitteddata[$extraprefix . $name] = $value; 00951 } 00952 } 00953 return $submitteddata; 00954 } 00955 00961 protected function get_all_submitted_qt_vars($postdata) { 00962 if (is_null($postdata)) { 00963 $postdata = $_POST; 00964 } 00965 00966 $pattern = '/^' . preg_quote($this->get_field_prefix()) . '[^-:]/'; 00967 $prefixlen = strlen($this->get_field_prefix()); 00968 00969 $submitteddata = array(); 00970 foreach ($_POST as $name => $value) { 00971 if (preg_match($pattern, $name)) { 00972 $submitteddata[substr($name, $prefixlen)] = $value; 00973 } 00974 } 00975 00976 return $submitteddata; 00977 } 00978 00986 public function get_submitted_data($postdata = null) { 00987 $submitteddata = $this->get_expected_data( 00988 $this->behaviour->get_expected_data(), $postdata, '-'); 00989 00990 $expected = $this->behaviour->get_expected_qt_data(); 00991 if ($expected === self::USE_RAW_DATA) { 00992 $submitteddata += $this->get_all_submitted_qt_vars($postdata); 00993 } else { 00994 $submitteddata += $this->get_expected_data($expected, $postdata, ''); 00995 } 00996 return $submitteddata; 00997 } 00998 01005 public function get_correct_response() { 01006 $response = $this->question->get_correct_response(); 01007 if (is_null($response)) { 01008 return null; 01009 } 01010 $imvars = $this->behaviour->get_correct_response(); 01011 foreach ($imvars as $name => $value) { 01012 $response['-' . $name] = $value; 01013 } 01014 return $response; 01015 } 01016 01023 public function set_question_summary($questionsummary) { 01024 $this->questionsummary = $questionsummary; 01025 $this->observer->notify_attempt_modified($this); 01026 } 01027 01031 public function get_question_summary() { 01032 return $this->questionsummary; 01033 } 01034 01038 public function get_response_summary() { 01039 return $this->responsesummary; 01040 } 01041 01045 public function get_right_answer_summary() { 01046 return $this->rightanswer; 01047 } 01048 01055 public function process_action($submitteddata, $timestamp = null, $userid = null, $existingstepid = null) { 01056 $pendingstep = new question_attempt_pending_step($submitteddata, $timestamp, $userid, $existingstepid); 01057 if ($this->behaviour->process_action($pendingstep) == self::KEEP) { 01058 $this->add_step($pendingstep); 01059 if ($pendingstep->response_summary_changed()) { 01060 $this->responsesummary = $pendingstep->get_new_response_summary(); 01061 } 01062 } 01063 } 01064 01074 public function finish($timestamp = null, $userid = null) { 01075 $this->process_action(array('-finish' => 1), $timestamp, $userid); 01076 } 01077 01085 public function regrade(question_attempt $oldqa, $finished) { 01086 $first = true; 01087 foreach ($oldqa->get_step_iterator() as $step) { 01088 $this->observer->notify_step_deleted($step, $this); 01089 if ($first) { 01090 $first = false; 01091 $this->start($oldqa->behaviour, $oldqa->get_variant(), $step->get_all_data(), 01092 $step->get_timecreated(), $step->get_user_id(), $step->get_id()); 01093 } else { 01094 $this->process_action($step->get_submitted_data(), 01095 $step->get_timecreated(), $step->get_user_id(), $step->get_id()); 01096 } 01097 } 01098 if ($finished) { 01099 $this->finish(); 01100 } 01101 } 01102 01111 public function manual_grade($comment, $mark, $timestamp = null, $userid = null) { 01112 $submitteddata = array('-comment' => $comment); 01113 if (!is_null($mark)) { 01114 $submitteddata['-mark'] = $mark; 01115 $submitteddata['-maxmark'] = $this->maxmark; 01116 } 01117 $this->process_action($submitteddata, $timestamp, $userid); 01118 } 01119 01121 public function has_manual_comment() { 01122 foreach ($this->steps as $step) { 01123 if ($step->has_behaviour_var('comment')) { 01124 return true; 01125 } 01126 } 01127 return false; 01128 } 01129 01134 public function get_manual_comment() { 01135 foreach ($this->get_reverse_step_iterator() as $step) { 01136 if ($step->has_behaviour_var('comment')) { 01137 return array($step->get_behaviour_var('comment'), 01138 $step->get_behaviour_var('commentformat')); 01139 } 01140 } 01141 return array(null, null); 01142 } 01143 01151 public function classify_response() { 01152 return $this->behaviour->classify_response(); 01153 } 01154 01164 public static function load_from_records($records, $questionattemptid, 01165 question_usage_observer $observer, $preferredbehaviour) { 01166 $record = $records->current(); 01167 while ($record->questionattemptid != $questionattemptid) { 01168 $record = $records->next(); 01169 if (!$records->valid()) { 01170 throw new coding_exception("Question attempt $questionattemptid not found in the database."); 01171 } 01172 $record = $records->current(); 01173 } 01174 01175 try { 01176 $question = question_bank::load_question($record->questionid); 01177 } catch (Exception $e) { 01178 // The question must have been deleted somehow. Create a missing 01179 // question to use in its place. 01180 $question = question_bank::get_qtype('missingtype')->make_deleted_instance( 01181 $record->questionid, $record->maxmark + 0); 01182 } 01183 01184 $qa = new question_attempt($question, $record->questionusageid, 01185 null, $record->maxmark + 0); 01186 $qa->set_database_id($record->questionattemptid); 01187 $qa->set_slot($record->slot); 01188 $qa->variant = $record->variant + 0; 01189 $qa->minfraction = $record->minfraction + 0; 01190 $qa->set_flagged($record->flagged); 01191 $qa->questionsummary = $record->questionsummary; 01192 $qa->rightanswer = $record->rightanswer; 01193 $qa->responsesummary = $record->responsesummary; 01194 $qa->timemodified = $record->timemodified; 01195 01196 $qa->behaviour = question_engine::make_behaviour( 01197 $record->behaviour, $qa, $preferredbehaviour); 01198 01199 $i = 0; 01200 while ($record && $record->questionattemptid == $questionattemptid && !is_null($record->attemptstepid)) { 01201 $qa->steps[$i] = question_attempt_step::load_from_records($records, $record->attemptstepid); 01202 if ($i == 0) { 01203 $question->apply_attempt_state($qa->steps[0]); 01204 } 01205 $i++; 01206 if ($records->valid()) { 01207 $record = $records->current(); 01208 } else { 01209 $record = false; 01210 } 01211 } 01212 01213 $qa->observer = $observer; 01214 01215 return $qa; 01216 } 01217 } 01218 01219 01229 class question_attempt_with_restricted_history extends question_attempt { 01233 protected $baseqa; 01234 01242 public function __construct(question_attempt $baseqa, $lastseq, $preferredbehaviour) { 01243 $this->baseqa = $baseqa->get_full_qa(); 01244 01245 if ($lastseq < 0 || $lastseq >= $this->baseqa->get_num_steps()) { 01246 throw new coding_exception('$lastseq out of range', $lastseq); 01247 } 01248 01249 $this->steps = array_slice($this->baseqa->steps, 0, $lastseq + 1); 01250 $this->observer = new question_usage_null_observer(); 01251 01252 // This should be a straight copy of all the remaining fields. 01253 $this->id = $this->baseqa->id; 01254 $this->usageid = $this->baseqa->usageid; 01255 $this->slot = $this->baseqa->slot; 01256 $this->question = $this->baseqa->question; 01257 $this->maxmark = $this->baseqa->maxmark; 01258 $this->minfraction = $this->baseqa->minfraction; 01259 $this->questionsummary = $this->baseqa->questionsummary; 01260 $this->responsesummary = $this->baseqa->responsesummary; 01261 $this->rightanswer = $this->baseqa->rightanswer; 01262 $this->flagged = $this->baseqa->flagged; 01263 01264 // Except behaviour, where we need to create a new one. 01265 $this->behaviour = question_engine::make_behaviour( 01266 $this->baseqa->get_behaviour_name(), $this, $preferredbehaviour); 01267 } 01268 01269 public function get_full_qa() { 01270 return $this->baseqa; 01271 } 01272 01273 public function get_full_step_iterator() { 01274 return $this->baseqa->get_step_iterator(); 01275 } 01276 01277 protected function add_step(question_attempt_step $step) { 01278 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 01279 } 01280 public function process_action($submitteddata, $timestamp = null, $userid = null) { 01281 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 01282 } 01283 public function start($preferredbehaviour, $variant, $submitteddata = array(), $timestamp = null, $userid = null) { 01284 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 01285 } 01286 01287 public function set_database_id($id) { 01288 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 01289 } 01290 public function set_flagged($flagged) { 01291 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 01292 } 01293 public function set_slot($slot) { 01294 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 01295 } 01296 public function set_question_summary($questionsummary) { 01297 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 01298 } 01299 public function set_usage_id($usageid) { 01300 coding_exception('Cannot modify a question_attempt_with_restricted_history.'); 01301 } 01302 } 01303 01304 01314 class question_attempt_step_iterator implements Iterator, ArrayAccess { 01316 protected $qa; 01318 protected $i; 01319 01325 public function __construct(question_attempt $qa) { 01326 $this->qa = $qa; 01327 $this->rewind(); 01328 } 01329 01331 public function current() { 01332 return $this->offsetGet($this->i); 01333 } 01335 public function key() { 01336 return $this->i; 01337 } 01338 public function next() { 01339 ++$this->i; 01340 } 01341 public function rewind() { 01342 $this->i = 0; 01343 } 01345 public function valid() { 01346 return $this->offsetExists($this->i); 01347 } 01348 01350 public function offsetExists($i) { 01351 return $i >= 0 && $i < $this->qa->get_num_steps(); 01352 } 01354 public function offsetGet($i) { 01355 return $this->qa->get_step($i); 01356 } 01357 public function offsetSet($offset, $value) { 01358 throw new coding_exception('You are only allowed read-only access to question_attempt::states through a question_attempt_step_iterator. Cannot set.'); 01359 } 01360 public function offsetUnset($offset) { 01361 throw new coding_exception('You are only allowed read-only access to question_attempt::states through a question_attempt_step_iterator. Cannot unset.'); 01362 } 01363 } 01364 01365 01373 class question_attempt_reverse_step_iterator extends question_attempt_step_iterator { 01374 public function next() { 01375 --$this->i; 01376 } 01377 01378 public function rewind() { 01379 $this->i = $this->qa->get_num_steps() - 1; 01380 } 01381 }