See Release Notes
Long Term Support Release
1 <?php 2 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. 14 // 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/>. 17 18 /** 19 * Defines restore_qtype_plugin class 20 * 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 26 */ 27 28 defined('MOODLE_INTERNAL') || die(); 29 30 /** 31 * Class extending standard restore_plugin in order to implement some 32 * helper methods related with the questions (qtype plugin) 33 * 34 * TODO: Finish phpdocs 35 */ 36 abstract class restore_qtype_plugin extends restore_plugin { 37 38 /* 39 * A simple answer to id cache for a single questions answers. 40 * @var array 41 */ 42 private $questionanswercache = array(); 43 44 /* 45 * The id of the current question in the questionanswercache. 46 * @var int 47 */ 48 private $questionanswercacheid = null; 49 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) 55 */ 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); 60 } 61 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); 65 } 66 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) 71 */ 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); 76 } 77 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); 81 } 82 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) 87 */ 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); 92 } 93 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); 97 } 98 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) 103 */ 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); 108 } 109 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); 113 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); 117 } 118 119 /** 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) 123 */ 124 public function process_question_answer($data) { 125 global $DB; 126 127 $data = (object)$data; 128 $oldid = $data->id; 129 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; 134 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', 147 ); 148 if (array_key_exists($data->fraction, $changes)) { 149 $data->fraction = $changes[$data->fraction]; 150 } 151 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); 159 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; 175 } 176 } 177 178 $rules = restore_course_task::define_decode_rules(); 179 $rulesactivity = restore_quiz_activity_task::define_decode_rules(); 180 $rules = array_merge($rules, $rulesactivity); 181 182 $decoder = $this->task->get_decoder(); 183 foreach ($rules as $rule) { 184 $decoder->add_rule($rule); 185 } 186 187 $contentdecoded = $decoder->decode_content($data->answertext); 188 if ($contentdecoded) { 189 $data->answertext = $contentdecoded; 190 } 191 192 if (!isset($this->questionanswercache[$data->answertext])) { 193 // If we haven't found the matching answer, something has gone really wrong, the question in the DB 194 // is missing answers, throw an exception. 195 $info = new stdClass(); 196 $info->filequestionid = $oldquestionid; 197 $info->dbquestionid = $newquestionid; 198 $info->answer = s($data->answertext); 199 throw new restore_step_exception('error_question_answers_missing_in_db', $info); 200 } 201 $newitemid = $this->questionanswercache[$data->answertext]; 202 } 203 // Create mapping (we'll use this intensively when restoring question_states. And also answerfeedback files) 204 $this->set_mapping('question_answer', $oldid, $newitemid); 205 } 206 207 /** 208 * Processes the numerical_unit element (question numerical units). Common for various qtypes. 209 * It handles both creation (if the question is being created) and mapping 210 * (if the question already existed and is being reused) 211 */ 212 public function process_question_numerical_unit($data) { 213 global $DB; 214 215 $data = (object)$data; 216 $oldid = $data->id; 217 218 // Detect if the question is created or mapped 219 $oldquestionid = $this->get_old_parentid('question'); 220 $newquestionid = $this->get_new_parentid('question'); 221 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; 222 223 // If the question has been created by restore, we need to create its question_numerical_units too 224 if ($questioncreated) { 225 // Adjust some columns 226 $data->question = $newquestionid; 227 // Insert record 228 $newitemid = $DB->insert_record('question_numerical_units', $data); 229 } 230 } 231 232 /** 233 * Processes the numerical_option element (question numerical options). Common for various qtypes. 234 * It handles both creation (if the question is being created) and mapping 235 * (if the question already existed and is being reused) 236 */ 237 public function process_question_numerical_option($data) { 238 global $DB; 239 240 $data = (object)$data; 241 $oldid = $data->id; 242 243 // Detect if the question is created or mapped 244 $oldquestionid = $this->get_old_parentid('question'); 245 $newquestionid = $this->get_new_parentid('question'); 246 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; 247 248 // If the question has been created by restore, we need to create its question_numerical_options too 249 if ($questioncreated) { 250 // Adjust some columns 251 $data->question = $newquestionid; 252 // Insert record 253 $newitemid = $DB->insert_record('question_numerical_options', $data); 254 // Create mapping (not needed, no files nor childs nor states here) 255 //$this->set_mapping('question_numerical_option', $oldid, $newitemid); 256 } 257 } 258 259 /** 260 * Processes the dataset_definition element (question dataset definitions). Common for various qtypes. 261 * It handles both creation (if the question is being created) and mapping 262 * (if the question already existed and is being reused) 263 */ 264 public function process_question_dataset_definition($data) { 265 global $DB; 266 267 $data = (object)$data; 268 $oldid = $data->id; 269 270 // Detect if the question is created or mapped 271 $oldquestionid = $this->get_old_parentid('question'); 272 $newquestionid = $this->get_new_parentid('question'); 273 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; 274 275 // If the question is mapped, nothing to do 276 if (!$questioncreated) { 277 return; 278 } 279 280 // Arrived here, let's see if the question_dataset_definition already exists in category or no 281 // (by category, name, type and enough items). Only for "shared" definitions (category != 0). 282 // If exists, reuse it, else, create it as "not shared" (category = 0) 283 $data->category = $this->get_mappingid('question_category', $data->category); 284 // If category is shared, look for definitions 285 $founddefid = null; 286 if ($data->category) { 287 $candidatedefs = $DB->get_records_sql("SELECT id, itemcount 288 FROM {question_dataset_definitions} 289 WHERE category = ? 290 AND name = ? 291 AND type = ?", array($data->category, $data->name, $data->type)); 292 foreach ($candidatedefs as $candidatedef) { 293 if ($candidatedef->itemcount >= $data->itemcount) { // Check it has enough items 294 $founddefid = $candidatedef->id; 295 break; // end loop, shared definition match found 296 } 297 } 298 // If there were candidates but none fulfilled the itemcount condition, create definition as not shared 299 if ($candidatedefs && !$founddefid) { 300 $data->category = 0; 301 } 302 } 303 // If haven't found any shared definition match, let's create it 304 if (!$founddefid) { 305 $newitemid = $DB->insert_record('question_dataset_definitions', $data); 306 // Set mapping, so dataset items will know if they must be created 307 $this->set_mapping('question_dataset_definition', $oldid, $newitemid); 308 309 // If we have found one shared definition match, use it 310 } else { 311 $newitemid = $founddefid; 312 // Set mapping to 0, so dataset items will know they don't need to be created 313 $this->set_mapping('question_dataset_definition', $oldid, 0); 314 } 315 316 // Arrived here, we have one $newitemid (create or reused). Create the question_datasets record 317 $questiondataset = new stdClass(); 318 $questiondataset->question = $newquestionid; 319 $questiondataset->datasetdefinition = $newitemid; 320 $DB->insert_record('question_datasets', $questiondataset); 321 } 322 323 /** 324 * Processes the dataset_item element (question dataset items). Common for various qtypes. 325 * It handles both creation (if the question is being created) and mapping 326 * (if the question already existed and is being reused) 327 */ 328 public function process_question_dataset_item($data) { 329 global $DB; 330 331 $data = (object)$data; 332 $oldid = $data->id; 333 334 // Detect if the question is created or mapped 335 $oldquestionid = $this->get_old_parentid('question'); 336 $newquestionid = $this->get_new_parentid('question'); 337 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; 338 339 // If the question is mapped, nothing to do 340 if (!$questioncreated) { 341 return; 342 } 343 344 // Detect if the question_dataset_definition is being created 345 $newdefinitionid = $this->get_new_parentid('question_dataset_definition'); 346 347 // If the definition is reused, nothing to do 348 if (!$newdefinitionid) { 349 return; 350 } 351 352 // let's create the question_dataset_items 353 $data->definition = $newdefinitionid; 354 $data->itemnumber = $data->number; 355 $DB->insert_record('question_dataset_items', $data); 356 } 357 358 /** 359 * Do any re-coding necessary in the student response. 360 * @param int $questionid the new id of the question 361 * @param int $sequencenumber of the step within the qusetion attempt. 362 * @param array the response data from the backup. 363 * @return array the recoded response. 364 */ 365 public function recode_response($questionid, $sequencenumber, array $response) { 366 return $response; 367 } 368 369 /** 370 * Decode legacy question_states.answer for this qtype. Used when restoring 371 * 2.0 attempt data. 372 */ 373 public function recode_legacy_state_answer($state) { 374 // By default, return answer unmodified, qtypes needing recode will override this 375 return $state->answer; 376 } 377 378 /** 379 * Return the contents of the questions stuff that must be processed by the links decoder 380 * 381 * Only common stuff to all plugins, in this case: 382 * - question: text and feedback 383 * - question_answers: text and feedbak 384 * 385 * Note each qtype will have, if needed, its own define_decode_contents method 386 */ 387 static public function define_plugin_decode_contents() { 388 389 $contents = array(); 390 391 $contents[] = new restore_decode_content('question', array('questiontext', 'generalfeedback'), 'question_created'); 392 $contents[] = new restore_decode_content('question_answers', array('answer', 'feedback'), 'question_answer'); 393 394 return $contents; 395 } 396 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body