MDL-49294 logging: Improve cleanup tests
[moodle.git] / backup / moodle2 / restore_qtype_plugin.class.php
blob60f599a38b9bc205968f570dff580000ed7ef615
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 /**
19 * Defines restore_qtype_plugin class
21 * @package core_backup
22 * @subpackage moodle2
23 * @category backup
24 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 defined('MOODLE_INTERNAL') || die();
30 /**
31 * Class extending standard restore_plugin in order to implement some
32 * helper methods related with the questions (qtype plugin)
34 * TODO: Finish phpdocs
36 abstract class restore_qtype_plugin extends restore_plugin {
39 * A simple answer to id cache for a single questions answers.
40 * @var array
42 private $questionanswercache = array();
45 * The id of the current question in the questionanswercache.
46 * @var int
48 private $questionanswercacheid = null;
50 /**
51 * Add to $paths the restore_path_elements needed
52 * to handle question_answers for a given question
53 * Used by various qtypes (calculated, essay, multianswer,
54 * multichoice, numerical, shortanswer, truefalse)
56 protected function add_question_question_answers(&$paths) {
57 // Check $paths is one array
58 if (!is_array($paths)) {
59 throw new restore_step_exception('paths_must_be_array', $paths);
62 $elename = 'question_answer';
63 $elepath = $this->get_pathfor('/answers/answer'); // we used get_recommended_name() so this works
64 $paths[] = new restore_path_element($elename, $elepath);
67 /**
68 * Add to $paths the restore_path_elements needed
69 * to handle question_numerical_units for a given question
70 * Used by various qtypes (calculated, numerical)
72 protected function add_question_numerical_units(&$paths) {
73 // Check $paths is one array
74 if (!is_array($paths)) {
75 throw new restore_step_exception('paths_must_be_array', $paths);
78 $elename = 'question_numerical_unit';
79 $elepath = $this->get_pathfor('/numerical_units/numerical_unit'); // we used get_recommended_name() so this works
80 $paths[] = new restore_path_element($elename, $elepath);
83 /**
84 * Add to $paths the restore_path_elements needed
85 * to handle question_numerical_options for a given question
86 * Used by various qtypes (calculated, numerical)
88 protected function add_question_numerical_options(&$paths) {
89 // Check $paths is one array
90 if (!is_array($paths)) {
91 throw new restore_step_exception('paths_must_be_array', $paths);
94 $elename = 'question_numerical_option';
95 $elepath = $this->get_pathfor('/numerical_options/numerical_option'); // we used get_recommended_name() so this works
96 $paths[] = new restore_path_element($elename, $elepath);
99 /**
100 * Add to $paths the restore_path_elements needed
101 * to handle question_datasets (defs and items) for a given question
102 * Used by various qtypes (calculated, numerical)
104 protected function add_question_datasets(&$paths) {
105 // Check $paths is one array
106 if (!is_array($paths)) {
107 throw new restore_step_exception('paths_must_be_array', $paths);
110 $elename = 'question_dataset_definition';
111 $elepath = $this->get_pathfor('/dataset_definitions/dataset_definition'); // we used get_recommended_name() so this works
112 $paths[] = new restore_path_element($elename, $elepath);
114 $elename = 'question_dataset_item';
115 $elepath = $this->get_pathfor('/dataset_definitions/dataset_definition/dataset_items/dataset_item');
116 $paths[] = new restore_path_element($elename, $elepath);
120 * Processes the answer element (question answers). Common for various qtypes.
121 * It handles both creation (if the question is being created) and mapping
122 * (if the question already existed and is being reused)
124 public function process_question_answer($data) {
125 global $DB;
127 $data = (object)$data;
128 $oldid = $data->id;
130 // Detect if the question is created or mapped
131 $oldquestionid = $this->get_old_parentid('question');
132 $newquestionid = $this->get_new_parentid('question');
133 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
135 // In the past, there were some sloppily rounded fractions around. Fix them up.
136 $changes = array(
137 '-0.66666' => '-0.6666667',
138 '-0.33333' => '-0.3333333',
139 '-0.16666' => '-0.1666667',
140 '-0.142857' => '-0.1428571',
141 '0.11111' => '0.1111111',
142 '0.142857' => '0.1428571',
143 '0.16666' => '0.1666667',
144 '0.33333' => '0.3333333',
145 '0.333333' => '0.3333333',
146 '0.66666' => '0.6666667',
148 if (array_key_exists($data->fraction, $changes)) {
149 $data->fraction = $changes[$data->fraction];
152 // If the question has been created by restore, we need to create its question_answers too
153 if ($questioncreated) {
154 // Adjust some columns
155 $data->question = $newquestionid;
156 $data->answer = $data->answertext;
157 // Insert record
158 $newitemid = $DB->insert_record('question_answers', $data);
160 // The question existed, we need to map the existing question_answers
161 } else {
162 // Have we cached the current question?
163 if ($this->questionanswercacheid !== $newquestionid) {
164 // The question changed, purge and start again!
165 $this->questionanswercache = array();
166 $params = array('question' => $newquestionid);
167 $answers = $DB->get_records('question_answers', $params, '', 'id, answer');
168 $this->questionanswercacheid = $newquestionid;
169 // Cache all cleaned answers for a simple text match.
170 foreach ($answers as $answer) {
171 // MDL-30018: Clean in the same way as {@link xml_writer::xml_safe_utf8()}.
172 $clean = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $answer->answer); // Clean CTRL chars.
173 $clean = preg_replace("/\r\n|\r/", "\n", $clean); // Normalize line ending.
174 $this->questionanswercache[$clean] = $answer->id;
178 if (!isset($this->questionanswercache[$data->answertext])) {
179 // If we haven't found the matching answer, something has gone really wrong, the question in the DB
180 // is missing answers, throw an exception.
181 $info = new stdClass();
182 $info->filequestionid = $oldquestionid;
183 $info->dbquestionid = $newquestionid;
184 $info->answer = $data->answertext;
185 throw new restore_step_exception('error_question_answers_missing_in_db', $info);
187 $newitemid = $this->questionanswercache[$data->answertext];
189 // Create mapping (we'll use this intensively when restoring question_states. And also answerfeedback files)
190 $this->set_mapping('question_answer', $oldid, $newitemid);
194 * Processes the numerical_unit element (question numerical units). Common for various qtypes.
195 * It handles both creation (if the question is being created) and mapping
196 * (if the question already existed and is being reused)
198 public function process_question_numerical_unit($data) {
199 global $DB;
201 $data = (object)$data;
202 $oldid = $data->id;
204 // Detect if the question is created or mapped
205 $oldquestionid = $this->get_old_parentid('question');
206 $newquestionid = $this->get_new_parentid('question');
207 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
209 // If the question has been created by restore, we need to create its question_numerical_units too
210 if ($questioncreated) {
211 // Adjust some columns
212 $data->question = $newquestionid;
213 // Insert record
214 $newitemid = $DB->insert_record('question_numerical_units', $data);
219 * Processes the numerical_option element (question numerical options). Common for various qtypes.
220 * It handles both creation (if the question is being created) and mapping
221 * (if the question already existed and is being reused)
223 public function process_question_numerical_option($data) {
224 global $DB;
226 $data = (object)$data;
227 $oldid = $data->id;
229 // Detect if the question is created or mapped
230 $oldquestionid = $this->get_old_parentid('question');
231 $newquestionid = $this->get_new_parentid('question');
232 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
234 // If the question has been created by restore, we need to create its question_numerical_options too
235 if ($questioncreated) {
236 // Adjust some columns
237 $data->question = $newquestionid;
238 // Insert record
239 $newitemid = $DB->insert_record('question_numerical_options', $data);
240 // Create mapping (not needed, no files nor childs nor states here)
241 //$this->set_mapping('question_numerical_option', $oldid, $newitemid);
246 * Processes the dataset_definition element (question dataset definitions). Common for various qtypes.
247 * It handles both creation (if the question is being created) and mapping
248 * (if the question already existed and is being reused)
250 public function process_question_dataset_definition($data) {
251 global $DB;
253 $data = (object)$data;
254 $oldid = $data->id;
256 // Detect if the question is created or mapped
257 $oldquestionid = $this->get_old_parentid('question');
258 $newquestionid = $this->get_new_parentid('question');
259 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
261 // If the question is mapped, nothing to do
262 if (!$questioncreated) {
263 return;
266 // Arrived here, let's see if the question_dataset_definition already exists in category or no
267 // (by category, name, type and enough items). Only for "shared" definitions (category != 0).
268 // If exists, reuse it, else, create it as "not shared" (category = 0)
269 $data->category = $this->get_mappingid('question_category', $data->category);
270 // If category is shared, look for definitions
271 $founddefid = null;
272 if ($data->category) {
273 $candidatedefs = $DB->get_records_sql("SELECT id, itemcount
274 FROM {question_dataset_definitions}
275 WHERE category = ?
276 AND name = ?
277 AND type = ?", array($data->category, $data->name, $data->type));
278 foreach ($candidatedefs as $candidatedef) {
279 if ($candidatedef->itemcount >= $data->itemcount) { // Check it has enough items
280 $founddefid = $candidatedef->id;
281 break; // end loop, shared definition match found
284 // If there were candidates but none fulfilled the itemcount condition, create definition as not shared
285 if ($candidatedefs && !$founddefid) {
286 $data->category = 0;
289 // If haven't found any shared definition match, let's create it
290 if (!$founddefid) {
291 $newitemid = $DB->insert_record('question_dataset_definitions', $data);
292 // Set mapping, so dataset items will know if they must be created
293 $this->set_mapping('question_dataset_definition', $oldid, $newitemid);
295 // If we have found one shared definition match, use it
296 } else {
297 $newitemid = $founddefid;
298 // Set mapping to 0, so dataset items will know they don't need to be created
299 $this->set_mapping('question_dataset_definition', $oldid, 0);
302 // Arrived here, we have one $newitemid (create or reused). Create the question_datasets record
303 $questiondataset = new stdClass();
304 $questiondataset->question = $newquestionid;
305 $questiondataset->datasetdefinition = $newitemid;
306 $DB->insert_record('question_datasets', $questiondataset);
310 * Processes the dataset_item element (question dataset items). Common for various qtypes.
311 * It handles both creation (if the question is being created) and mapping
312 * (if the question already existed and is being reused)
314 public function process_question_dataset_item($data) {
315 global $DB;
317 $data = (object)$data;
318 $oldid = $data->id;
320 // Detect if the question is created or mapped
321 $oldquestionid = $this->get_old_parentid('question');
322 $newquestionid = $this->get_new_parentid('question');
323 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
325 // If the question is mapped, nothing to do
326 if (!$questioncreated) {
327 return;
330 // Detect if the question_dataset_definition is being created
331 $newdefinitionid = $this->get_new_parentid('question_dataset_definition');
333 // If the definition is reused, nothing to do
334 if (!$newdefinitionid) {
335 return;
338 // let's create the question_dataset_items
339 $data->definition = $newdefinitionid;
340 $data->itemnumber = $data->number;
341 $DB->insert_record('question_dataset_items', $data);
345 * Do any re-coding necessary in the student response.
346 * @param int $questionid the new id of the question
347 * @param int $sequencenumber of the step within the qusetion attempt.
348 * @param array the response data from the backup.
349 * @return array the recoded response.
351 public function recode_response($questionid, $sequencenumber, array $response) {
352 return $response;
356 * Decode legacy question_states.answer for this qtype. Used when restoring
357 * 2.0 attempt data.
359 public function recode_legacy_state_answer($state) {
360 // By default, return answer unmodified, qtypes needing recode will override this
361 return $state->answer;
365 * Return the contents of the questions stuff that must be processed by the links decoder
367 * Only common stuff to all plugins, in this case:
368 * - question: text and feedback
369 * - question_answers: text and feedbak
371 * Note each qtype will have, if needed, its own define_decode_contents method
373 static public function define_plugin_decode_contents() {
375 $contents = array();
377 $contents[] = new restore_decode_content('question', array('questiontext', 'generalfeedback'), 'question_created');
378 $contents[] = new restore_decode_content('question_answers', array('answer', 'feedback'), 'question_answer');
380 return $contents;