MDL-31495 Performance improvement in question engine upgrade SQL
[moodle.git] / question / engine / upgrade / upgradelib.php
CommitLineData
0859ff65
TH
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 the code required to upgrade all the attempt data from
19 * old versions of Moodle into the tables used by the new question engine.
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
28defined('MOODLE_INTERNAL') || die();
29
30global $CFG;
39759ac4 31require_once($CFG->dirroot . '/question/engine/bank.php');
0859ff65
TH
32require_once($CFG->dirroot . '/question/engine/upgrade/logger.php');
33require_once($CFG->dirroot . '/question/engine/upgrade/behaviourconverters.php');
34
35
36/**
37 * This class manages upgrading all the question attempts from the old database
38 * structure to the new question engine.
39 *
40 * @copyright 2010 The Open University
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42 */
43class question_engine_attempt_upgrader {
44 /** @var question_engine_upgrade_question_loader */
45 protected $questionloader;
cd300cf3 46 /** @var question_engine_assumption_logger */
0859ff65 47 protected $logger;
cd300cf3
TH
48 /** @var int used by {@link prevent_timeout()}. */
49 protected $dotcounter = 0;
bb28e3bc
TH
50 /** @var progress_bar */
51 protected $progressbar = null;
18ab06ba
TH
52 /** @var boolean */
53 protected $doingbackup = false;
cd300cf3
TH
54
55 /**
56 * Called before starting to upgrade all the attempts at a particular quiz.
57 * @param int $done the number of quizzes processed so far.
58 * @param int $outof the total number of quizzes to process.
59 * @param int $quizid the id of the quiz that is about to be processed.
60 */
0859ff65 61 protected function print_progress($done, $outof, $quizid) {
bb28e3bc
TH
62 if (is_null($this->progressbar)) {
63 $this->progressbar = new progress_bar('qe2upgrade');
08c458d1 64 $this->progressbar->create();
bb28e3bc
TH
65 }
66
cd300cf3 67 gc_collect_cycles(); // This was really helpful in PHP 5.2. Perhaps remove.
bb28e3bc
TH
68 $a = new stdClass();
69 $a->done = $done;
69384c19 70 $a->outof = $outof;
bb28e3bc
TH
71 $a->info = $quizid;
72 $this->progressbar->update($done, $outof, get_string('upgradingquizattempts', 'quiz', $a));
0859ff65
TH
73 }
74
75 protected function prevent_timeout() {
76 set_time_limit(300);
18ab06ba
TH
77 if ($this->doingbackup) {
78 return;
79 }
cd300cf3
TH
80 echo '.';
81 $this->dotcounter += 1;
82 if ($this->dotcounter % 100 == 0) {
83 echo '<br />';
84 }
0859ff65
TH
85 }
86
87 protected function get_quiz_ids() {
10273012
TH
88 global $CFG, $DB;
89
90 // Look to see if the admin has set things up to only upgrade certain attempts.
6401b3c5
TH
91 $partialupgradefile = $CFG->dirroot . '/' . $CFG->admin .
92 '/tool/qeupgradehelper/partialupgrade.php';
93 $partialupgradefunction = 'tool_qeupgradehelper_get_quizzes_to_upgrade';
10273012
TH
94 if (is_readable($partialupgradefile)) {
95 include_once($partialupgradefile);
96 if (function_exists($partialupgradefunction)) {
97 $quizids = $partialupgradefunction();
98
99 // Ignore any quiz ids that do not acually exist.
100 if (empty($quizids)) {
101 return array();
102 }
103 list($test, $params) = $DB->get_in_or_equal($quizids);
104 return $DB->get_fieldset_sql("
105 SELECT id
106 FROM {quiz}
107 WHERE id $test
108 ORDER BY id", $params);
109 }
110 }
111
112 // Otherwise, upgrade all attempts.
113 return $DB->get_fieldset_sql('SELECT id FROM {quiz} ORDER BY id');
0859ff65
TH
114 }
115
116 public function convert_all_quiz_attempts() {
cd300cf3
TH
117 global $DB;
118
0859ff65 119 $quizids = $this->get_quiz_ids();
cd300cf3 120 if (empty($quizids)) {
0859ff65
TH
121 return true;
122 }
123
124 $done = 0;
125 $outof = count($quizids);
126 $this->logger = new question_engine_assumption_logger();
127
10273012 128 foreach ($quizids as $quizid) {
0859ff65
TH
129 $this->print_progress($done, $outof, $quizid);
130
cd300cf3
TH
131 $quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
132 $this->update_all_attempts_at_quiz($quiz);
0859ff65
TH
133
134 $done += 1;
135 }
136
137 $this->print_progress($outof, $outof, 'All done!');
138 $this->logger = null;
0859ff65
TH
139 }
140
cd300cf3
TH
141 public function get_attempts_extra_where() {
142 return ' AND needsupgradetonewqe = 1';
0859ff65
TH
143 }
144
cd300cf3
TH
145 public function update_all_attempts_at_quiz($quiz) {
146 global $DB;
0859ff65
TH
147
148 // Wipe question loader cache.
149 $this->questionloader = new question_engine_upgrade_question_loader($this->logger);
150
cd300cf3 151 $transaction = $DB->start_delegated_transaction();
0859ff65 152
cd300cf3
TH
153 $params = array('quizid' => $quiz->id);
154 $where = 'quiz = :quizid AND preview = 0' . $this->get_attempts_extra_where();
0859ff65 155
cd300cf3
TH
156 $quizattemptsrs = $DB->get_recordset_select('quiz_attempts', $where, $params, 'uniqueid');
157 $questionsessionsrs = $DB->get_recordset_sql("
2af7d0d8
MA
158 SELECT s.*
159 FROM {question_sessions} s
160 JOIN {quiz_attempts} a ON (attemptid = uniqueid)
161 WHERE $where
162 ORDER BY attemptid, questionid
cd300cf3 163 ", $params);
0859ff65 164
cd300cf3 165 $questionsstatesrs = $DB->get_recordset_sql("
2af7d0d8
MA
166 SELECT s.*
167 FROM {question_states} s
168 JOIN {quiz_attempts} ON (s.attempt = uniqueid)
169 WHERE $where
170 ORDER BY s.attempt, question, seq_number, s.id
cd300cf3 171 ", $params);
0859ff65 172
cd300cf3
TH
173 $datatodo = $quizattemptsrs && $questionsessionsrs && $questionsstatesrs;
174 while ($datatodo && $quizattemptsrs->valid()) {
175 $attempt = $quizattemptsrs->current();
176 $quizattemptsrs->next();
177 $this->convert_quiz_attempt($quiz, $attempt, $questionsessionsrs, $questionsstatesrs);
0859ff65
TH
178 }
179
cd300cf3
TH
180 $quizattemptsrs->close();
181 $questionsessionsrs->close();
182 $questionsstatesrs->close();
0859ff65 183
cd300cf3 184 $transaction->allow_commit();
0859ff65
TH
185 }
186
cd300cf3
TH
187 protected function convert_quiz_attempt($quiz, $attempt, moodle_recordset $questionsessionsrs,
188 moodle_recordset $questionsstatesrs) {
0859ff65
TH
189 $qas = array();
190 $this->logger->set_current_attempt_id($attempt->id);
191 while ($qsession = $this->get_next_question_session($attempt, $questionsessionsrs)) {
192 $question = $this->load_question($qsession->questionid, $quiz->id);
193 $qstates = $this->get_question_states($attempt, $question, $questionsstatesrs);
194 try {
9c197f44
TH
195 $qas[$qsession->questionid] = $this->convert_question_attempt(
196 $quiz, $attempt, $question, $qsession, $qstates);
0859ff65
TH
197 } catch (Exception $e) {
198 notify($e->getMessage());
199 }
200 }
201 $this->logger->set_current_attempt_id(null);
202
0859ff65
TH
203 $questionorder = array();
204 foreach (explode(',', $quiz->questions) as $questionid) {
205 if ($questionid == 0) {
206 continue;
207 }
208 if (!array_key_exists($questionid, $qas)) {
209 $this->logger->log_assumption("Supplying minimal open state for
210 question {$questionid} in attempt {$attempt->id} at quiz
211 {$attempt->quiz}, since the session was missing.", $attempt->id);
212 try {
1645ad22 213 $question = $this->load_question($questionid, $quiz->id);
0859ff65
TH
214 $qas[$questionid] = $this->supply_missing_question_attempt(
215 $quiz, $attempt, $question);
216 } catch (Exception $e) {
217 notify($e->getMessage());
218 }
219 }
220 }
221
222 return $this->save_usage($quiz->preferredbehaviour, $attempt, $qas, $quiz->questions);
223 }
224
18ab06ba 225 public function save_usage($preferredbehaviour, $attempt, $qas, $quizlayout) {
0859ff65 226 $missing = array();
0859ff65
TH
227
228 $layout = explode(',', $attempt->layout);
229 $questionkeys = array_combine(array_values($layout), array_keys($layout));
230
cd300cf3 231 $this->set_quba_preferred_behaviour($attempt->uniqueid, $preferredbehaviour);
0859ff65
TH
232
233 $i = 0;
234 foreach (explode(',', $quizlayout) as $questionid) {
235 if ($questionid == 0) {
236 continue;
237 }
238 $i++;
239
240 if (!array_key_exists($questionid, $qas)) {
241 $missing[] = $questionid;
ffe4d23a 242 $layout[$questionkeys[$questionid]] = $questionid;
0859ff65
TH
243 continue;
244 }
245
246 $qa = $qas[$questionid];
247 $qa->questionusageid = $attempt->uniqueid;
248 $qa->slot = $i;
c83ed025
TH
249 if (textlib::strlen($qa->questionsummary) > question_bank::MAX_SUMMARY_LENGTH) {
250 // It seems some people write very long quesions! MDL-30760
251 $qa->questionsummary = textlib::substr($qa->questionsummary,
252 0, question_bank::MAX_SUMMARY_LENGTH - 3) . '...';
253 }
cd300cf3 254 $this->insert_record('question_attempts', $qa);
0859ff65
TH
255 $layout[$questionkeys[$questionid]] = $qa->slot;
256
257 foreach ($qa->steps as $step) {
258 $step->questionattemptid = $qa->id;
cd300cf3 259 $this->insert_record('question_attempt_steps', $step);
0859ff65
TH
260
261 foreach ($step->data as $name => $value) {
262 $datum = new stdClass();
263 $datum->attemptstepid = $step->id;
264 $datum->name = $name;
265 $datum->value = $value;
cd300cf3 266 $this->insert_record('question_attempt_step_data', $datum, false);
0859ff65
TH
267 }
268 }
269 }
270
cd300cf3 271 $this->set_quiz_attempt_layout($attempt->uniqueid, implode(',', $layout));
0859ff65
TH
272
273 if ($missing) {
274 notify("Question sessions for questions " .
275 implode(', ', $missing) .
276 " were missing when upgrading question usage {$attempt->uniqueid}.");
277 }
0859ff65
TH
278 }
279
280 protected function set_quba_preferred_behaviour($qubaid, $preferredbehaviour) {
cd300cf3
TH
281 global $DB;
282 $DB->set_field('question_usages', 'preferredbehaviour', $preferredbehaviour,
283 array('id' => $qubaid));
0859ff65
TH
284 }
285
286 protected function set_quiz_attempt_layout($qubaid, $layout) {
cd300cf3
TH
287 global $DB;
288 $DB->set_field('quiz_attempts', 'layout', $layout, array('uniqueid' => $qubaid));
289 $DB->set_field('quiz_attempts', 'needsupgradetonewqe', 0, array('uniqueid' => $qubaid));
0859ff65
TH
290 }
291
292 protected function delete_quiz_attempt($qubaid) {
cd300cf3
TH
293 global $DB;
294 $DB->delete_records('quiz_attempts', array('uniqueid' => $qubaid));
295 $DB->delete_records('question_attempts', array('id' => $qubaid));
0859ff65
TH
296 }
297
298 protected function insert_record($table, $record, $saveid = true) {
cd300cf3
TH
299 global $DB;
300 $newid = $DB->insert_record($table, $record, $saveid);
0859ff65
TH
301 if ($saveid) {
302 $record->id = $newid;
303 }
304 return $newid;
305 }
306
307 public function load_question($questionid, $quizid = null) {
308 return $this->questionloader->get_question($questionid, $quizid);
309 }
310
667cdde3
TH
311 public function load_dataset($questionid, $selecteditem) {
312 return $this->questionloader->load_dataset($questionid, $selecteditem);
313 }
314
cd300cf3 315 public function get_next_question_session($attempt, moodle_recordset $questionsessionsrs) {
39759ac4
TH
316 if (!$questionsessionsrs->valid()) {
317 return false;
318 }
319
cd300cf3 320 $qsession = $questionsessionsrs->current();
39759ac4 321 if ($qsession->attemptid != $attempt->uniqueid) {
0859ff65
TH
322 // No more question sessions belonging to this attempt.
323 return false;
324 }
325
326 // Session found, move the pointer in the RS and return the record.
cd300cf3 327 $questionsessionsrs->next();
0859ff65
TH
328 return $qsession;
329 }
330
cd300cf3 331 public function get_question_states($attempt, $question, moodle_recordset $questionsstatesrs) {
0859ff65
TH
332 $qstates = array();
333
39759ac4
TH
334 while ($questionsstatesrs->valid()) {
335 $state = $questionsstatesrs->current();
336 if ($state->attempt != $attempt->uniqueid ||
0859ff65
TH
337 $state->question != $question->id) {
338 // We have found all the states for this attempt. Stop.
339 break;
340 }
341
342 // Add the new state to the array, and advance.
ceb4d2ed 343 $qstates[] = $state;
cd300cf3 344 $questionsstatesrs->next();
0859ff65
TH
345 }
346
347 return $qstates;
348 }
349
350 protected function get_converter_class_name($question, $quiz, $qsessionid) {
987e0e91
HB
351 global $DB;
352 if ($question->qtype == 'deleted') {
353 $where = '(question = :questionid OR '.$DB->sql_like('answer', ':randomid').') AND event = 7';
354 $params = array('questionid'=>$question->id, 'randomid'=>"random{$question->id}-%");
355 if ($DB->record_exists_select('question_states', $where, $params)) {
356 $this->logger->log_assumption("Assuming that deleted question {$question->id} was manually graded.");
357 return 'qbehaviour_manualgraded_converter';
358 }
359 }
cd300cf3 360 if ($question->qtype == 'essay') {
0859ff65
TH
361 return 'qbehaviour_manualgraded_converter';
362 } else if ($question->qtype == 'description') {
363 return 'qbehaviour_informationitem_converter';
0859ff65
TH
364 } else if ($quiz->preferredbehaviour == 'deferredfeedback') {
365 return 'qbehaviour_deferredfeedback_converter';
cd300cf3
TH
366 } else if ($quiz->preferredbehaviour == 'adaptive') {
367 return 'qbehaviour_adaptive_converter';
368 } else if ($quiz->preferredbehaviour == 'adaptivenopenalty') {
369 return 'qbehaviour_adaptivenopenalty_converter';
0859ff65 370 } else {
cd300cf3
TH
371 throw new coding_exception("Question session {$qsessionid}
372 has an unexpected preferred behaviour {$quiz->preferredbehaviour}.");
0859ff65
TH
373 }
374 }
375
376 public function supply_missing_question_attempt($quiz, $attempt, $question) {
377 if ($question->qtype == 'random') {
cd300cf3
TH
378 throw new coding_exception("Cannot supply a missing qsession for question
379 {$question->id} in attempt {$attempt->id}.");
0859ff65
TH
380 }
381
382 $converterclass = $this->get_converter_class_name($question, $quiz, 'missing');
383
cd300cf3 384 $qbehaviourupdater = new $converterclass($quiz, $attempt, $question,
667cdde3 385 null, null, $this->logger, $this);
0859ff65
TH
386 $qa = $qbehaviourupdater->supply_missing_qa();
387 $qbehaviourupdater->discard();
388 return $qa;
389 }
390
391 public function convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates) {
392 $this->prevent_timeout();
393
394 if ($question->qtype == 'random') {
395 list($question, $qstates) = $this->decode_random_attempt($qstates, $question->maxmark);
396 $qsession->questionid = $question->id;
397 }
398
399 $converterclass = $this->get_converter_class_name($question, $quiz, $qsession->id);
400
cd300cf3 401 $qbehaviourupdater = new $converterclass($quiz, $attempt, $question, $qsession,
667cdde3 402 $qstates, $this->logger, $this);
0859ff65
TH
403 $qa = $qbehaviourupdater->get_converted_qa();
404 $qbehaviourupdater->discard();
405 return $qa;
406 }
407
408 protected function decode_random_attempt($qstates, $maxmark) {
409 $realquestionid = null;
410 foreach ($qstates as $i => $state) {
411 if (strpos($state->answer, '-') < 6) {
412 // Broken state, skip it.
413 $this->logger->log_assumption("Had to skip brokes state {$state->id}
414 for question {$state->question}.");
415 unset($qstates[$i]);
416 continue;
417 }
418 list($randombit, $realanswer) = explode('-', $state->answer, 2);
419 $newquestionid = substr($randombit, 6);
420 if ($realquestionid && $realquestionid != $newquestionid) {
cd300cf3
TH
421 throw new coding_exception("Question session {$this->qsession->id}
422 for random question points to two different real questions
423 {$realquestionid} and {$newquestionid}.");
0859ff65
TH
424 }
425 $qstates[$i]->answer = $realanswer;
426 }
427
428 if (empty($newquestionid)) {
429 // This attempt only had broken states. Set a fake $newquestionid to
430 // prevent a null DB error later.
431 $newquestionid = 0;
432 }
433
434 $newquestion = $this->load_question($newquestionid);
435 $newquestion->maxmark = $maxmark;
436 return array($newquestion, $qstates);
437 }
18ab06ba
TH
438
439 public function prepare_to_restore() {
440 $this->doingbackup = true; // Prevent printing of dots to stop timeout on upgrade.
441 $this->logger = new dummy_question_engine_assumption_logger();
442 $this->questionloader = new question_engine_upgrade_question_loader($this->logger);
443 }
0859ff65
TH
444}
445
446
447/**
448 * This class deals with loading (and caching) question definitions during the
449 * question engine upgrade.
450 *
451 * @copyright 2010 The Open University
452 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
453 */
454class question_engine_upgrade_question_loader {
455 private $cache = array();
667cdde3 456 private $datasetcache = array();
0859ff65
TH
457
458 public function __construct($logger) {
459 $this->logger = $logger;
460 }
461
462 protected function load_question($questionid, $quizid) {
cd300cf3 463 global $DB;
0859ff65
TH
464
465 if ($quizid) {
cd300cf3 466 $question = $DB->get_record_sql("
0859ff65 467 SELECT q.*, qqi.grade AS maxmark
cd300cf3
TH
468 FROM {question} q
469 JOIN {quiz_question_instances} qqi ON qqi.question = q.id
0859ff65
TH
470 WHERE q.id = $questionid AND qqi.quiz = $quizid");
471 } else {
cd300cf3 472 $question = $DB->get_record('question', array('id' => $questionid));
0859ff65
TH
473 }
474
475 if (!$question) {
476 return null;
477 }
478
479 if (empty($question->defaultmark)) {
480 if (!empty($question->defaultgrade)) {
481 $question->defaultmark = $question->defaultgrade;
482 } else {
483 $question->defaultmark = 0;
484 }
485 unset($question->defaultgrade);
486 }
487
488 $qtype = question_bank::get_qtype($question->qtype, false);
489 if ($qtype->name() === 'missingtype') {
490 $this->logger->log_assumption("Dealing with question id {$question->id}
491 that is of an unknown type {$question->qtype}.");
cd300cf3
TH
492 $question->questiontext = '<p>' . get_string('warningmissingtype', 'quiz') .
493 '</p>' . $question->questiontext;
0859ff65
TH
494 }
495
496 $qtype->get_question_options($question);
497
498 return $question;
499 }
500
501 public function get_question($questionid, $quizid) {
502 if (isset($this->cache[$questionid])) {
503 return $this->cache[$questionid];
504 }
505
506 $question = $this->load_question($questionid, $quizid);
507
508 if (!$question) {
509 $this->logger->log_assumption("Dealing with question id {$questionid}
510 that was missing from the database.");
511 $question = new stdClass();
512 $question->id = $questionid;
513 $question->qtype = 'deleted';
514 $question->maxmark = 1; // Guess, but that is all we can do.
515 $question->questiontext = get_string('deletedquestiontext', 'qtype_missingtype');
516 }
517
518 $this->cache[$questionid] = $question;
519 return $this->cache[$questionid];
520 }
667cdde3
TH
521
522 public function load_dataset($questionid, $selecteditem) {
523 global $DB;
524
525 if (isset($this->datasetcache[$questionid][$selecteditem])) {
526 return $this->datasetcache[$questionid][$selecteditem];
527 }
528
529 $this->datasetcache[$questionid][$selecteditem] = $DB->get_records_sql_menu('
530 SELECT qdd.name, qdi.value
531 FROM {question_dataset_items} qdi
532 JOIN {question_dataset_definitions} qdd ON qdd.id = qdi.definition
533 JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition
534 WHERE qd.question = ?
535 AND qdi.itemnumber = ?
536 ', array($questionid, $selecteditem));
537 return $this->datasetcache[$questionid][$selecteditem];
538 }
0859ff65
TH
539}
540
541
cd300cf3
TH
542/**
543 * Base class for the classes that convert the question-type specific bits of
544 * the attempt data.
545 *
546 * @copyright 2010 The Open University
547 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
548 */
0859ff65 549abstract class question_qtype_attempt_updater {
cd300cf3 550 /** @var object the question definition data. */
0859ff65 551 protected $question;
cd300cf3 552 /** @var question_behaviour_attempt_updater */
0859ff65
TH
553 protected $updater;
554 /** @var question_engine_assumption_logger */
555 protected $logger;
667cdde3
TH
556 /** @var question_engine_attempt_upgrader */
557 protected $qeupdater;
0859ff65 558
667cdde3 559 public function __construct($updater, $question, $logger, $qeupdater) {
0859ff65
TH
560 $this->updater = $updater;
561 $this->question = $question;
562 $this->logger = $logger;
667cdde3 563 $this->qeupdater = $qeupdater;
0859ff65
TH
564 }
565
566 public function discard() {
567 // Help the garbage collector, which seems to be struggling.
568 $this->updater = null;
569 $this->question = null;
570 $this->logger = null;
667cdde3 571 $this->qeupdater = null;
0859ff65
TH
572 }
573
574 protected function to_text($html) {
575 return $this->updater->to_text($html);
576 }
577
578 public function question_summary() {
579 return $this->to_text($this->question->questiontext);
580 }
581
582 public function compare_answers($answer1, $answer2) {
583 return $answer1 == $answer2;
584 }
585
fbf51a48
HB
586 public function is_blank_answer($state) {
587 return $state->answer == '';
588 }
d12d4d59 589
0859ff65
TH
590 public abstract function right_answer();
591 public abstract function response_summary($state);
592 public abstract function was_answered($state);
593 public abstract function set_first_step_data_elements($state, &$data);
594 public abstract function set_data_elements_for_step($state, &$data);
595 public abstract function supply_missing_first_step_data(&$data);
596}
597
598
599class question_deleted_question_attempt_updater extends question_qtype_attempt_updater {
600 public function right_answer() {
601 return '';
602 }
603
604 public function response_summary($state) {
605 return $state->answer;
606 }
607
608 public function was_answered($state) {
609 return !empty($state->answer);
610 }
611
612 public function set_first_step_data_elements($state, &$data) {
613 $data['upgradedfromdeletedquestion'] = $state->answer;
614 }
615
616 public function supply_missing_first_step_data(&$data) {
617 }
618
619 public function set_data_elements_for_step($state, &$data) {
620 $data['upgradedfromdeletedquestion'] = $state->answer;
621 }
622}