Merge remote-tracking branch 'moodle/master' into MDL-20636_master_new_question_engine
[moodle.git] / backup / moodle2 / restore_stepslib.php
index 7c66910..d37030b 100644 (file)
@@ -2310,6 +2310,17 @@ class restore_create_categories_and_questions extends restore_structure_step {
         // we have loaded qcatids there for all parsed questions
         $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid);
 
+        // In the past, there were some very sloppy values of penalty. Fix them.
+        if ($data->penalty >= 0.33 && $data->penalty <= 0.34) {
+            $data->penalty = 0.3333333;
+        }
+        if ($data->penalty >= 0.66 && $data->penalty <= 0.67) {
+            $data->penalty = 0.6666667;
+        }
+        if ($data->penalty >= 1) {
+            $data->penalty = 1;
+        }
+
         $data->timecreated  = $this->apply_date_offset($data->timecreated);
         $data->timemodified = $this->apply_date_offset($data->timemodified);
 
@@ -2339,6 +2350,47 @@ class restore_create_categories_and_questions extends restore_structure_step {
         // step will be in charge of restoring all the question files
     }
 
+        protected function process_question_hint($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $oldid = $data->id;
+
+        // Detect if the question is created or mapped
+        $oldquestionid   = $this->get_old_parentid('question');
+        $newquestionid   = $this->get_new_parentid('question');
+        $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
+
+        // If the question has been created by restore, we need to create its question_answers too
+        if ($questioncreated) {
+            // Adjust some columns
+            $data->questionid = $newquestionid;
+            // Insert record
+            $newitemid = $DB->insert_record('question_answers', $data);
+
+        // The question existed, we need to map the existing question_answers
+        } else {
+            // Look in question_answers by answertext matching
+            $sql = 'SELECT id
+                      FROM {question_hints}
+                     WHERE questionid = ?
+                       AND ' . $DB->sql_compare_text('hint', 255) . ' = ' . $DB->sql_compare_text('?', 255);
+            $params = array($newquestionid, $data->hint);
+            $newitemid = $DB->get_field_sql($sql, $params);
+            // If we haven't found the newitemid, something has gone really wrong, question in DB
+            // is missing answers, exception
+            if (!$newitemid) {
+                $info = new stdClass();
+                $info->filequestionid = $oldquestionid;
+                $info->dbquestionid   = $newquestionid;
+                $info->hint           = $data->hint;
+                throw new restore_step_exception('error_question_hint_missing_in_db', $info);
+            }
+        }
+        // Create mapping (we'll use this intensively when restoring question_states. And also answerfeedback files)
+        $this->set_mapping('question_hint', $oldid, $newitemid);
+    }
+
     protected function after_execute() {
         global $DB;
 
@@ -2461,6 +2513,8 @@ class restore_create_question_files extends restore_execution_step {
                                               $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true);
             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answerfeedback',
                                               $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true);
