MDL-35987 qtype_multianswer: Prevent restore failure due to bad sequence
[moodle.git] / question / type / multianswer / backup / moodle2 / restore_qtype_multianswer_plugin.class.php
CommitLineData
41941110 1<?php
41941110
EL
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 * @package moodlecore
19 * @subpackage backup-moodle2
20 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 */
23
a17b297d 24
41941110
EL
25defined('MOODLE_INTERNAL') || die();
26
bd156853 27require_once($CFG->dirroot . '/question/type/multianswer/questiontype.php');
41941110
EL
28/**
29 * restore plugin class that provides the necessary information
30 * needed to restore one multianswer qtype plugin
f7970e3c
TH
31 *
32 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41941110
EL
34 */
35class restore_qtype_multianswer_plugin extends restore_qtype_plugin {
36
37 /**
38 * Returns the paths to be handled by the plugin at question level
39 */
40 protected function define_question_plugin_structure() {
41941110
EL
41 $paths = array();
42
3d9645ae 43 // This qtype uses question_answers, add them.
41941110
EL
44 $this->add_question_question_answers($paths);
45
3d9645ae 46 // Add own qtype stuff.
41941110 47 $elename = 'multianswer';
59a3fcd3 48 $elepath = $this->get_pathfor('/multianswer');
41941110
EL
49 $paths[] = new restore_path_element($elename, $elepath);
50
3d9645ae 51 return $paths; // And we return the interesting paths.
41941110
EL
52 }
53
54 /**
55 * Process the qtype/multianswer element
56 */
57 public function process_multianswer($data) {
58 global $DB;
59
60 $data = (object)$data;
61 $oldid = $data->id;
62
3d9645ae 63 // Detect if the question is created or mapped.
41941110
EL
64 $oldquestionid = $this->get_old_parentid('question');
65 $newquestionid = $this->get_new_parentid('question');
66 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
67
59a3fcd3 68 // If the question has been created by restore, we need to create its
3d9645ae 69 // question_multianswer too.
41941110 70 if ($questioncreated) {
3d9645ae 71 // Adjust some columns.
41941110
EL
72 $data->question = $newquestionid;
73 // Note: multianswer->sequence is a list of question->id values. We aren't
74 // recoding them here (because some questions can be missing yet). Instead
75 // we'll perform the recode in the {@link after_execute} method of the plugin
3d9645ae 76 // that gets executed once all questions have been created.
77 // Insert record.
41941110 78 $newitemid = $DB->insert_record('question_multianswer', $data);
3d9645ae 79 // Create mapping (need it for after_execute recode of sequence).
41941110 80 $this->set_mapping('question_multianswer', $oldid, $newitemid);
41941110
EL
81 }
82 }
83
84 /**
85 * This method is executed once the whole restore_structure_step
86 * this step is part of ({@link restore_create_categories_and_questions})
87 * has ended processing the whole xml structure. Its name is:
88 * "after_execute_" + connectionpoint ("question")
89 *
90 * For multianswer qtype we use it to restore the sequence column,
91 * containing one list of question ids
92 */
93 public function after_execute_question() {
94 global $DB;
95 // Now that all the questions have been restored, let's process
3d9645ae 96 // the created question_multianswer sequences (list of question ids).
59a3fcd3
TH
97 $rs = $DB->get_recordset_sql("
98 SELECT qma.id, qma.sequence
99 FROM {question_multianswer} qma
100 JOIN {backup_ids_temp} bi ON bi.newitemid = qma.question
101 WHERE bi.backupid = ?
102 AND bi.itemname = 'question_created'",
103 array($this->get_restoreid()));
41941110 104 foreach ($rs as $rec) {
9a601adc
FM
105 $sequencearr = preg_split('/,/', $rec->sequence, -1, PREG_SPLIT_NO_EMPTY);
106 if (substr_count($rec->sequence, ',') + 1 != count($sequencearr)) {
107 $this->task->log('Invalid sequence found in restored multianswer question ' . $rec->id, backup::LOG_WARNING);
108 }
109
41941110
EL
110 foreach ($sequencearr as $key => $question) {
111 $sequencearr[$key] = $this->get_mappingid('question', $question);
112 }
113 $sequence = implode(',', $sequencearr);
59a3fcd3
TH
114 $DB->set_field('question_multianswer', 'sequence', $sequence,
115 array('id' => $rec->id));
bd156853
PP
116 if (!empty($sequence)) {
117 // Get relevant data indexed by positionkey from the multianswers table.
118 $wrappedquestions = $DB->get_records_list('question', 'id',
119 explode(',', $sequence), 'id ASC');
120 foreach ($wrappedquestions as $wrapped) {
121 if ($wrapped->qtype == 'multichoice') {
122 question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped);
123 if (isset($wrapped->options->shuffleanswers)) {
124 preg_match('/'.ANSWER_REGEX.'/s', $wrapped->questiontext, $answerregs);
abc24912
SL
125 if (isset($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE]) &&
126 $answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE] !== '') {
bd156853
PP
127 $wrapped->options->shuffleanswers = 0;
128 $DB->set_field_select('qtype_multichoice_options', 'shuffleanswers', '0', "id =:select",
129 array('select' => $wrapped->options->id) );
130 }
131 }
132 }
133 }
134 }
41941110
EL
135 }
136 $rs->close();
137 }
138
98a3898e
TH
139 public function recode_response($questionid, $sequencenumber, array $response) {
140 global $DB;
141
142 $qtypes = $DB->get_records_menu('question', array('parent' => $questionid),
143 '', 'id, qtype');
144
145 $sequence = $DB->get_field('question_multianswer', 'sequence',
146 array('question' => $questionid));
147
148 $fakestep = new question_attempt_step_read_only($response);
149
150 foreach (explode(',', $sequence) as $key => $subqid) {
151 $i = $key + 1;
152
153 $substep = new question_attempt_step_subquestion_adapter($fakestep, 'sub' . $i . '_');
154 $recodedresponse = $this->step->questions_recode_response_data($qtypes[$subqid],
155 $subqid, $sequencenumber, $substep->get_all_data());
156
157 foreach ($recodedresponse as $name => $value) {
158 $response[$substep->add_prefix($name)] = $value;
159 }
160 }
161
162 return $response;
163 }
164
41941110
EL
165 /**
166 * Given one question_states record, return the answer
167 * recoded pointing to all the restored stuff for multianswer questions
168 *
169 * answer is one comma separated list of hypen separated pairs
170 * containing sequence (pointing to questions sequence in question_multianswer)
171 * and mixed answers. We'll delegate
172 * the recoding of answers to the proper qtype
173 */
18ab06ba 174 public function recode_legacy_state_answer($state) {
41941110
EL
175 global $DB;
176 $answer = $state->answer;
177 $resultarr = array();
3d9645ae 178 // Get sequence of questions.
59a3fcd3
TH
179 $sequence = $DB->get_field('question_multianswer', 'sequence',
180 array('question' => $state->question));
41941110 181 $sequencearr = explode(',', $sequence);
3d9645ae 182 // Let's process each pair.
41941110
EL
183 foreach (explode(',', $answer) as $pair) {
184 $pairarr = explode('-', $pair);
185 $sequenceid = $pairarr[0];
186 $subanswer = $pairarr[1];
3d9645ae 187 // Calculate the questionid based on sequenceid.
188 // Note it is already one *new* questionid that doesn't need mapping.
bd156853 189 $questionid = $sequencearr[$sequenceid - 1];
3d9645ae 190 // Fetch qtype of the question (needed for delegation).
41941110 191 $questionqtype = $DB->get_field('question', 'qtype', array('id' => $questionid));
3d9645ae 192 // Delegate subanswer recode to proper qtype, faking one question_states record.
41941110
EL
193 $substate = new stdClass();
194 $substate->question = $questionid;
195 $substate->answer = $subanswer;
18ab06ba 196 $newanswer = $this->step->restore_recode_legacy_answer($substate, $questionqtype);
41941110
EL
197 $resultarr[] = implode('-', array($sequenceid, $newanswer));
198 }
199 return implode(',', $resultarr);
200 }
201
202}