MDL-35987 qtype_multianswer: Prevent restore failure due to bad sequence
[moodle.git] / question / type / multianswer / backup / moodle2 / restore_qtype_multianswer_plugin.class.php
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/>.
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  */
25 defined('MOODLE_INTERNAL') || die();
27 require_once($CFG->dirroot . '/question/type/multianswer/questiontype.php');
28 /**
29  * restore plugin class that provides the necessary information
30  * needed to restore one multianswer qtype plugin
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
34  */
35 class restore_qtype_multianswer_plugin extends restore_qtype_plugin {
37     /**
38      * Returns the paths to be handled by the plugin at question level
39      */
40     protected function define_question_plugin_structure() {
41         $paths = array();
43         // This qtype uses question_answers, add them.
44         $this->add_question_question_answers($paths);
46         // Add own qtype stuff.
47         $elename = 'multianswer';
48         $elepath = $this->get_pathfor('/multianswer');
49         $paths[] = new restore_path_element($elename, $elepath);
51         return $paths; // And we return the interesting paths.
52     }
54     /**
55      * Process the qtype/multianswer element
56      */
57     public function process_multianswer($data) {
58         global $DB;
60         $data = (object)$data;
61         $oldid = $data->id;
63         // Detect if the question is created or mapped.
64         $oldquestionid   = $this->get_old_parentid('question');
65         $newquestionid   = $this->get_new_parentid('question');
66         $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
68         // If the question has been created by restore, we need to create its
69         // question_multianswer too.
70         if ($questioncreated) {
71             // Adjust some columns.
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
76             // that gets executed once all questions have been created.
77             // Insert record.
78             $newitemid = $DB->insert_record('question_multianswer', $data);
79             // Create mapping (need it for after_execute recode of sequence).
80             $this->set_mapping('question_multianswer', $oldid, $newitemid);
81         }
82     }
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
96         // the created question_multianswer sequences (list of question ids).
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()));
104         foreach ($rs as $rec) {
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             }
110             foreach ($sequencearr as $key => $question) {
111                 $sequencearr[$key] = $this->get_mappingid('question', $question);
112             }
113             $sequence = implode(',', $sequencearr);
114             $DB->set_field('question_multianswer', 'sequence', $sequence,
115                     array('id' => $rec->id));
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);
125                             if (isset($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE]) &&
126                                     $answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE] !== '') {
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             }
135         }
136         $rs->close();
137     }
139     public function recode_response($questionid, $sequencenumber, array $response) {
140         global $DB;
142         $qtypes = $DB->get_records_menu('question', array('parent' => $questionid),
143                 '', 'id, qtype');
145         $sequence = $DB->get_field('question_multianswer', 'sequence',
146                 array('question' => $questionid));
148         $fakestep = new question_attempt_step_read_only($response);
150         foreach (explode(',', $sequence) as $key => $subqid) {
151             $i = $key + 1;
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());
157             foreach ($recodedresponse as $name => $value) {
158                 $response[$substep->add_prefix($name)] = $value;
159             }
160         }
162         return $response;
163     }
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      */
174     public function recode_legacy_state_answer($state) {
175         global $DB;
176         $answer = $state->answer;
177         $resultarr = array();
178         // Get sequence of questions.
179         $sequence = $DB->get_field('question_multianswer', 'sequence',
180                 array('question' => $state->question));
181         $sequencearr = explode(',', $sequence);
182         // Let's process each pair.
183         foreach (explode(',', $answer) as $pair) {
184             $pairarr = explode('-', $pair);
185             $sequenceid = $pairarr[0];
186             $subanswer = $pairarr[1];
187             // Calculate the questionid based on sequenceid.
188             // Note it is already one *new* questionid that doesn't need mapping.
189             $questionid = $sequencearr[$sequenceid - 1];
190             // Fetch qtype of the question (needed for delegation).
191             $questionqtype = $DB->get_field('question', 'qtype', array('id' => $questionid));
192             // Delegate subanswer recode to proper qtype, faking one question_states record.
193             $substate = new stdClass();
194             $substate->question = $questionid;
195             $substate->answer = $subanswer;
196             $newanswer = $this->step->restore_recode_legacy_answer($substate, $questionqtype);
197             $resultarr[] = implode('-', array($sequenceid, $newanswer));
198         }
199         return implode(',', $resultarr);
200     }