NOBUG: Changed to _sql() variant to properly use sql_compare_text(). Kudos to aparup...
[moodle.git] / backup / moodle2 / restore_qtype_plugin.class.php
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.
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/>.
18 /**
19  * @package    moodlecore
20  * @subpackage backup-moodle2
21  * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 /**
26  * Class extending standard restore_plugin in order to implement some
27  * helper methods related with the questions (qtype plugin)
28  *
29  * TODO: Finish phpdocs
30  */
31 abstract class restore_qtype_plugin extends restore_plugin {
33     /**
34      * Add to $paths the restore_path_elements needed
35      * to handle question_answers for a given question
36      * Used by various qtypes (calculated, essay, multianswer,
37      * multichoice, numerical, shortanswer, truefalse)
38      */
39     protected function add_question_question_answers(&$paths) {
40         // Check $paths is one array
41         if (!is_array($paths)) {
42             throw new restore_step_exception('paths_must_be_array', $paths);
43         }
45         $elename = 'question_answer';
46         $elepath = $this->get_pathfor('/answers/answer'); // we used get_recommended_name() so this works
47         $paths[] = new restore_path_element($elename, $elepath);
48     }
50     /**
51      * Add to $paths the restore_path_elements needed
52      * to handle question_numerical_units for a given question
53      * Used by various qtypes (calculated, numerical)
54      */
55     protected function add_question_numerical_units(&$paths) {
56         // Check $paths is one array
57         if (!is_array($paths)) {
58             throw new restore_step_exception('paths_must_be_array', $paths);
59         }
61         $elename = 'question_numerical_unit';
62         $elepath = $this->get_pathfor('/numerical_units/numerical_unit'); // we used get_recommended_name() so this works
63         $paths[] = new restore_path_element($elename, $elepath);
64     }
66     /**
67      * Add to $paths the restore_path_elements needed
68      * to handle question_numerical_options for a given question
69      * Used by various qtypes (calculated, numerical)
70      */
71     protected function add_question_numerical_options(&$paths) {
72         // Check $paths is one array
73         if (!is_array($paths)) {
74             throw new restore_step_exception('paths_must_be_array', $paths);
75         }
77         $elename = 'question_numerical_option';
78         $elepath = $this->get_pathfor('/numerical_options/numerical_option'); // we used get_recommended_name() so this works
79         $paths[] = new restore_path_element($elename, $elepath);
80     }
82     /**
83      * Add to $paths the restore_path_elements needed
84      * to handle question_datasets (defs and items) for a given question
85      * Used by various qtypes (calculated, numerical)
86      */
87     protected function add_question_datasets(&$paths) {
88         // Check $paths is one array
89         if (!is_array($paths)) {
90             throw new restore_step_exception('paths_must_be_array', $paths);
91         }
93         $elename = 'question_dataset_definition';
94         $elepath = $this->get_pathfor('/dataset_definitions/dataset_definition'); // we used get_recommended_name() so this works
95         $paths[] = new restore_path_element($elename, $elepath);
97         $elename = 'question_dataset_item';
98         $elepath = $this->get_pathfor('/dataset_definitions/dataset_definition/dataset_items/dataset_item');
99         $paths[] = new restore_path_element($elename, $elepath);
100     }
102     /**
103      * Processes the answer element (question answers). Common for various qtypes.
104      * It handles both creation (if the question is being created) and mapping
105      * (if the question already existed and is being reused)
106      */
107     public function process_question_answer($data) {
108         global $DB;
110         $data = (object)$data;
111         $oldid = $data->id;
113         // Detect if the question is created or mapped
114         $oldquestionid   = $this->get_old_parentid('question');
115         $newquestionid   = $this->get_new_parentid('question');
116         $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
118         // If the question has been created by restore, we need to create its question_answers too
119         if ($questioncreated) {
120             // Adjust some columns
121             $data->question = $newquestionid;
122             $data->answer = $data->answertext;
123             // Insert record
124             $newitemid = $DB->insert_record('question_answers', $data);
126         // The question existed, we need to map the existing question_answers
127         } else {
128             // Look in question_answers by answertext matching
129             $sql = 'SELECT id
130                       FROM {question_answers}
131                      WHERE question = ?
132                        AND ' . $DB->sql_compare_text('answer', 255) . ' = ' . $DB->sql_compare_text('?', 255);
133             $params = array($newquestionid, $data->answertext);
134             $newitemid = $DB->get_field_sql($sql, $params);
135             // If we haven't found the newitemid, something has gone really wrong, question in DB
136             // is missing answers, exception
137             if (!$newitemid) {
138                 $info = new stdClass();
139                 $info->filequestionid = $oldquestionid;
140                 $info->dbquestionid   = $newquestionid;
141                 $info->answer         = $data->answertext;
142                 throw restore_step_exception('error_question_answers_missing_in_db', $info);
143             }
144         }
145         // Create mapping (we'll use this intensively when restoring question_states. And also answerfeedback files)
146         $this->set_mapping('question_answer', $oldid, $newitemid);
147     }
149     /**
150      * Processes the numerical_unit element (question numerical units). Common for various qtypes.
151      * It handles both creation (if the question is being created) and mapping
152      * (if the question already existed and is being reused)
153      */
154     public function process_question_numerical_unit($data) {
155         global $DB;
157         $data = (object)$data;
158         $oldid = $data->id;
160         // Detect if the question is created or mapped
161         $oldquestionid   = $this->get_old_parentid('question');
162         $newquestionid   = $this->get_new_parentid('question');
163         $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
165         // If the question has been created by restore, we need to create its question_numerical_units too
166         if ($questioncreated) {
167             // Adjust some columns
168             $data->question = $newquestionid;
169             // Insert record
170             $newitemid = $DB->insert_record('question_numerical_units', $data);
171         }
172     }
174     /**
175      * Processes the numerical_option element (question numerical options). Common for various qtypes.
176      * It handles both creation (if the question is being created) and mapping
177      * (if the question already existed and is being reused)
178      */
179     public function process_question_numerical_option($data) {
180         global $DB;
182         $data = (object)$data;
183         $oldid = $data->id;
185         // Detect if the question is created or mapped
186         $oldquestionid   = $this->get_old_parentid('question');
187         $newquestionid   = $this->get_new_parentid('question');
188         $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
190         // If the question has been created by restore, we need to create its question_numerical_options too
191         if ($questioncreated) {
192             // Adjust some columns
193             $data->question = $newquestionid;
194             // Insert record
195             $newitemid = $DB->insert_record('question_numerical_options', $data);
196             // Create mapping (not needed, no files nor childs nor states here)
197             //$this->set_mapping('question_numerical_option', $oldid, $newitemid);
198         }
199     }
201     /**
202      * Processes the dataset_definition element (question dataset definitions). Common for various qtypes.
203      * It handles both creation (if the question is being created) and mapping
204      * (if the question already existed and is being reused)
205      */
206     public function process_question_dataset_definition($data) {
207         global $DB;
209         $data = (object)$data;
210         $oldid = $data->id;
212         // Detect if the question is created or mapped
213         $oldquestionid   = $this->get_old_parentid('question');
214         $newquestionid   = $this->get_new_parentid('question');
215         $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
217         // If the question is mapped, nothing to do
218         if (!$questioncreated) {
219             return;
220         }
222         // Arrived here, let's see if the question_dataset_definition already exists in category or no
223         // (by category, name, type and enough items). Only for "shared" definitions (category != 0).
224         // If exists, reuse it, else, create it as "not shared" (category = 0)
225         $data->category = $this->get_mappingid('question_category', $data->category);
226         // If category is shared, look for definitions
227         $founddefid = null;
228         if ($data->category) {
229             $candidatedefs = $DB->get_records_sql("SELECT id, itemcount
230                                                      FROM {question_dataset_definitions}
231                                                     WHERE category = ?
232                                                       AND name = ?
233                                                       AND type = ?", array($data->category, $data->name, $data->type));
234             foreach ($candidatedefs as $candidatedef) {
235                 if ($candidatedef->itemcount >= $data->itemcount) { // Check it has enough items
236                     $founddefid = $candidatedef->id;
237                     break; // end loop, shared definition match found
238                 }
239             }
240             // If there were candidates but none fulfilled the itemcount condition, create definition as not shared
241             if ($candidatedefs && !$founddefid) {
242                 $data->category = 0;
243             }
244         }
245         // If haven't found any shared definition match, let's create it
246         if (!$founddefid) {
247             $newitemid = $DB->insert_record('question_dataset_definitions', $data);
248             // Set mapping, so dataset items will know if they must be created
249             $this->set_mapping('question_dataset_definition', $oldid, $newitemid);
251         // If we have found one shared definition match, use it
252         } else {
253             $newitemid = $founddefid;
254             // Set mapping to 0, so dataset items will know they don't need to be created
255             $this->set_mapping('question_dataset_definition', $oldid, 0);
256         }
258         // Arrived here, we have one $newitemid (create or reused). Create the question_datasets record
259         $questiondataset = new stdClass();
260         $questiondataset->question = $newquestionid;
261         $questiondataset->datasetdefinition = $newitemid;
262         $DB->insert_record('question_datasets', $questiondataset);
263     }
265     /**
266      * Processes the dataset_item element (question dataset items). Common for various qtypes.
267      * It handles both creation (if the question is being created) and mapping
268      * (if the question already existed and is being reused)
269      */
270     public function process_question_dataset_item($data) {
271         global $DB;
273         $data = (object)$data;
274         $oldid = $data->id;
276         // Detect if the question is created or mapped
277         $oldquestionid   = $this->get_old_parentid('question');
278         $newquestionid   = $this->get_new_parentid('question');
279         $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
281         // If the question is mapped, nothing to do
282         if (!$questioncreated) {
283             return;
284         }
286         // Detect if the question_dataset_definition is being created
287         $newdefinitionid = $this->get_new_parentid('question_dataset_definition');
289         // If the definition is reused, nothing to do
290         if (!$newdefinitionid) {
291             return;
292         }
294         // let's create the question_dataset_items
295         $data->definition = $newdefinitionid;
296         $data->itemnumber = $data->number;
297         $DB->insert_record('question_dataset_items', $data);
298     }
300     /**
301      * Decode one question_states for this qtype (default impl)
302      */
303     public function recode_state_answer($state) {
304         // By default, return answer unmodified, qtypes needing recode will override this
305         return $state->answer;
306     }
308     /**
309      * Return the contents of the questions stuff that must be processed by the links decoder
310      *
311      * Only common stuff to all plugins, in this case:
312      * - question: text and feedback
313      * - question_answers: text and feedbak
314      *
315      * Note each qtype will have, if needed, its own define_decode_contents method
316      */
317     static public function define_plugin_decode_contents() {
319         $contents = array();
321         $contents[] = new restore_decode_content('question', array('questiontext', 'generalfeedback'), 'question_created');
322         $contents[] = new restore_decode_content('question_answers', array('answer', 'feedback'), 'question_answer');
324         return $contents;
325     }