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