+            restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'hint',
+                                              $oldctxid, $this->task->get_userid(), 'question_hint', null, $newctxid, true);
             // Add qtype dependent files
             $components = backup_qtype_plugin::get_components_and_fileareas($question->qtype);
             foreach ($components as $component => $fileareas) {
@@ -2481,12 +2535,16 @@ class restore_create_question_files extends restore_execution_step {
  * (like the quiz module), to support qtype plugins, states and sessions
  */
 abstract class restore_questions_activity_structure_step extends restore_activity_structure_step {
+    /** @var array question_attempt->id to qtype. */
+    protected $qtypes = array();
+    /** @var array question_attempt->id to questionid. */
+    protected $newquestionids = array();
 
     /**
      * Attach below $element (usually attempts) the needed restore_path_elements
-     * to restore question_states
+     * to restore question_usages and all they contain.
      */
-    protected function add_question_attempts_states($element, &$paths) {
+    protected function add_question_usages($element, &$paths) {
         // Check $element is restore_path_element
         if (! $element instanceof restore_path_element) {
             throw new restore_step_exception('element_must_be_restore_path_element', $element);
@@ -2495,70 +2553,114 @@ abstract class restore_questions_activity_structure_step extends restore_activit
         if (!is_array($paths)) {
             throw new restore_step_exception('paths_must_be_array', $paths);
         }
-        $paths[] = new restore_path_element('question_state', $element->get_path() . '/states/state');
+        $paths[] = new restore_path_element('question_usage',
+                $element->get_path() . '/question_usage');
+        $paths[] = new restore_path_element('question_attempt',
+                $element->get_path() . '/question_usage/question_attempts/question_attempt');
+        $paths[] = new restore_path_element('question_attempt_step',
+                $element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step',
+                true);
+        $paths[] = new restore_path_element('question_attempt_step_data',
+                $element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step/response/variable');
+
+        // TODO Put back code for restoring legacy 2.0 backups.
+        // $paths[] = new restore_path_element('question_state', $element->get_path() . '/states/state');
+        // $paths[] = new restore_path_element('question_session', $element->get_path() . '/sessions/session');
     }
 
     /**
-     * Attach below $element (usually attempts) the needed restore_path_elements
-     * to restore question_sessions
+     * Process question_usages
      */
-    protected function add_question_attempts_sessions($element, &$paths) {
-        // Check $element is restore_path_element
-        if (! $element instanceof restore_path_element) {
-            throw new restore_step_exception('element_must_be_restore_path_element', $element);
-        }
-        // Check $paths is one array
-        if (!is_array($paths)) {
-            throw new restore_step_exception('paths_must_be_array', $paths);
-        }
-        $paths[] = new restore_path_element('question_session', $element->get_path() . '/sessions/session');
+    protected function process_question_usage($data) {
+        global $DB;
+
+        // Clear our caches.
+        $this->qtypes = array();
+        $this->newquestionids = array();
+
+        $data = (object)$data;
+        $oldid = $data->id;
+
+        $oldcontextid = $this->get_task()->get_old_contextid();
+        $data->contextid  = $this->get_mappingid('context', $this->task->get_old_contextid());
+
+        // Everything ready, insert (no mapping needed)
+        $newitemid = $DB->insert_record('question_usages', $data);
+
+        $this->inform_new_usage_id($newitemid);
+
+        $this->set_mapping('question_usage', $oldid, $newitemid, false);
     }
 
     /**
-     * Process question_states
+     * When process_question_usage creates the new usage, it calls this method
+     * to let the activity link to the new usage. For example, the quiz uses
+     * this method to set quiz_attempts.uniqueid to the new usage id.
+     * @param integer $newusageid
+     */
+    abstract protected function inform_new_usage_id($newusageid);
+
+    /**
+     * Process question_attempts
      */
-    protected function process_question_state($data) {
+    protected function process_question_attempt($data) {
         global $DB;
 
         $data = (object)$data;
         $oldid = $data->id;
+        $question = $this->get_mapping('question', $data->questionid);
 
-        // Get complete question mapping, we'll need info
-        $question = $this->get_mapping('question', $data->question);
+        $data->questionusageid = $this->get_new_parentid('question_usage');
+        $data->questionid      = $question->newitemid;
+        $data->timemodified    = $this->apply_date_offset($data->timemodified);
 
-        // In the quiz_attempt mapping we are storing uniqueid
-        // and not id, so this gets the correct question_attempt to point to
-        $data->attempt  = $this->get_new_parentid('quiz_attempt');
-        $data->question = $question->newitemid;
-        $data->answer   = $this->restore_recode_answer($data, $question->info->qtype); // Delegate recoding of answer
-        $data->timestamp= $this->apply_date_offset($data->timestamp);
+        $newitemid = $DB->insert_record('question_attempts', $data);
 
-        // Everything ready, insert and create mapping (needed by question_sessions)
-        $newitemid = $DB->insert_record('question_states', $data);
-        $this->set_mapping('question_state', $oldid, $newitemid);
+        $this->set_mapping('question_attempt', $oldid, $newitemid);
+        $this->qtypes[$newitemid] = $question->info->qtype;
+        $this->newquestionids[$newitemid] = $data->questionid;
     }
 
     /**
-     * Process question_sessions
+     * Process question_attempt_steps
      */
-    protected function process_question_session($data) {
+    protected function process_question_attempt_step($data) {
         global $DB;
 
         $data = (object)$data;
         $oldid = $data->id;
 
-        // In the quiz_attempt mapping we are storing uniqueid
-        // and not id, so this gets the correct question_attempt to point to
-        $data->attemptid  = $this->get_new_parentid('quiz_attempt');
-        $data->questionid = $this->get_mappingid('question', $data->questionid);
-        $data->newest     = $this->get_mappingid('question_state', $data->newest);
-        $data->newgraded  = $this->get_mappingid('question_state', $data->newgraded);
+        // Pull out the response data.
+        $response = array();
+        if (!empty($data->response['variable'])) {
+            foreach ($data->response['variable'] as $variable) {
+                $response[$variable['name']] = $variable['value'];
+            }
+        }
+        unset($data->response);
 
-        // Everything ready, insert (no mapping needed)
-        $newitemid = $DB->insert_record('question_sessions', $data);
+        $data->questionattemptid = $this->get_new_parentid('question_attempt');
+        $data->timecreated = $this->apply_date_offset($data->timecreated);
+        $data->userid      = $this->get_mappingid('user', $data->userid);
 
-        // Note: question_sessions haven't files associated. On purpose manualcomment is lacking
-        // support for them, so we don't need to handle them here.
+        // Everything ready, insert and create mapping (needed by question_sessions)
+        $newitemid = $DB->insert_record('question_attempt_steps', $data);
+        $this->set_mapping('question_attempt_step', $oldid, $newitemid, true);
+
+        // Now process the response data.
+        $qtyperestorer = $this->get_qtype_restorer($this->qtypes[$data->questionattemptid]);
+        if ($qtyperestorer) {
+            $response = $qtyperestorer->recode_response(
+                    $this->newquestionids[$data->questionattemptid],
+                    $data->sequencenumber, $response);
+        }
+        foreach ($response as $name => $value) {
+            $row = new stdClass();
+            $row->attemptstepid = $newitemid;
+            $row->name = $name;
+            $row->value = $value;
+            $DB->insert_record('question_attempt_step_data', $row, false);
+        }
     }
 
     /**
@@ -2581,25 +2683,33 @@ abstract class restore_questions_activity_structure_step extends restore_activit
     }
 
     /**
-     * Given one question_states record, return the answer
-     * recoded pointing to all the restored stuff
+     * Get the restore_qtype_plugin subclass for a specific question type.
+     * @param string $qtype e.g. multichoice.
+     * @return restore_qtype_plugin instance.
      */
-    public function restore_recode_answer($state, $qtype) {
+    public function get_qtype_restorer($qtype) {
         // Build one static cache to store {@link restore_qtype_plugin}
         // while we are needing them, just to save zillions of instantiations
         // or using static stuff that will break our nice API
         static $qtypeplugins = array();
 
-        // If we haven't the corresponding restore_qtype_plugin for current qtype
-        // instantiate it and add to cache
         if (!isset($qtypeplugins[$qtype])) {
             $classname = 'restore_qtype_' . $qtype . '_plugin';
             if (class_exists($classname)) {
                 $qtypeplugins[$qtype] = new $classname('qtype', $qtype, $this);
             } else {
-                $qtypeplugins[$qtype] = false;
+                $qtypeplugins[$qtype] = null;
             }
         }
-        return !empty($qtypeplugins[$qtype]) ? $qtypeplugins[$qtype]->recode_state_answer($state) : $state->answer;
+        return $qtypeplugins[$qtype];
+    }
+
+    protected function after_execute() {
+        parent::after_execute();
+
+        // Restore any files belonging to responses.
+        foreach (question_engine::get_all_response_file_areas() as $filearea) {
+            $this->add_related_files('question', $filearea, 'question_attempt_step');
+        }
     }
 }