|
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 00028 defined('MOODLE_INTERNAL') || die(); 00029 00030 00045 abstract class question_behaviour_attempt_updater { 00047 protected $qtypeupdater; 00049 protected $logger; 00051 protected $qeupdater; 00052 00057 public $qa; 00058 00060 protected $quiz; 00062 protected $attempt; 00064 protected $question; 00066 protected $qsession; 00068 protected $qstates; 00069 00074 protected $sequencenumber; 00076 protected $finishstate; 00077 00078 public function __construct($quiz, $attempt, $question, $qsession, $qstates, $logger, $qeupdater) { 00079 $this->quiz = $quiz; 00080 $this->attempt = $attempt; 00081 $this->question = $question; 00082 $this->qsession = $qsession; 00083 $this->qstates = $qstates; 00084 $this->logger = $logger; 00085 $this->qeupdater = $qeupdater; 00086 } 00087 00088 public function discard() { 00089 // Help the garbage collector, which seems to be struggling. 00090 $this->quiz = null; 00091 $this->attempt = null; 00092 $this->question = null; 00093 $this->qsession = null; 00094 $this->qstates = null; 00095 $this->qa = null; 00096 $this->qtypeupdater->discard(); 00097 $this->qtypeupdater = null; 00098 $this->logger = null; 00099 $this->qeupdater = null; 00100 } 00101 00102 protected abstract function behaviour_name(); 00103 00104 public function get_converted_qa() { 00105 $this->initialise_qa(); 00106 $this->convert_steps(); 00107 return $this->qa; 00108 } 00109 00110 protected function create_missing_first_step() { 00111 $step = new stdClass(); 00112 $step->state = 'todo'; 00113 $step->data = array(); 00114 $step->fraction = null; 00115 $step->timecreated = $this->attempt->timestart ? $this->attempt->timestart : time(); 00116 $step->userid = $this->attempt->userid; 00117 $this->qtypeupdater->supply_missing_first_step_data($step->data); 00118 return $step; 00119 } 00120 00121 public function supply_missing_qa() { 00122 $this->initialise_qa(); 00123 $this->qa->timemodified = $this->attempt->timestart; 00124 $this->sequencenumber = 0; 00125 $this->add_step($this->create_missing_first_step()); 00126 return $this->qa; 00127 } 00128 00129 protected function initialise_qa() { 00130 $this->qtypeupdater = $this->make_qtype_updater(); 00131 00132 $qa = new stdClass(); 00133 $qa->questionid = $this->question->id; 00134 $qa->variant = 1; 00135 $qa->behaviour = $this->behaviour_name(); 00136 $qa->questionsummary = $this->qtypeupdater->question_summary($this->question); 00137 $qa->rightanswer = $this->qtypeupdater->right_answer($this->question); 00138 $qa->maxmark = $this->question->maxmark; 00139 $qa->minfraction = 0; 00140 $qa->flagged = 0; 00141 $qa->responsesummary = ''; 00142 $qa->timemodified = 0; 00143 $qa->steps = array(); 00144 00145 $this->qa = $qa; 00146 } 00147 00148 protected function convert_steps() { 00149 $this->finishstate = null; 00150 $this->startstate = null; 00151 $this->sequencenumber = 0; 00152 foreach ($this->qstates as $state) { 00153 $this->process_state($state); 00154 } 00155 $this->finish_up(); 00156 } 00157 00158 protected function process_state($state) { 00159 $step = $this->make_step($state); 00160 $method = 'process' . $state->event; 00161 $this->$method($step, $state); 00162 } 00163 00164 protected function finish_up() { 00165 } 00166 00167 protected function add_step($step) { 00168 $step->sequencenumber = $this->sequencenumber; 00169 $this->qa->steps[] = $step; 00170 $this->sequencenumber++; 00171 } 00172 00173 protected function discard_last_state() { 00174 array_pop($this->qa->steps); 00175 $this->sequencenumber--; 00176 } 00177 00178 protected function unexpected_event($state) { 00179 throw new coding_exception("Unexpected event {$state->event} in state {$state->id} in question session {$this->qsession->id}."); 00180 } 00181 00182 protected function process0($step, $state) { 00183 if ($this->startstate) { 00184 if ($state->answer == reset($this->qstates)->answer) { 00185 return; 00186 } else if ($this->quiz->attemptonlast && $this->sequencenumber == 1) { 00187 // There was a bug in attemptonlast in the past, which meant that 00188 // it created two inconsistent open states, with the second taking 00189 // priority. Simulate that be discarding the first open state, then 00190 // continuing. 00191 $this->logger->log_assumption("Ignoring bogus state in attempt at question {$state->question}"); 00192 $this->sequencenumber = 0; 00193 $this->qa->steps = array(); 00194 } else if ($this->qtypeupdater->is_blank_answer($state)) { 00195 $this->logger->log_assumption("Ignoring second start state with blank answer in attempt at question {$state->question}"); 00196 return; 00197 } else { 00198 throw new coding_exception("Two inconsistent open states for question session {$this->qsession->id}."); 00199 } 00200 } 00201 $step->state = 'todo'; 00202 $this->startstate = $state; 00203 $this->add_step($step); 00204 } 00205 00206 protected function process1($step, $state) { 00207 $this->unexpected_event($state); 00208 } 00209 00210 protected function process2($step, $state) { 00211 if ($this->qtypeupdater->was_answered($state)) { 00212 $step->state = 'complete'; 00213 } else { 00214 $step->state = 'todo'; 00215 } 00216 $this->add_step($step); 00217 } 00218 00219 protected function process3($step, $state) { 00220 return $this->process6($step, $state); 00221 } 00222 00223 protected function process4($step, $state) { 00224 $this->unexpected_event($state); 00225 } 00226 00227 protected function process5($step, $state) { 00228 $this->unexpected_event($state); 00229 } 00230 00231 protected abstract function process6($step, $state); 00232 protected abstract function process7($step, $state); 00233 00234 protected function process8($step, $state) { 00235 return $this->process6($step, $state); 00236 } 00237 00238 protected function process9($step, $state) { 00239 if (!$this->finishstate) { 00240 $submitstate = clone($state); 00241 $submitstate->event = 8; 00242 $submitstate->grade = 0; 00243 $this->process_state($submitstate); 00244 } 00245 00246 $step->data['-comment'] = $this->qsession->manualcomment; 00247 if ($this->question->maxmark > 0) { 00248 $step->fraction = $state->grade / $this->question->maxmark; 00249 $step->state = $this->manual_graded_state_for_fraction($step->fraction); 00250 $step->data['-mark'] = $state->grade; 00251 $step->data['-maxmark'] = $this->question->maxmark; 00252 } else { 00253 $step->state = 'manfinished'; 00254 } 00255 unset($step->data['answer']); 00256 $step->userid = null; 00257 $this->add_step($step); 00258 } 00259 00264 protected function make_qtype_updater() { 00265 global $CFG; 00266 00267 if ($this->question->qtype == 'deleted') { 00268 return new question_deleted_question_attempt_updater( 00269 $this, $this->question, $this->logger, $this->qeupdater); 00270 } 00271 00272 $path = $CFG->dirroot . '/question/type/' . $this->question->qtype . '/db/upgradelib.php'; 00273 if (!is_readable($path)) { 00274 throw new coding_exception("Question type {$this->question->qtype} 00275 is missing important code (the file {$path}) 00276 required to run the upgrade to the new question engine."); 00277 } 00278 include_once($path); 00279 $class = 'qtype_' . $this->question->qtype . '_qe2_attempt_updater'; 00280 if (!class_exists($class)) { 00281 throw new coding_exception("Question type {$this->question->qtype} 00282 is missing important code (the class {$class}) 00283 required to run the upgrade to the new question engine."); 00284 } 00285 return new $class($this, $this->question, $this->logger, $this->qeupdater); 00286 } 00287 00288 public function to_text($html) { 00289 return trim(html_to_text($html, 0, false)); 00290 } 00291 00292 protected function graded_state_for_fraction($fraction) { 00293 if ($fraction < 0.000001) { 00294 return 'gradedwrong'; 00295 } else if ($fraction > 0.999999) { 00296 return 'gradedright'; 00297 } else { 00298 return 'gradedpartial'; 00299 } 00300 } 00301 00302 protected function manual_graded_state_for_fraction($fraction) { 00303 if ($fraction < 0.000001) { 00304 return 'mangrwrong'; 00305 } else if ($fraction > 0.999999) { 00306 return 'mangrright'; 00307 } else { 00308 return 'mangrpartial'; 00309 } 00310 } 00311 00312 protected function make_step($state){ 00313 $step = new stdClass(); 00314 $step->data = array(); 00315 00316 if ($state->event == 0 || $this->sequencenumber == 0) { 00317 $this->qtypeupdater->set_first_step_data_elements($state, $step->data); 00318 } else { 00319 $this->qtypeupdater->set_data_elements_for_step($state, $step->data); 00320 } 00321 00322 $step->fraction = null; 00323 $step->timecreated = $state->timestamp ? $state->timestamp : time(); 00324 $step->userid = $this->attempt->userid; 00325 00326 $summary = $this->qtypeupdater->response_summary($state); 00327 if (!is_null($summary)) { 00328 $this->qa->responsesummary = $summary; 00329 } 00330 $this->qa->timemodified = max($this->qa->timemodified, $state->timestamp); 00331 00332 return $step; 00333 } 00334 } 00335 00336 00337 class qbehaviour_deferredfeedback_converter extends question_behaviour_attempt_updater { 00338 protected function behaviour_name() { 00339 return 'deferredfeedback'; 00340 } 00341 00342 protected function process6($step, $state) { 00343 if (!$this->startstate) { 00344 $this->logger->log_assumption("Ignoring bogus submit before open in attempt at question {$state->question}"); 00345 // WTF, but this has happened a few times in our DB. It seems it is safe to ignore. 00346 return; 00347 } 00348 00349 if ($this->finishstate) { 00350 if ($this->finishstate->answer != $state->answer || 00351 $this->finishstate->grade != $state->grade || 00352 $this->finishstate->raw_grade != $state->raw_grade || 00353 $this->finishstate->penalty != $state->penalty) { 00354 $this->logger->log_assumption("Two inconsistent finish states found for question session {$this->qsession->id} in attempt at question {$state->question} keeping the later one."); 00355 $this->discard_last_state(); 00356 } else { 00357 $this->logger->log_assumption("Ignoring extra finish states in attempt at question {$state->question}"); 00358 return; 00359 } 00360 } 00361 00362 if ($this->question->maxmark > 0) { 00363 $step->fraction = $state->grade / $this->question->maxmark; 00364 $step->state = $this->graded_state_for_fraction($step->fraction); 00365 } else { 00366 $step->state = 'finished'; 00367 } 00368 $step->data['-finish'] = '1'; 00369 $this->finishstate = $state; 00370 $this->add_step($step); 00371 } 00372 00373 protected function process7($step, $state) { 00374 $this->unexpected_event($state); 00375 } 00376 } 00377 00378 00379 class qbehaviour_manualgraded_converter extends question_behaviour_attempt_updater { 00380 protected function behaviour_name() { 00381 return 'manualgraded'; 00382 } 00383 00384 protected function process6($step, $state) { 00385 $step->state = 'needsgrading'; 00386 if (!$this->finishstate) { 00387 $step->data['-finish'] = '1'; 00388 $this->finishstate = $state; 00389 } 00390 $this->add_step($step); 00391 } 00392 00393 protected function process7($step, $state) { 00394 return $this->process2($step, $state); 00395 } 00396 } 00397 00398 00399 class qbehaviour_informationitem_converter extends question_behaviour_attempt_updater { 00400 protected function behaviour_name() { 00401 return 'informationitem'; 00402 } 00403 00404 protected function process0($step, $state) { 00405 if ($this->startstate) { 00406 return; 00407 } 00408 $step->state = 'todo'; 00409 $this->startstate = $state; 00410 $this->add_step($step); 00411 } 00412 00413 protected function process2($step, $state) { 00414 $this->unexpected_event($state); 00415 } 00416 00417 protected function process3($step, $state) { 00418 $this->unexpected_event($state); 00419 } 00420 00421 protected function process6($step, $state) { 00422 if ($this->finishstate) { 00423 return; 00424 } 00425 00426 $step->state = 'finished'; 00427 $step->data['-finish'] = '1'; 00428 $this->finishstate = $state; 00429 $this->add_step($step); 00430 } 00431 00432 protected function process7($step, $state) { 00433 return $this->process6($step, $state); 00434 } 00435 00436 protected function process8($step, $state) { 00437 return $this->process6($step, $state); 00438 } 00439 } 00440 00441 00442 class qbehaviour_adaptive_converter extends question_behaviour_attempt_updater { 00443 protected $try; 00444 protected $laststepwasatry = false; 00445 protected $finished = false; 00446 protected $bestrawgrade = 0; 00447 00448 protected function behaviour_name() { 00449 return 'adaptive'; 00450 } 00451 00452 protected function finish_up() { 00453 parent::finish_up(); 00454 if ($this->finishstate || !$this->attempt->timefinish) { 00455 return; 00456 } 00457 00458 $state = end($this->qstates); 00459 $step = $this->make_step($state); 00460 $this->process6($step, $state); 00461 } 00462 00463 protected function process0($step, $state) { 00464 $this->try = 1; 00465 $this->laststepwasatry = false; 00466 parent::process0($step, $state); 00467 } 00468 00469 protected function process2($step, $state) { 00470 if ($this->finishstate) { 00471 $this->logger->log_assumption("Ignoring bogus save after submit in an " . 00472 "adaptive attempt at question {$state->question} " . 00473 "(question session {$this->qsession->id})"); 00474 return; 00475 } 00476 00477 if ($this->question->maxmark > 0) { 00478 $step->fraction = $state->grade / $this->question->maxmark; 00479 } 00480 00481 $this->laststepwasatry = false; 00482 parent::process2($step, $state); 00483 } 00484 00485 protected function process3($step, $state) { 00486 if ($this->question->maxmark > 0) { 00487 $step->fraction = $state->grade / $this->question->maxmark; 00488 if ($this->graded_state_for_fraction($step->fraction) == 'gradedright') { 00489 $step->state = 'complete'; 00490 } else { 00491 $step->state = 'todo'; 00492 } 00493 } else { 00494 $step->state = 'complete'; 00495 } 00496 00497 $this->bestrawgrade = max($state->raw_grade, $this->bestrawgrade); 00498 00499 $step->data['-_try'] = $this->try; 00500 $this->try += 1; 00501 $this->laststepwasatry = true; 00502 if ($this->question->maxmark > 0) { 00503 $step->data['-_rawfraction'] = $state->raw_grade / $this->question->maxmark; 00504 } else { 00505 $step->data['-_rawfraction'] = 0; 00506 } 00507 $step->data['-submit'] = 1; 00508 00509 $this->add_step($step); 00510 } 00511 00512 protected function process6($step, $state) { 00513 if ($this->finishstate) { 00514 if (!$this->qtypeupdater->compare_answers($this->finishstate->answer, $state->answer) || 00515 $this->finishstate->grade != $state->grade || 00516 $this->finishstate->raw_grade != $state->raw_grade || 00517 $this->finishstate->penalty != $state->penalty) { 00518 throw new coding_exception("Two inconsistent finish states found for question session {$this->qsession->id}."); 00519 } else { 00520 $this->logger->log_assumption("Ignoring extra finish states in attempt at question {$state->question}"); 00521 return; 00522 } 00523 } 00524 00525 $this->bestrawgrade = max($state->raw_grade, $this->bestrawgrade); 00526 00527 if ($this->question->maxmark > 0) { 00528 $step->fraction = $state->grade / $this->question->maxmark; 00529 $step->state = $this->graded_state_for_fraction( 00530 $this->bestrawgrade / $this->question->maxmark); 00531 } else { 00532 $step->state = 'finished'; 00533 } 00534 00535 $step->data['-finish'] = 1; 00536 if ($this->laststepwasatry) { 00537 $this->try -= 1; 00538 } 00539 $step->data['-_try'] = $this->try; 00540 if ($this->question->maxmark > 0) { 00541 $step->data['-_rawfraction'] = $state->raw_grade / $this->question->maxmark; 00542 } else { 00543 $step->data['-_rawfraction'] = 0; 00544 } 00545 00546 $this->finishstate = $state; 00547 $this->add_step($step); 00548 } 00549 00550 protected function process7($step, $state) { 00551 $this->unexpected_event($state); 00552 } 00553 } 00554 00555 00556 class qbehaviour_adaptivenopenalty_converter extends qbehaviour_adaptive_converter { 00557 protected function behaviour_name() { 00558 return 'adaptivenopenalty'; 00559 } 00560 }