Differences Between: [Versions 310 and 402] [Versions 310 and 403]
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 contains classes for handling the different question behaviours 19 * during upgrade. 20 * 21 * @package moodlecore 22 * @subpackage questionengine 23 * @copyright 2010 The Open University 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 */ 26 27 28 defined('MOODLE_INTERNAL') || die(); 29 30 31 /** 32 * Base class for managing the upgrade of a question using a particular behaviour. 33 * 34 * This class takes as input: 35 * 1. Various backgroud data like $quiz, $attempt and $question. 36 * 2. The data about the question session to upgrade $qsession and $qstates. 37 * Working through that data, it builds up 38 * 3. The equivalent new data $qa. This has roughly the same data as a 39 * question_attempt object belonging to the new question engine would have, but 40 * $this->qa is built up from stdClass objects. 41 * 42 * @copyright 2010 The Open University 43 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 44 */ 45 abstract class question_behaviour_attempt_updater { 46 /** @var question_qtype_attempt_updater */ 47 protected $qtypeupdater; 48 /** @var question_engine_assumption_logger */ 49 protected $logger; 50 /** @var question_engine_attempt_upgrader */ 51 protected $qeupdater; 52 53 /** 54 * @var object this is the data for the upgraded questions attempt that 55 * we are building. 56 */ 57 public $qa; 58 59 /** @var object the quiz settings. */ 60 protected $quiz; 61 /** @var object the quiz attempt data. */ 62 protected $attempt; 63 /** @var object the question definition data. */ 64 protected $question; 65 /** @var object the question session to be upgraded. */ 66 protected $qsession; 67 /** @var array the question states for the session to be upgraded. */ 68 protected $qstates; 69 70 /** 71 * @var int counts the question_steps as they are converted to 72 * question_attempt_steps. 73 */ 74 protected $sequencenumber; 75 /** @var object pointer to the state that has already finished this attempt. */ 76 protected $finishstate; 77 78 public function __construct($quiz, $attempt, $question, $qsession, $qstates, $logger, $qeupdater) { 79 $this->quiz = $quiz; 80 $this->attempt = $attempt; 81 $this->question = $question; 82 $this->qsession = $qsession; 83 $this->qstates = $qstates; 84 $this->logger = $logger; 85 $this->qeupdater = $qeupdater; 86 } 87 88 public function discard() { 89 // Help the garbage collector, which seems to be struggling. 90 $this->quiz = null; 91 $this->attempt = null; 92 $this->question = null; 93 $this->qsession = null; 94 $this->qstates = null; 95 $this->qa = null; 96 $this->qtypeupdater->discard(); 97 $this->qtypeupdater = null; 98 $this->logger = null; 99 $this->qeupdater = null; 100 } 101 102 protected abstract function behaviour_name(); 103 104 public function get_converted_qa() { 105 $this->initialise_qa(); 106 $this->convert_steps(); 107 return $this->qa; 108 } 109 110 protected function create_missing_first_step() { 111 $step = new stdClass(); 112 $step->state = 'todo'; 113 $step->data = array(); 114 $step->fraction = null; 115 $step->timecreated = $this->attempt->timestart ? $this->attempt->timestart : time(); 116 $step->userid = $this->attempt->userid; 117 $this->qtypeupdater->supply_missing_first_step_data($step->data); 118 return $step; 119 } 120 121 public function supply_missing_qa() { 122 $this->initialise_qa(); 123 $this->qa->timemodified = $this->attempt->timestart; 124 $this->sequencenumber = 0; 125 $this->add_step($this->create_missing_first_step()); 126 return $this->qa; 127 } 128 129 protected function initialise_qa() { 130 $this->qtypeupdater = $this->make_qtype_updater(); 131 132 $qa = new stdClass(); 133 $qa->questionid = $this->question->id; 134 $qa->variant = 1; 135 $qa->behaviour = $this->behaviour_name(); 136 $qa->questionsummary = $this->qtypeupdater->question_summary($this->question); 137 $qa->rightanswer = $this->qtypeupdater->right_answer($this->question); 138 $qa->maxmark = $this->question->maxmark; 139 $qa->minfraction = 0; 140 $qa->maxfraction = 1; 141 $qa->flagged = 0; 142 $qa->responsesummary = ''; 143 $qa->timemodified = 0; 144 $qa->steps = array(); 145 146 $this->qa = $qa; 147 } 148 149 protected function convert_steps() { 150 $this->finishstate = null; 151 $this->startstate = null; 152 $this->sequencenumber = 0; 153 foreach ($this->qstates as $state) { 154 $this->process_state($state); 155 } 156 $this->finish_up(); 157 } 158 159 protected function process_state($state) { 160 $step = $this->make_step($state); 161 $method = 'process' . $state->event; 162 $this->$method($step, $state); 163 } 164 165 protected function finish_up() { 166 } 167 168 protected function add_step($step) { 169 $step->sequencenumber = $this->sequencenumber; 170 $this->qa->steps[] = $step; 171 $this->sequencenumber++; 172 } 173 174 protected function discard_last_state() { 175 array_pop($this->qa->steps); 176 $this->sequencenumber--; 177 } 178 179 protected function unexpected_event($state) { 180 throw new coding_exception("Unexpected event {$state->event} in state {$state->id} in question session {$this->qsession->id}."); 181 } 182 183 protected function process0($step, $state) { 184 if ($this->startstate) { 185 if ($state->answer == reset($this->qstates)->answer) { 186 return; 187 } else if ($this->quiz->attemptonlast && $this->sequencenumber == 1) { 188 // There was a bug in attemptonlast in the past, which meant that 189 // it created two inconsistent open states, with the second taking 190 // priority. Simulate that be discarding the first open state, then 191 // continuing. 192 $this->logger->log_assumption("Ignoring bogus state in attempt at question {$state->question}"); 193 $this->sequencenumber = 0; 194 $this->qa->steps = array(); 195 } else if ($this->qtypeupdater->is_blank_answer($state)) { 196 $this->logger->log_assumption("Ignoring second start state with blank answer in attempt at question {$state->question}"); 197 return; 198 } else { 199 throw new coding_exception("Two inconsistent open states for question session {$this->qsession->id}."); 200 } 201 } 202 $step->state = 'todo'; 203 $this->startstate = $state; 204 $this->add_step($step); 205 } 206 207 protected function process1($step, $state) { 208 $this->unexpected_event($state); 209 } 210 211 protected function process2($step, $state) { 212 if ($this->qtypeupdater->was_answered($state)) { 213 $step->state = 'complete'; 214 } else { 215 $step->state = 'todo'; 216 } 217 $this->add_step($step); 218 } 219 220 protected function process3($step, $state) { 221 return $this->process6($step, $state); 222 } 223 224 protected function process4($step, $state) { 225 $this->unexpected_event($state); 226 } 227 228 protected function process5($step, $state) { 229 $this->unexpected_event($state); 230 } 231 232 protected abstract function process6($step, $state); 233 protected abstract function process7($step, $state); 234 235 protected function process8($step, $state) { 236 return $this->process6($step, $state); 237 } 238 239 protected function process9($step, $state) { 240 if (!$this->finishstate) { 241 $submitstate = clone($state); 242 $submitstate->event = 8; 243 $submitstate->grade = 0; 244 $this->process_state($submitstate); 245 } 246 247 $step->data['-comment'] = $this->qsession->manualcomment; 248 if ($this->question->maxmark > 0) { 249 $step->fraction = $state->grade / $this->question->maxmark; 250 $step->state = $this->manual_graded_state_for_fraction($step->fraction); 251 $step->data['-mark'] = $state->grade; 252 $step->data['-maxmark'] = $this->question->maxmark; 253 } else { 254 $step->state = 'manfinished'; 255 } 256 unset($step->data['answer']); 257 $step->userid = null; 258 $this->add_step($step); 259 } 260 261 /** 262 * @param object $question a question definition 263 * @return qtype_updater 264 */ 265 protected function make_qtype_updater() { 266 global $CFG; 267 268 if ($this->question->qtype == 'deleted') { 269 return new question_deleted_question_attempt_updater( 270 $this, $this->question, $this->logger, $this->qeupdater); 271 } 272 273 $path = $CFG->dirroot . '/question/type/' . $this->question->qtype . '/db/upgradelib.php'; 274 if (!is_readable($path)) { 275 throw new coding_exception("Question type {$this->question->qtype} 276 is missing important code (the file {$path}) 277 required to run the upgrade to the new question engine."); 278 } 279 include_once($path); 280 $class = 'qtype_' . $this->question->qtype . '_qe2_attempt_updater'; 281 if (!class_exists($class)) { 282 throw new coding_exception("Question type {$this->question->qtype} 283 is missing important code (the class {$class}) 284 required to run the upgrade to the new question engine."); 285 } 286 return new $class($this, $this->question, $this->logger, $this->qeupdater); 287 } 288 289 public function to_text($html) { 290 return trim(html_to_text($html, 0, false)); 291 } 292 293 protected function graded_state_for_fraction($fraction) { 294 if ($fraction < 0.000001) { 295 return 'gradedwrong'; 296 } else if ($fraction > 0.999999) { 297 return 'gradedright'; 298 } else { 299 return 'gradedpartial'; 300 } 301 } 302 303 protected function manual_graded_state_for_fraction($fraction) { 304 if ($fraction < 0.000001) { 305 return 'mangrwrong'; 306 } else if ($fraction > 0.999999) { 307 return 'mangrright'; 308 } else { 309 return 'mangrpartial'; 310 } 311 } 312 313 protected function make_step($state){ 314 $step = new stdClass(); 315 $step->data = array(); 316 317 if ($state->event == 0 || $this->sequencenumber == 0) { 318 $this->qtypeupdater->set_first_step_data_elements($state, $step->data); 319 } else { 320 $this->qtypeupdater->set_data_elements_for_step($state, $step->data); 321 } 322 323 $step->fraction = null; 324 $step->timecreated = $state->timestamp ? $state->timestamp : time(); 325 $step->userid = $this->attempt->userid; 326 327 $summary = $this->qtypeupdater->response_summary($state); 328 if (!is_null($summary)) { 329 $this->qa->responsesummary = $summary; 330 } 331 $this->qa->timemodified = max($this->qa->timemodified, $state->timestamp); 332 333 return $step; 334 } 335 } 336 337 338 class qbehaviour_deferredfeedback_converter extends question_behaviour_attempt_updater { 339 protected function behaviour_name() { 340 return 'deferredfeedback'; 341 } 342 343 protected function process6($step, $state) { 344 if (!$this->startstate) { 345 $this->logger->log_assumption("Ignoring bogus submit before open in attempt at question {$state->question}"); 346 // WTF, but this has happened a few times in our DB. It seems it is safe to ignore. 347 return; 348 } 349 350 if ($this->finishstate) { 351 if ($this->finishstate->answer != $state->answer || 352 $this->finishstate->grade != $state->grade || 353 $this->finishstate->raw_grade != $state->raw_grade || 354 $this->finishstate->penalty != $state->penalty) { 355 $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."); 356 $this->discard_last_state(); 357 } else { 358 $this->logger->log_assumption("Ignoring extra finish states in attempt at question {$state->question}"); 359 return; 360 } 361 } 362 363 if ($this->question->maxmark > 0) { 364 $step->fraction = $state->grade / $this->question->maxmark; 365 $step->state = $this->graded_state_for_fraction($step->fraction); 366 } else { 367 $step->state = 'finished'; 368 } 369 $step->data['-finish'] = '1'; 370 $this->finishstate = $state; 371 $this->add_step($step); 372 } 373 374 protected function process7($step, $state) { 375 $this->unexpected_event($state); 376 } 377 } 378 379 380 class qbehaviour_manualgraded_converter extends question_behaviour_attempt_updater { 381 protected function behaviour_name() { 382 return 'manualgraded'; 383 } 384 385 protected function process6($step, $state) { 386 $step->state = 'needsgrading'; 387 if (!$this->finishstate) { 388 $step->data['-finish'] = '1'; 389 $this->finishstate = $state; 390 } 391 $this->add_step($step); 392 } 393 394 protected function process7($step, $state) { 395 return $this->process2($step, $state); 396 } 397 } 398 399 400 class qbehaviour_informationitem_converter extends question_behaviour_attempt_updater { 401 protected function behaviour_name() { 402 return 'informationitem'; 403 } 404 405 protected function process0($step, $state) { 406 if ($this->startstate) { 407 return; 408 } 409 $step->state = 'todo'; 410 $this->startstate = $state; 411 $this->add_step($step); 412 } 413 414 protected function process2($step, $state) { 415 $this->unexpected_event($state); 416 } 417 418 protected function process3($step, $state) { 419 $this->unexpected_event($state); 420 } 421 422 protected function process6($step, $state) { 423 if ($this->finishstate) { 424 return; 425 } 426 427 $step->state = 'finished'; 428 $step->data['-finish'] = '1'; 429 $this->finishstate = $state; 430 $this->add_step($step); 431 } 432 433 protected function process7($step, $state) { 434 return $this->process6($step, $state); 435 } 436 437 protected function process8($step, $state) { 438 return $this->process6($step, $state); 439 } 440 } 441 442 443 class qbehaviour_adaptive_converter extends question_behaviour_attempt_updater { 444 protected $try; 445 protected $laststepwasatry = false; 446 protected $finished = false; 447 protected $bestrawgrade = 0; 448 449 protected function behaviour_name() { 450 return 'adaptive'; 451 } 452 453 protected function finish_up() { 454 parent::finish_up(); 455 if ($this->finishstate || !$this->attempt->timefinish) { 456 return; 457 } 458 459 $state = end($this->qstates); 460 $step = $this->make_step($state); 461 $this->process6($step, $state); 462 } 463 464 protected function process0($step, $state) { 465 $this->try = 1; 466 $this->laststepwasatry = false; 467 parent::process0($step, $state); 468 } 469 470 protected function process2($step, $state) { 471 if ($this->finishstate) { 472 $this->logger->log_assumption("Ignoring bogus save after submit in an " . 473 "adaptive attempt at question {$state->question} " . 474 "(question session {$this->qsession->id})"); 475 return; 476 } 477 478 if ($this->question->maxmark > 0) { 479 $step->fraction = $state->grade / $this->question->maxmark; 480 } 481 482 $this->laststepwasatry = false; 483 parent::process2($step, $state); 484 } 485 486 protected function process3($step, $state) { 487 if ($this->question->maxmark > 0) { 488 $step->fraction = $state->grade / $this->question->maxmark; 489 if ($this->graded_state_for_fraction($step->fraction) == 'gradedright') { 490 $step->state = 'complete'; 491 } else { 492 $step->state = 'todo'; 493 } 494 } else { 495 $step->state = 'complete'; 496 } 497 498 $this->bestrawgrade = max($state->raw_grade, $this->bestrawgrade); 499 500 $step->data['-_try'] = $this->try; 501 $this->try += 1; 502 $this->laststepwasatry = true; 503 if ($this->question->maxmark > 0) { 504 $step->data['-_rawfraction'] = $state->raw_grade / $this->question->maxmark; 505 } else { 506 $step->data['-_rawfraction'] = 0; 507 } 508 $step->data['-submit'] = 1; 509 510 $this->add_step($step); 511 } 512 513 protected function process6($step, $state) { 514 if ($this->finishstate) { 515 if (!$this->qtypeupdater->compare_answers($this->finishstate->answer, $state->answer) || 516 $this->finishstate->grade != $state->grade || 517 $this->finishstate->raw_grade != $state->raw_grade || 518 $this->finishstate->penalty != $state->penalty) { 519 throw new coding_exception("Two inconsistent finish states found for question session {$this->qsession->id}."); 520 } else { 521 $this->logger->log_assumption("Ignoring extra finish states in attempt at question {$state->question}"); 522 return; 523 } 524 } 525 526 $this->bestrawgrade = max($state->raw_grade, $this->bestrawgrade); 527 528 if ($this->question->maxmark > 0) { 529 $step->fraction = $state->grade / $this->question->maxmark; 530 $step->state = $this->graded_state_for_fraction( 531 $this->bestrawgrade / $this->question->maxmark); 532 } else { 533 $step->state = 'finished'; 534 } 535 536 $step->data['-finish'] = 1; 537 if ($this->laststepwasatry) { 538 $this->try -= 1; 539 } 540 $step->data['-_try'] = $this->try; 541 if ($this->question->maxmark > 0) { 542 $step->data['-_rawfraction'] = $state->raw_grade / $this->question->maxmark; 543 } else { 544 $step->data['-_rawfraction'] = 0; 545 } 546 547 $this->finishstate = $state; 548 $this->add_step($step); 549 } 550 551 protected function process7($step, $state) { 552 $this->unexpected_event($state); 553 } 554 } 555 556 557 class qbehaviour_adaptivenopenalty_converter extends qbehaviour_adaptive_converter { 558 protected function behaviour_name() { 559 return 'adaptivenopenalty'; 560 } 561 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body