diff --git a/backup/moodle2/backup_final_task.class.php b/backup/moodle2/backup_final_task.class.php index 83058866636cf..d39a20e610a44 100644 --- a/backup/moodle2/backup_final_task.class.php +++ b/backup/moodle2/backup_final_task.class.php @@ -48,6 +48,9 @@ public function build() { // including membership based on setting $this->add_step(new backup_groups_structure_step('groups', 'groups.xml')); + // Generate the questions file with the final annotated question_categories + $this->add_step(new backup_questions_structure_step('questions', 'questions.xml')); + // Annotate all the question files for the already annotated question // categories (this is performed here and not in the structure step because // it involves multiple contexts and as far as we are always backup-ing @@ -55,9 +58,6 @@ public function build() { // done in a single pass $this->add_step(new backup_annotate_all_question_files('question_files')); - // Generate the questions file with the final annotated question_categories - $this->add_step(new backup_questions_structure_step('questions', 'questions.xml')); - // Annotate all the user files (conditionally) (private, profile and icon files) // Because each user has its own context, we need a separate/specialised step here // This step also ensures that the contexts for all the users exist, so next diff --git a/backup/moodle2/backup_qtype_plugin.class.php b/backup/moodle2/backup_qtype_plugin.class.php index e44cf397cb7ae..c0d48436f4260 100644 --- a/backup/moodle2/backup_qtype_plugin.class.php +++ b/backup/moodle2/backup_qtype_plugin.class.php @@ -143,7 +143,7 @@ protected function add_question_datasets($element) { $items->add_child($item); // Set the sources - $definition->set_source_sql('SELECT * + $definition->set_source_sql('SELECT qdd.* FROM {question_dataset_definitions} qdd JOIN {question_datasets} qd ON qd.datasetdefinition = qdd.id WHERE qd.question = ?', array(backup::VAR_PARENTID)); @@ -155,4 +155,52 @@ protected function add_question_datasets($element) { // don't need to annotate ids nor files } + + /** + * Returns all the components and fileareas used by all the installed qtypes + * + * The method introspects each qtype, asking it about fileareas used. Then, + * one 2-level array is returned. 1st level is the component name (qtype_xxxx) + * and 2nd level is one array of filearea => mappings to look + * + * Note that this function is used both in backup and restore, so it is important + * to use the same mapping names (usually, name of the table in singular) always + * + * TODO: Surely this can be promoted to backup_plugin easily and make it to + * work for ANY plugin, not only qtypes (but we don't need it for now) + */ + public static function get_components_and_fileareas($filter = null) { + $components = array(); + // Get all the plugins of this type + $qtypes = get_plugin_list('qtype'); + foreach ($qtypes as $name => $path) { + // Apply filter if specified + if (!is_null($filter) && $filter != $name) { + continue; + } + // Calculate the componentname + $componentname = 'qtype_' . $name; + // Get the plugin fileareas (all them MUST belong to the same component) + $classname = 'backup_qtype_' . $name . '_plugin'; + if (class_exists($classname)) { + $elements = call_user_func(array($classname, 'get_qtype_fileareas')); + if ($elements) { + // If there are elements, add them to $components + $components[$componentname] = $elements; + } + } + } + return $components; + } + + /** + * Returns one array with filearea => mappingname elements for the qtype + * + * Used by {@link get_components_and_fileareas} to know about all the qtype + * files to be processed both in backup and restore. + */ + public static function get_qtype_fileareas() { + // By default, return empty array, only qtypes having own fileareas will override this + return array(); + } } diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index e0f6c9f5de85c..d03ae24390fed 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -160,7 +160,7 @@ protected function prepare_activity_structure($activitystructure) { /** * Abstract structure step, to be used by all the activities using core questions stuff - * (namelu quiz module), supporting question plugins, states and sessions + * (namely quiz module), supporting question plugins, states and sessions */ abstract class backup_questions_activity_structure_step extends backup_activity_structure_step { @@ -1562,10 +1562,20 @@ protected function define_execution() { JOIN {backup_ids_temp} bi ON bi.itemid = qc.id WHERE bi.backupid = ? AND bi.itemname = 'question_categoryfinal'", array($this->get_backupid())); + // To know about qtype specific components/fileareas + $components = backup_qtype_plugin::get_components_and_fileareas(); + // Let's loop foreach($rs as $record) { // We don't need to specify filearea nor itemid as far as by // component and context it's enough to annotate the whole bank files + // This backups "questiontext", "generalfeedback" and "answerfeedback" fileareas (all them + // belonging to the "question" component backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', null, null); + // Again, it is enough to pick files only by context and component + // Do it for qtype specific components + foreach ($components as $component => $fileareas) { + backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, $component, null, null); + } } $rs->close(); } @@ -1620,6 +1630,7 @@ protected function define_structure() { $question->set_source_table('question', array('category' => backup::VAR_PARENTID)); // don't need to annotate ids nor files + // (already done by {@link backup_annotate_all_question_files} return $qcategories; } diff --git a/backup/moodle2/restore_final_task.class.php b/backup/moodle2/restore_final_task.class.php index 9febd7269546f..cd82ab8f3e279 100644 --- a/backup/moodle2/restore_final_task.class.php +++ b/backup/moodle2/restore_final_task.class.php @@ -35,6 +35,14 @@ class restore_final_task extends restore_task { */ public function build() { + // Move all the CONTEXT_MODULE question qcats to their + // final (newly created) module context + $this->add_step(new restore_move_module_questions_categories('move_module_question_categories')); + + // Create all the question files now that every question is in place + // and every category has its final contextid associated + $this->add_step(new restore_create_question_files('create_question_files')); + // Review all the block_position records in backup_ids in order // match them now that all the contexts are created populating DB // as needed. Only if we are restoring blocks. diff --git a/backup/moodle2/restore_plan_builder.class.php b/backup/moodle2/restore_plan_builder.class.php index 3f096899e4dfd..46ea999c091ae 100644 --- a/backup/moodle2/restore_plan_builder.class.php +++ b/backup/moodle2/restore_plan_builder.class.php @@ -31,6 +31,10 @@ require_once($CFG->dirroot . '/backup/moodle2/restore_final_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_block_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_default_block_task.class.php'); +require_once($CFG->dirroot . '/backup/moodle2/restore_plugin.class.php'); +require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_plugin.class.php'); +require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php'); +require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_subplugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_settingslib.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_stepslib.php'); diff --git a/backup/moodle2/restore_plugin.class.php b/backup/moodle2/restore_plugin.class.php new file mode 100644 index 0000000000000..bb0898de6e600 --- /dev/null +++ b/backup/moodle2/restore_plugin.class.php @@ -0,0 +1,172 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Class implementing the plugins support for moodle2 restore + * + * TODO: Finish phpdocs + * TODO: Add support for declaring decode_contents (not decode_rules) + */ +abstract class restore_plugin { + + protected $plugintype; + protected $pluginname; + protected $connectionpoint; + protected $step; + protected $task; + + public function __construct($plugintype, $pluginname, $step) { + $this->plugintype = $plugintype; + $this->pluginname = $pluginname; + $this->step = $step; + $this->task = $step->get_task(); + $this->connectionpoint = ''; + } + + public function define_plugin_structure($connectionpoint) { + if (!$connectionpoint instanceof restore_path_element) { + throw new restore_step_exception('restore_path_element_required', $connectionpoint); + } + + $paths = array(); + $this->connectionpoint = $connectionpoint; + $methodname = 'define_' . basename($this->connectionpoint->get_path()) . '_plugin_structure'; + + if (method_exists($this, $methodname)) { + if ($bluginpaths = $this->$methodname()) { + foreach ($bluginpaths as $path) { + $path->set_processing_object($this); + $paths[] = $path; + } + } + } + return $paths; + } + + /** + * after_execute dispatcher for any restore_plugin class + * + * This method will dispatch execution to the corresponding + * after_execute_xxx() method when available, with xxx + * being the connection point of the instance, so plugin + * classes with multiple connection points will support + * multiple after_execute methods, one for each connection point + */ + public function launch_after_execute_methods() { + // Check if the after_execute method exists and launch it + $afterexecute = 'after_execute_' . basename($this->connectionpoint->get_path()); + if (method_exists($this, $afterexecute)) { + $this->$afterexecute(); + } + } + +// Protected API starts here + +// restore_step/structure_step/task wrappers + + protected function get_restoreid() { + if (is_null($this->task)) { + throw new restore_step_exception('not_specified_restore_task'); + } + return $this->task->get_restoreid(); + } + + /** + * To send ids pairs to backup_ids_table and to store them into paths + * + * This method will send the given itemname and old/new ids to the + * backup_ids_temp table, and, at the same time, will save the new id + * into the corresponding restore_path_element for easier access + * by children. Also will inject the known old context id for the task + * in case it's going to be used for restoring files later + */ + protected function set_mapping($itemname, $oldid, $newid, $restorefiles = false, $filesctxid = null, $parentid = null) { + $this->step->set_mapping($itemname, $oldid, $newid, $restorefiles, $filesctxid, $parentid); + } + + /** + * Returns the latest (parent) old id mapped by one pathelement + */ + protected function get_old_parentid($itemname) { + return $this->step->get_old_parentid($itemname); + } + + /** + * Returns the latest (parent) new id mapped by one pathelement + */ + protected function get_new_parentid($itemname) { + return $this->step->get_new_parentid($itemname); + } + + /** + * Return the new id of a mapping for the given itemname + * + */ + protected function get_mappingid($itemname, $oldid) { + return $this->step->get_mappingid($itemname, $oldid); + } + + /** + * Return the complete mapping from the given itemname, itemid + */ + protected function get_mapping($itemname, $oldid) { + return $this->step->get_mapping($itemname, $oldid); + } + + /** + * Add all the existing file, given their component and filearea and one backup_ids itemname to match with + */ + protected function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) { + $this->step->add_related_files($component, $filearea, $mappingitemname, $filesctxid, $olditemid); + } + + /** + * Apply course startdate offset based in original course startdate and course_offset_startdate setting + * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple + * executions in the same request + */ + protected function apply_date_offset($value) { + return $this->step->apply_date_offset($value); + } + + /** + * Simple helper function that returns the name for the restore_path_element + * It's not mandatory to use it but recommended ;-) + */ + protected function get_namefor($name = '') { + $name = $name !== '' ? '_' . $name : ''; + return $this->plugintype . '_' . $this->pluginname . $name; + } + + /** + * Simple helper function that returns the base (prefix) of the path for the restore_path_element + * Useful if we used get_recommended_name() in backup. It's not mandatory to use it but recommended ;-) + */ + protected function get_pathfor($path = '') { + $path = trim($path, '/') !== '' ? '/' . trim($path, '/') : ''; + return $this->connectionpoint->get_path() . '/' . + 'plugin_' . $this->plugintype . '_' . + $this->pluginname . '_' . basename($this->connectionpoint->get_path()) . $path; + } +} diff --git a/backup/moodle2/restore_qtype_plugin.class.php b/backup/moodle2/restore_qtype_plugin.class.php new file mode 100644 index 0000000000000..91cebff60ad05 --- /dev/null +++ b/backup/moodle2/restore_qtype_plugin.class.php @@ -0,0 +1,302 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Class extending standard restore_plugin in order to implement some + * helper methods related with the questions (qtype plugin) + * + * TODO: Finish phpdocs + */ +abstract class restore_qtype_plugin extends restore_plugin { + + /** + * Add to $paths the restore_path_elements needed + * to handle question_answers for a given question + * Used by various qtypes (calculated, essay, multianswer, + * multichoice, numerical, shortanswer, truefalse) + */ + protected function add_question_question_answers(&$paths) { + // Check $paths is one array + if (!is_array($paths)) { + throw new restore_step_exception('paths_must_be_array', $paths); + } + + $elename = 'question_answer'; + $elepath = $this->get_pathfor('/answers/answer'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + } + + /** + * Add to $paths the restore_path_elements needed + * to handle question_numerical_units for a given question + * Used by various qtypes (calculated, numerical) + */ + protected function add_question_numerical_units(&$paths) { + // Check $paths is one array + if (!is_array($paths)) { + throw new restore_step_exception('paths_must_be_array', $paths); + } + + $elename = 'question_numerical_unit'; + $elepath = $this->get_pathfor('/numerical_units/numerical_unit'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + } + + /** + * Add to $paths the restore_path_elements needed + * to handle question_numerical_options for a given question + * Used by various qtypes (calculated, numerical) + */ + protected function add_question_numerical_options(&$paths) { + // Check $paths is one array + if (!is_array($paths)) { + throw new restore_step_exception('paths_must_be_array', $paths); + } + + $elename = 'question_numerical_option'; + $elepath = $this->get_pathfor('/numerical_options/numerical_option'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + } + + /** + * Add to $paths the restore_path_elements needed + * to handle question_datasets (defs and items) for a given question + * Used by various qtypes (calculated, numerical) + */ + protected function add_question_datasets(&$paths) { + // Check $paths is one array + if (!is_array($paths)) { + throw new restore_step_exception('paths_must_be_array', $paths); + } + + $elename = 'question_dataset_definition'; + $elepath = $this->get_pathfor('/dataset_definitions/dataset_definition'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + $elename = 'question_dataset_item'; + $elepath = $this->get_pathfor('/dataset_definitions/dataset_definition/dataset_items/dataset_item'); + $paths[] = new restore_path_element($elename, $elepath); + } + + /** + * Processes the answer element (question answers). Common for various qtypes. + * It handles both creation (if the question is being created) and mapping + * (if the question already existed and is being reused) + */ + public function process_question_answer($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->question = $newquestionid; + $data->answer = $data->answertext; + // 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 + $newitemid = $DB->get_field('question_answers', 'id', array('question' => $newquestionid, 'answer' => $data->answertext)); + // 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->answer = $data->answertext; + throw restore_step_exception('error_question_answers_missing_in_db', $info); + } + } + // Create mapping (we'll use this intensively when restoring question_states. And also answerfeedback files) + $this->set_mapping('question_answer', $oldid, $newitemid); + } + + /** + * Processes the numerical_unit element (question numerical units). Common for various qtypes. + * It handles both creation (if the question is being created) and mapping + * (if the question already existed and is being reused) + */ + public function process_question_numerical_unit($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_numerical_units too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + // Insert record + $newitemid = $DB->insert_record('question_numerical_units', $data); + } + } + + /** + * Processes the numerical_option element (question numerical options). Common for various qtypes. + * It handles both creation (if the question is being created) and mapping + * (if the question already existed and is being reused) + */ + public function process_question_numerical_option($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_numerical_options too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + // Insert record + $newitemid = $DB->insert_record('question_numerical_options', $data); + // Create mapping (not needed, no files nor childs nor states here) + //$this->set_mapping('question_numerical_option', $oldid, $newitemid); + } + } + + /** + * Processes the dataset_definition element (question dataset definitions). Common for various qtypes. + * It handles both creation (if the question is being created) and mapping + * (if the question already existed and is being reused) + */ + public function process_question_dataset_definition($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 is mapped, nothing to do + if (!$questioncreated) { + return; + } + + // Arrived here, let's see if the question_dataset_definition already exists in category or no + // (by category, name, type and enough items). Only for "shared" definitions (category != 0). + // If exists, reuse it, else, create it as "not shared" (category = 0) + $data->category = $this->get_mappingid('question_category', $data->category); + // If category is shared, look for definitions + $founddefid = null; + if ($data->category) { + $candidatedefs = $DB->get_records_sql("SELECT id, itemcount + FROM {question_dataset_definitions} + WHERE category = ? + AND name = ? + AND type = ?", array($data->category, $data->name, $data->type)); + foreach ($candidatedefs as $candidatedef) { + if ($candidatedef->itemcount >= $data->itemcount) { // Check it has enough items + $founddefid = $candidatedef->id; + break; // end loop, shared definition match found + } + } + // If there were candidates but none fulfilled the itemcount condition, create definition as not shared + if ($candidatedefs && !$founddefid) { + $data->category = 0; + } + } + // If haven't found any shared definition match, let's create it + if (!$founddefid) { + $newitemid = $DB->insert_record('question_dataset_definitions', $data); + // Set mapping, so dataset items will know if they must be created + $this->set_mapping('question_dataset_definition', $oldid, $newitemid); + + // If we have found one shared definition match, use it + } else { + $newitemid = $founddefid; + // Set mapping to 0, so dataset items will know they don't need to be created + $this->set_mapping('question_dataset_definition', $oldid, 0); + } + + // Arrived here, we have one $newitemid (create or reused). Create the question_datasets record + $questiondataset = new stdClass(); + $questiondataset->question = $newquestionid; + $questiondataset->datasetdefinition = $newitemid; + $DB->insert_record('question_datasets', $questiondataset); + } + + /** + * Processes the dataset_item element (question dataset items). Common for various qtypes. + * It handles both creation (if the question is being created) and mapping + * (if the question already existed and is being reused) + */ + public function process_question_dataset_item($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 is mapped, nothing to do + if (!$questioncreated) { + return; + } + + // Detect if the question_dataset_definition is being created + $newdefinitionid = $this->get_new_parentid('question_dataset_definition'); + + // If the definition is reused, nothing to do + if (!$newdefinitionid) { + return; + } + + // let's create the question_dataset_items + $data->definition = $newdefinitionid; + $data->itemnumber = $data->number; + $DB->insert_record('question_dataset_items', $data); + } + + /** + * Decode one question_states for this qtype (default impl) + */ + public function recode_state_answer($state) { + // By default, return answer unmodified, qtypes needing recode will override this + return $state->answer; + } +} diff --git a/backup/moodle2/restore_root_task.class.php b/backup/moodle2/restore_root_task.class.php index 239d20bd82c9c..04b7f4b5c3832 100644 --- a/backup/moodle2/restore_root_task.class.php +++ b/backup/moodle2/restore_root_task.class.php @@ -40,6 +40,9 @@ public function build() { // If we haven't preloaded information, load all the included inforef records to temp_ids table $this->add_step(new restore_load_included_inforef_records('load_inforef_records')); + // Load all the needed files to temp_ids table + $this->add_step(new restore_load_included_files('load_file_records', 'files.xml')); + // If we haven't preloaded information, load all the needed roles to temp_ids_table $this->add_step(new restore_load_and_map_roles('load_and_map_roles')); @@ -47,13 +50,10 @@ public function build() { $this->add_step(new restore_load_included_users('load_user_records')); // If we haven't preloaded information and are restoring user info, process all those needed users - // creating/mapping them as needed. Any problem here will cause exception as far as prechecks have + // marking for create/map them as needed. Any problem here will cause exception as far as prechecks have // performed the same process so, it's not possible to have errors here $this->add_step(new restore_process_included_users('process_user_records')); - // Load all the needed files to temp_ids table - $this->add_step(new restore_load_included_files('load_file_records', 'files.xml')); - // Unconditionally, create all the needed users calculated in the previous step $this->add_step(new restore_create_included_users('create_users')); @@ -66,6 +66,18 @@ public function build() { // Unconditionally, load create all the needed outcomes $this->add_step(new restore_outcomes_structure_step('create_scales', 'outcomes.xml')); + // If we haven't preloaded information, load all the needed categories and questions (reduced) to temp_ids_table + $this->add_step(new restore_load_categories_and_questions('load_categories_and_questions')); + + // If we haven't preloaded information, process all the loaded categories and questions + // marking them for creation/mapping as needed. Any problem here will cause exception + // because this same process has been executed and reported by restore prechecks, so + // it is not possible to have errors here. + $this->add_step(new restore_process_categories_and_questions('process_categories_and_questions')); + + // Unconditionally, create and map all the categories and questions + $this->add_step(new restore_create_categories_and_questions('create_categories_and_questions', 'questions.xml')); + // At the end, mark it as built $this->built = true; } diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 8b5bf18474fa1..9b799d26d4db0 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -488,10 +488,13 @@ public function process_file($data) { // load it if needed: // - it it is one of the annotated inforef files (course/section/activity/block) - // - it is one "user", "group", "grouping" or "grade" component file (that aren't sent to inforef ever) + // - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever) + // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use, + // but then we'll need to change it to load plugins itself (because this is executed too early in restore) $isfileref = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id); $iscomponent = ($data->component == 'user' || $data->component == 'group' || - $data->component == 'grouping' || $data->component == 'grade'); + $data->component == 'grouping' || $data->component == 'grade' || + $data->component == 'question' || substr($data->component, 0, 5) == 'qtype'); if ($isfileref || $iscomponent) { restore_dbops::set_backup_files_record($this->get_restoreid(), $data); } @@ -833,6 +836,43 @@ protected function after_execute() { } } +/** + * Execution step that, *conditionally* (if there isn't preloaded information + * will load all the question categories and questions (header info only) + * to backup_temp_ids. They will be stored with "question_category" and + * "question" itemnames and with their original contextid and question category + * id as paremitemids + */ +class restore_load_categories_and_questions extends restore_execution_step { + + protected function define_execution() { + + if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do + return; + } + $file = $this->get_basepath() . '/questions.xml'; + restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file); + } +} + +/** + * Execution step that, *conditionally* (if there isn't preloaded information) + * will process all the needed categories and questions + * in order to decide and perform any action with them (create / map / error) + * Note: Any error will cause exception, as far as this is the same processing + * than the one into restore prechecks (that should have stopped process earlier) + */ +class restore_process_categories_and_questions extends restore_execution_step { + + protected function define_execution() { + + if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do + return; + } + restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite()); + } +} + /** * Structure step that will read the section.xml creating/updating sections * as needed, rebuilding course cache and other friends @@ -1989,3 +2029,366 @@ protected function apply_activity_instance($newitemid) { $this->set_mapping($modulename, $oldid, $newitemid, true); } } + +/** + * Structure step in charge of creating/mapping all the qcats and qs + * by parsing the questions.xml file and checking it against the + * results calculated by {@link restore_process_categories_and_questions} + * and stored in backup_ids_temp + */ +class restore_create_categories_and_questions extends restore_structure_step { + + protected function define_structure() { + + $category = new restore_path_element('question_category', '/question_categories/question_category'); + $question = new restore_path_element('question', '/question_categories/question_category/questions/question'); + + // Apply for 'qtype' plugins optional paths at question level + $this->add_plugin_structure('qtype', $question); + + return array($category, $question); + } + + protected function process_question_category($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + // Check we have one mapping for this category + if (!$mapping = $this->get_mapping('question_category', $oldid)) { + return; // No mapping = this category doesn't need to be created/mapped + } + + // Check we have to create the category (newitemid = 0) + if ($mapping->newitemid) { + return; // newitemid != 0, this category is going to be mapped. Nothing to do + } + + // Arrived here, newitemid = 0, we need to create the category + // we'll do it at parentitemid context, but for CONTEXT_MODULE + // categories, that will be created at CONTEXT_COURSE and moved + // to module context later when the activity is created + if ($mapping->info->contextlevel == CONTEXT_MODULE) { + $mapping->parentitemid = $this->get_mappingid('context', $this->task->get_old_contextid()); + } + $data->contextid = $mapping->parentitemid; + + // Let's create the question_category and save mapping + $newitemid = $DB->insert_record('question_categories', $data); + $this->set_mapping('question_category', $oldid, $newitemid); + // Also annotate them as question_category_created, we need + // that later when remapping parents + $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid); + } + + protected function process_question($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + // Check we have one mapping for this question + if (!$questionmapping = $this->get_mapping('question', $oldid)) { + return; // No mapping = this question doesn't need to be created/mapped + } + + // Get the mapped category (cannot use get_new_parentid() because not + // all the categories have been created, so it is not always available + // Instead we get the mapping for the question->parentitemid because + // we have loaded qcatids there for all parsed questions + $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid); + + $data->timecreated = $this->apply_date_offset($data->timecreated); + $data->timemodified = $this->apply_date_offset($data->timemodified); + + $userid = $this->get_mappingid('user', $data->createdby); + $data->createdby = $userid ? $userid : $this->task->get_userid(); + + $userid = $this->get_mappingid('user', $data->modifiedby); + $data->modifiedby = $userid ? $userid : $this->task->get_userid(); + + // With newitemid = 0, let's create the question + if (!$questionmapping->newitemid) { + $newitemid = $DB->insert_record('question', $data); + $this->set_mapping('question', $oldid, $newitemid); + // Also annotate them as question_created, we need + // that later when remapping parents (keeping the old categoryid as parentid) + $this->set_mapping('question_created', $oldid, $newitemid, false, null, $questionmapping->parentitemid); + } else { + // By performing this set_mapping() we make get_old/new_parentid() to work for all the + // children elements of the 'question' one (so qtype plugins will know the question they belong to) + $this->set_mapping('question', $oldid, $questionmapping->newitemid); + } + + // Note, we don't restore any question files yet + // as far as the CONTEXT_MODULE categories still + // haven't their contexts to be restored to + // The {@link restore_create_question_files}, executed in the final step + // step will be in charge of restoring all the question files + } + + protected function after_execute() { + global $DB; + + // First of all, recode all the created question_categories->parent fields + $qcats = $DB->get_records('backup_ids_temp', array( + 'backupid' => $this->get_restoreid(), + 'itemname' => 'question_category_created')); + foreach ($qcats as $qcat) { + $newparent = 0; + $dbcat = $DB->get_record('question_categories', array('id' => $qcat->newitemid)); + // Get new parent (mapped or created, so we look in quesiton_category mappings) + if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array( + 'backupid' => $this->get_restoreid(), + 'itemname' => 'question_category', + 'itemid' => $dbcat->parent))) { + // contextids must match always, as far as we always include complete qbanks, just check it + $newparentctxid = $DB->get_field('question_categories', 'contextid', array('id' => $newparent)); + if ($dbcat->contextid == $newparentctxid) { + $DB->set_field('question_categories', 'parent', $newparent, array('id' => $dbcat->id)); + } else { + $newparent = 0; // No ctx match for both cats, no parent relationship + } + } + // Here with $newparent empty, problem with contexts or remapping, set it to top cat + if (!$newparent) { + $DB->set_field('question_categories', 'parent', 0, array('id' => $dbcat->id)); + } + } + + // Now, recode all the created question->parent fields + $qs = $DB->get_records('backup_ids_temp', array( + 'backupid' => $this->get_restoreid(), + 'itemname' => 'question_created')); + foreach ($qs as $q) { + $newparent = 0; + $dbq = $DB->get_record('question', array('id' => $q->newitemid)); + // Get new parent (mapped or created, so we look in question mappings) + if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array( + 'backupid' => $this->get_restoreid(), + 'itemname' => 'question', + 'itemid' => $dbq->parent))) { + $DB->set_field('question', 'parent', $newparent, array('id' => $dbq->id)); + } + } + + // Note, we don't restore any question files yet + // as far as the CONTEXT_MODULE categories still + // haven't their contexts to be restored to + // The {@link restore_create_question_files}, executed in the final step + // step will be in charge of restoring all the question files + } +} + +/** + * Execution step that will move all the CONTEXT_MODULE question categories + * created at early stages of restore in course context (because modules weren't + * created yet) to their target module (matching by old-new-contextid mapping) + */ +class restore_move_module_questions_categories extends restore_execution_step { + + protected function define_execution() { + global $DB; + + $contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE); + foreach ($contexts as $contextid => $contextlevel) { + // Only if context mapping exists (i.e. the module has been restored) + if ($newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) { + // Update all the qcats having their parentitemid set to the original contextid + $modulecats = $DB->get_records_sql("SELECT itemid, newitemid + FROM {backup_ids_temp} + WHERE backupid = ? + AND itemname = 'question_category' + AND parentitemid = ?", array($this->get_restoreid(), $contextid)); + foreach ($modulecats as $modulecat) { + $DB->set_field('question_categories', 'contextid', $newcontext->newitemid, array('id' => $modulecat->newitemid)); + // And set new contextid also in question_category mapping (will be + // used by {@link restore_create_question_files} later + restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid, $modulecat->newitemid, $newcontext->newitemid); + } + } + } + } +} + +/** + * Execution step that will create all the question/answers/qtype-specific files for the restored + * questions. It must be executed after {@link restore_move_module_questions_categories} + * because only then each question is in its final category and only then the + * context can be determined + * + * TODO: Improve this. Instead of looping over each question, it can be reduced to + * be done by contexts (this will save a huge ammount of queries) + */ +class restore_create_question_files extends restore_execution_step { + + protected function define_execution() { + global $DB; + + // Let's process only created questions + $questionsrs = $DB->get_recordset_sql("SELECT bi.itemid, bi.newitemid, bi.parentitemid, q.qtype + FROM {backup_ids_temp} bi + JOIN {question} q ON q.id = bi.newitemid + WHERE bi.backupid = ? + AND bi.itemname = 'question_created'", array($this->get_restoreid())); + foreach ($questionsrs as $question) { + // Get question_category mapping, it contains the target context for the question + if (!$qcatmapping = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'question_category', $question->parentitemid)) { + // Something went really wrong, cannot find the question_category for the question + debugging('Error fetching target context for question', DEBUG_DEVELOPER); + continue; + } + // Calculate source and target contexts + $oldctxid = $qcatmapping->info->contextid; + $newctxid = $qcatmapping->parentitemid; + + // Add common question files (question and question_answer ones) + restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'questiontext', + $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true); + restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'generalfeedback', + $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); + // Add qtype dependent files + $components = backup_qtype_plugin::get_components_and_fileareas($question->qtype); + foreach ($components as $component => $fileareas) { + foreach ($fileareas as $filearea => $mapping) { + // Use itemid only if mapping is question_created + $itemid = ($mapping == 'question_created') ? $question->itemid : null; + restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, $filearea, + $oldctxid, $this->task->get_userid(), $mapping, $itemid, $newctxid, true); + } + } + } + $questionsrs->close(); + } +} + +/** + * Abstract structure step, to be used by all the activities using core questions stuff + * (like the quiz module), to support qtype plugins, states and sessions + */ +abstract class restore_questions_activity_structure_step extends restore_activity_structure_step { + + /** + * Attach below $element (usually attempts) the needed restore_path_elements + * to restore question_states + */ + protected function add_question_attempts_states($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_state', $element->get_path() . '/states/state'); + } + + /** + * Attach below $element (usually attempts) the needed restore_path_elements + * to restore question_sessions + */ + 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'); + } + + /** + * Process question_states + */ + protected function process_question_state($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + // Get complete question mapping, we'll need info + $question = $this->get_mapping('question', $data->question); + + // 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); + + // Everything ready, insert and create mapping (needed by question_sessions) + $newitemid = $DB->insert_record('question_states', $data); + $this->set_mapping('question_state', $oldid, $newitemid); + } + + /** + * Process question_sessions + */ + protected function process_question_session($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); + + // Everything ready, insert (no mapping needed) + $newitemid = $DB->insert_record('question_sessions', $data); + + // Note: question_sessions haven't files associated. On purpose manualcomment is lacking + // support for them, so we don't need to handle them here. + } + + /** + * Given a list of question->ids, separated by commas, returns the + * recoded list, with all the restore question mappings applied. + * Note: Used by quiz->questions and quiz_attempts->layout + * Note: 0 = page break (unconverted) + */ + protected function questions_recode_layout($layout) { + // Extracts question id from sequence + if ($questionids = explode(',', $layout)) { + foreach ($questionids as $id => $questionid) { + if ($questionid) { // If it is zero then this is a pagebreak, don't translate + $newquestionid = $this->get_mappingid('question', $questionid); + $questionids[$id] = $newquestionid; + } + } + } + return implode(',', $questionids); + } + + /** + * Given one question_states record, return the answer + * recoded pointing to all the restored stuff + */ + public function restore_recode_answer($state, $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; + } + } + return !empty($qtypeplugins[$qtype]) ? $qtypeplugins[$qtype]->recode_state_answer($state) : $state->answer; + } +} diff --git a/backup/moodle2/restore_subplugin.class.php b/backup/moodle2/restore_subplugin.class.php index 5c95b1f2a0afb..0f732dc4dc5c8 100644 --- a/backup/moodle2/restore_subplugin.class.php +++ b/backup/moodle2/restore_subplugin.class.php @@ -64,6 +64,23 @@ public function define_subplugin_structure($connectionpoint) { return $paths; } + /** + * after_execute dispatcher for any restore_subplugin class + * + * This method will dispatch execution to the corresponding + * after_execute_xxx() method when available, with xxx + * being the connection point of the instance, so subplugin + * classes with multiple connection points will support + * multiple after_execute methods, one for each connection point + */ + public function launch_after_execute_methods() { + // Check if the after_execute method exists and launch it + $afterexecute = 'after_execute_' . basename($this->connectionpoint->get_path()); + if (method_exists($this, $afterexecute)) { + $this->$afterexecute(); + } + } + // Protected API starts here // restore_step/structure_step/task wrappers diff --git a/backup/restorelib.php b/backup/restorelib.php index fd5781e65471a..afd7c82d54096 100644 --- a/backup/restorelib.php +++ b/backup/restorelib.php @@ -1,28 +1,4 @@ libdir.'/gradelib.php'); - -/** - * Group backup/restore constants, 0. - */ -define('RESTORE_GROUPS_NONE', 0); - -/** - * Group backup/restore constants, 1. - */ -define('RESTORE_GROUPS_ONLY', 1); - -/** - * Group backup/restore constants, 2. - */ -define('RESTORE_GROUPINGS_ONLY', 2); - -/** - * Group backup/restore constants, course/all. - */ -define('RESTORE_GROUPS_GROUPINGS', 3); - //This function iterates over all modules in backup file, searching for a //MODNAME_refresh_events() to execute. Perhaps it should ve moved to central Moodle... function restore_refresh_events($restore) { @@ -84,811 +60,6 @@ function restore_set_format_data($restore,$xml_file) { return true; } - - /** - * This function creates all the gradebook data from xml - */ - function restore_create_gradebook($restore,$xml_file) { - global $CFG, $DB; - - $status = true; - //Check it exists - if (!file_exists($xml_file)) { - return false; - } - - // Get info from xml - // info will contain the number of record to process - $info = restore_read_xml_gradebook($restore, $xml_file); - - // If we have info, then process - if (empty($info)) { - return $status; - } - - if (empty($CFG->disablegradehistory) and isset($info->gradebook_histories) and $info->gradebook_histories == "true") { - $restore_histories = true; - } else { - $restore_histories = false; - } - - // make sure top course category exists - $course_category = grade_category::fetch_course_category($restore->course_id); - $course_category->load_grade_item(); - - // we need to know if all grade items that were backed up are being restored - // if that is not the case, we do not restore grade categories nor gradeitems of category type or course type - // i.e. the aggregated grades of that category - - $restoreall = true; // set to false if any grade_item is not selected/restored or already exist - $importing = !empty($SESSION->restore->importing); - - if ($importing) { - $restoreall = false; - - } else { - $prev_grade_items = grade_item::fetch_all(array('courseid'=>$restore->course_id)); - $prev_grade_cats = grade_category::fetch_all(array('courseid'=>$restore->course_id)); - - // if any categories already present, skip restore of categories from backup - course item or category already exist - if (count($prev_grade_items) > 1 or count($prev_grade_cats) > 1) { - $restoreall = false; - } - unset($prev_grade_items); - unset($prev_grade_cats); - - if ($restoreall) { - if ($recs = $DB->get_records("backup_ids", array('table_name'=>'grade_items', 'backup_code'=>$restore->backup_unique_code), "", "old_id")) { - foreach ($recs as $rec) { - if ($data = backup_getid($restore->backup_unique_code,'grade_items',$rec->old_id)) { - - $info = $data->info; - // do not restore if this grade_item is a mod, and - $itemtype = backup_todb($info['GRADE_ITEM']['#']['ITEMTYPE']['0']['#']); - - if ($itemtype == 'mod') { - $olditeminstance = backup_todb($info['GRADE_ITEM']['#']['ITEMINSTANCE']['0']['#']); - $itemmodule = backup_todb($info['GRADE_ITEM']['#']['ITEMMODULE']['0']['#']); - - if (empty($restore->mods[$itemmodule]->granular)) { - continue; - } else if (!empty($restore->mods[$itemmodule]->instances[$olditeminstance]->restore)) { - continue; - } - // at least one activity should not be restored - do not restore categories and manual items at all - $restoreall = false; - break; - } - } - } - } - } - } - - // Start ul - if (!defined('RESTORE_SILENTLY')) { - echo ''; - } - return $status; - } - //This function creates all the structures messages and contacts function restore_create_messages($restore,$xml_file) { global $CFG, $DB; diff --git a/backup/util/dbops/restore_dbops.class.php b/backup/util/dbops/restore_dbops.class.php index 1e9ac82427d35..98fd2ce053482 100644 --- a/backup/util/dbops/restore_dbops.class.php +++ b/backup/util/dbops/restore_dbops.class.php @@ -233,17 +233,421 @@ public static function load_users_to_tempids($restoreid, $usersfile) { $xmlparser->process(); } + /** + * Load the needed questions.xml file to backup_ids table for future reference + */ + public static function load_categories_and_questions_to_tempids($restoreid, $questionsfile) { + + if (!file_exists($questionsfile)) { // Shouldn't happen ever, but... + throw new backup_helper_exception('missing_questions_xml_file', $questionsfile); + } + // Let's parse, custom processor will do its work, sending info to DB + $xmlparser = new progressive_parser(); + $xmlparser->set_file($questionsfile); + $xmlprocessor = new restore_questions_parser_processor($restoreid); + $xmlparser->set_processor($xmlprocessor); + $xmlparser->process(); + } + + /** + * Check all the included categories and questions, deciding the action to perform + * for each one (mapping / creation) and returning one array of problems in case + * something is wrong. + * + * There are some basic rules that the method below will always try to enforce: + * + * Rule1: Targets will be, always, calculated for *whole* question banks (a.k.a. contexid source), + * so, given 2 question categories belonging to the same bank, their target bank will be + * always the same. If not, we can be incurring into "fragmentation", leading to random/cloze + * problems (qtypes having "child" questions). + * + * Rule2: The 'moodle/question:managecategory' and 'moodle/question:add' capabilities will be + * checked before creating any category/question respectively and, if the cap is not allowed + * into upper contexts (system, coursecat)) but in lower ones (course), the *whole* question bank + * will be created there. + * + * Rule3: Coursecat question banks not existing in the target site will be created as course + * (lower ctx) question banks, never as "guessed" coursecat question banks base on depth or so. + * + * Rule4: System question banks will be created at system context if user has perms to do so. Else they + * will created as course (lower ctx) question banks (similary to rule3). In other words, course ctx + * if always a fallback for system and coursecat question banks. + * + * Also, there are some notes to clarify the scope of this method: + * + * Note1: This method won't create any question category nor question at all. It simply will calculate + * which actions (create/map) must be performed for each element and where, validating that all those + * actions are doable by the user executing the restore operation. Any problem found will be + * returned in the problems array, causing the restore process to stop with error. + * + * Note2: To decide if one question bank (all its question categories and questions) is going to be remapped, + * then all the categories and questions must exist in the same target bank. If able to do so, missing + * qcats and qs will be created (rule2). But if, at the end, something is missing, the whole question bank + * will be recreated at course ctx (rule1), no matter if that duplicates some categories/questions. + * + * Note3: We'll be using the newitemid column in the temp_ids table to store the action to be performed + * with each question category and question. newitemid = 0 means the qcat/q needs to be created and + * any other value means the qcat/q is mapped. Also, for qcats, parentitemid will contain the target + * context where the categories have to be created (but for module contexts where we'll keep the old + * one until the activity is created) + * + * Note4: All these "actions" will be "executed" later by {@link restore_create_categories_and_questions} + */ + public static function precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite) { + + $problems = array(); + + // TODO: Check all qs, looking their qtypes are restorable + + // Precheck all qcats and qs looking for target contexts / warnings / errors + list($syserr, $syswarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_SYSTEM); + list($caterr, $catwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSECAT); + list($couerr, $couwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSE); + list($moderr, $modwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_MODULE); + + // Acummulate and handle errors and warnings + $errors = array_merge($syserr, $caterr, $couerr, $moderr); + $warnings = array_merge($syswarn, $catwarn, $couwarn, $modwarn); + if (!empty($errors)) { + $problems['errors'] = $errors; + } + if (!empty($warnings)) { + $problems['warnings'] = $warnings; + } + return $problems; + } + + /** + * This function will process all the question banks present in restore + * at some contextlevel (from CONTEXT_SYSTEM to CONTEXT_MODULE), finding + * the target contexts where each bank will be restored and returning + * warnings/errors as needed. + * + * Some contextlevels (system, coursecat), will delegate process to + * course level if any problem is found (lack of permissions, non-matching + * target context...). Other contextlevels (course, module) will + * cause return error if some problem is found. + * + * At the end, if no errors were found, all the categories in backup_temp_ids + * will be pointing (parentitemid) to the target context where they must be + * created later in the restore process. + * + * Note: at the time these prechecks are executed, activities haven't been + * created yet so, for CONTEXT_MODULE banks, we keep the old contextid + * in the parentitemid field. Once the activity (and its context) has been + * created, we'll update that context in the required qcats + * + * Caller {@link precheck_categories_and_questions} will, simply, execute + * this function for all the contextlevels, acting as a simple controller + * of warnings and errors. + * + * The function returns 2 arrays, one containing errors and another containing + * warnings. Both empty if no errors/warnings are found. + */ + public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, $contextlevel) { + global $CFG, $DB; + + // To return any errors and warnings found + $errors = array(); + $warnings = array(); + + // Specify which fallbacks must be performed + $fallbacks = array( + CONTEXT_SYSTEM => CONTEXT_COURSE, + CONTEXT_COURSECAT => CONTEXT_COURSE); + + // For any contextlevel, follow this process logic: + // + // 0) Iterate over each context (qbank) + // 1) Iterate over each qcat in the context, matching by stamp for the found target context + // 2a) No match, check if user can create qcat and q + // 3a) User can, mark the qcat and all dependent qs to be created in that target context + // 3b) User cannot, check if we are in some contextlevel with fallback + // 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop + // 4b) No fallback, error. End qcat loop. + // 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version + // 5a) No match, check if user can add q + // 6a) User can, mark the q to be created + // 6b) User cannot, check if we are in some contextlevel with fallback + // 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop + // 7b) No fallback, error. End qcat loop + // 5b) Match, mark q to be mapped + + // Get all the contexts (question banks) in restore for the given contextlevel + $contexts = self::restore_get_question_banks($restoreid, $contextlevel); + + // 0) Iterate over each context (qbank) + foreach ($contexts as $contextid => $contextlevel) { + // Init some perms + $canmanagecategory = false; + $canadd = false; + // get categories in context (bank) + $categories = self::restore_get_question_categories($restoreid, $contextid); + // cache permissions if $targetcontext is found + if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) { + $canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid); + $canadd = has_capability('moodle/question:add', $targetcontext, $userid); + } + // 1) Iterate over each qcat in the context, matching by stamp for the found target context + foreach ($categories as $category) { + $matchcat = false; + if ($targetcontext) { + $matchcat = $DB->get_record('question_categories', array( + 'contextid' => $targetcontext->id, + 'stamp' => $category->stamp)); + } + // 2a) No match, check if user can create qcat and q + if (!$matchcat) { + // 3a) User can, mark the qcat and all dependent qs to be created in that target context + if ($canmanagecategory && $canadd) { + // Set parentitemid to targetcontext, BUT for CONTEXT_MODULE categories, where + // we keep the source contextid unmodified (for easier matching later when the + // activities are created) + $parentitemid = $targetcontext->id; + if ($contextlevel == CONTEXT_MODULE) { + $parentitemid = null; // null means "not modify" a.k.a. leave original contextid + } + self::set_backup_ids_record($restoreid, 'question_category', $category->id, 0, $parentitemid); + // Nothing else to mark, newitemid = 0 means create + + // 3b) User cannot, check if we are in some contextlevel with fallback + } else { + // 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop + if (array_key_exists($contextlevel, $fallbacks)) { + foreach ($categories as $movedcat) { + $movedcat->contextlevel = $fallbacks[$contextlevel]; + self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat); + // Warn about the performed fallback + $warnings[] = get_string('qcategory2coursefallback', 'backup', $movedcat); + } + + // 4b) No fallback, error. End qcat loop. + } else { + $errors[] = get_string('qcategorycannotberestored', 'backup', $category); + } + break; // out from qcat loop (both 4a and 4b), we have decided about ALL categories in context (bank) + } + + // 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version + } else { + self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id); + $questions = self::restore_get_questions($restoreid, $category->id); + foreach ($questions as $question) { + $matchq = $DB->get_record('question', array( + 'category' => $matchcat->id, + 'stamp' => $question->stamp, + 'version' => $question->version)); + // 5a) No match, check if user can add q + if (!$matchq) { + // 6a) User can, mark the q to be created + if ($canadd) { + // Nothing to mark, newitemid means create + + // 6b) User cannot, check if we are in some contextlevel with fallback + } else { + // 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loo + if (array_key_exists($contextlevel, $fallbacks)) { + foreach ($categories as $movedcat) { + $movedcat->contextlevel = $fallbacks[$contextlevel]; + self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat); + // Warn about the performed fallback + $warnings[] = get_string('question2coursefallback', 'backup', $movedcat); + } + + // 7b) No fallback, error. End qcat loop + } else { + $errors[] = get_string('questioncannotberestored', 'backup', $question); + } + break 2; // out from qcat loop (both 7a and 7b), we have decided about ALL categories in context (bank) + } + + // 5b) Match, mark q to be mapped + } else { + self::set_backup_ids_record($restoreid, 'question', $question->id, $matchq->id); + } + } + } + } + } + + return array($errors, $warnings); + } + + /** + * Return one array of contextid => contextlevel pairs + * of question banks to be checked for one given restore operation + * ordered from CONTEXT_SYSTEM downto CONTEXT_MODULE + * If contextlevel is specified, then only banks corresponding to + * that level are returned + */ + public static function restore_get_question_banks($restoreid, $contextlevel = null) { + global $DB; + + $results = array(); + $qcats = $DB->get_records_sql("SELECT itemid, parentitemid AS contextid + FROM {backup_ids_temp} + WHERE backupid = ? + AND itemname = 'question_category'", array($restoreid)); + foreach ($qcats as $qcat) { + // If this qcat context haven't been acummulated yet, do that + if (!isset($results[$qcat->contextid])) { + $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid); + // Filter by contextlevel if necessary + if (is_null($contextlevel) || $contextlevel == $temprec->info->contextlevel) { + $results[$qcat->contextid] = $temprec->info->contextlevel; + } + } + } + // Sort by value (contextlevel from CONTEXT_SYSTEM downto CONTEXT_MODULE) + asort($results); + return $results; + } + + /** + * Return one array of question_category records for + * a given restore operation and one restore context (question bank) + */ + public static function restore_get_question_categories($restoreid, $contextid) { + global $DB; + + $results = array(); + $qcats = $DB->get_records_sql("SELECT itemid + FROM {backup_ids_temp} + WHERE backupid = ? + AND itemname = 'question_category' + AND parentitemid = ?", array($restoreid, $contextid)); + foreach ($qcats as $qcat) { + $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid); + $results[$qcat->itemid] = $temprec->info; + } + return $results; + } + + /** + * Calculates the best context found to restore one collection of qcats, + * al them belonging to the same context (question bank), returning the + * target context found (object) or false + */ + public static function restore_find_best_target_context($categories, $courseid, $contextlevel) { + global $DB; + + $targetcontext = false; + + // Depending of $contextlevel, we perform different actions + switch ($contextlevel) { + // For system is easy, the best context is the system context + case CONTEXT_SYSTEM: + $targetcontext = get_context_instance(CONTEXT_SYSTEM); + break; + + // For coursecat, we are going to look for stamps in all the + // course categories between CONTEXT_SYSTEM and CONTEXT_COURSE + // (i.e. in all the course categories in the path) + // + // And only will return one "best" target context if all the + // matches belong to ONE and ONLY ONE context. If multiple + // matches are found, that means that there is some annoying + // qbank "fragmentation" in the categories, so we'll fallback + // to create the qbank at course level + case CONTEXT_COURSECAT: + // Build the array of stamps we are going to match + $stamps = array(); + foreach ($categories as $category) { + $stamps[] = $category->stamp; + } + $contexts = array(); + // Build the array of contexts we are going to look + $systemctx = get_context_instance(CONTEXT_SYSTEM); + $coursectx = get_context_instance(CONTEXT_COURSE, $courseid); + $parentctxs= get_parent_contexts($coursectx); + foreach ($parentctxs as $parentctx) { + // Exclude system context + if ($parentctx == $systemctx->id) { + continue; + } + $contexts[] = $parentctx; + } + if (!empty($stamps) && !empty($contexts)) { + // Prepare the query + list($stamp_sql, $stamp_params) = $DB->get_in_or_equal($stamps); + list($context_sql, $context_params) = $DB->get_in_or_equal($contexts); + $sql = "SELECT contextid + FROM {question_categories} + WHERE stamp $stamp_sql + AND contextid $context_sql"; + $params = array_merge($stamp_params, $context_params); + $matchingcontexts = $DB->get_records_sql($sql, $params); + // Only if ONE and ONLY ONE context is found, use it as valid target + if (count($matchingcontexts) == 1) { + $targetcontext = get_context_instance_by_id(reset($matchingcontexts)->contextid); + } + } + break; + + // For course is easy, the best context is the course context + case CONTEXT_COURSE: + $targetcontext = get_context_instance(CONTEXT_COURSE, $courseid); + break; + + // For module is easy, there is not best context, as far as the + // activity hasn't been created yet. So we return context course + // for them, so permission checks and friends will work. Note this + // case is handled by {@link prechek_precheck_qbanks_by_level} + // in an special way + case CONTEXT_MODULE: + $targetcontext = get_context_instance(CONTEXT_COURSE, $courseid); + break; + } + return $targetcontext; + } + + /** + * Return one array of question records for + * a given restore operation and one question category + */ + public static function restore_get_questions($restoreid, $qcatid) { + global $DB; + + $results = array(); + $qs = $DB->get_records_sql("SELECT itemid + FROM {backup_ids_temp} + WHERE backupid = ? + AND itemname = 'question' + AND parentitemid = ?", array($restoreid, $qcatid)); + foreach ($qs as $q) { + $temprec = self::get_backup_ids_record($restoreid, 'question', $q->itemid); + $results[$q->itemid] = $temprec->info; + } + return $results; + } + /** * Given one component/filearea/context and * optionally one source itemname to match itemids * put the corresponding files in the pool */ - public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null) { + public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false) { global $DB; - // Get new context, must exist or this will fail - if (!$newcontextid = self::get_backup_ids_record($restoreid, 'context', $oldcontextid)->newitemid) { - throw new restore_dbops_exception('unknown_context_mapping', $oldcontextid); + if ($forcenewcontextid) { + // Some components can have "forced" new contexts (example: questions can end belonging to non-standard context mappings, + // with questions originally at system/coursecat context in source being restored to course context in target). So we need + // to be able to force the new contextid + $newcontextid = $forcenewcontextid; + } else { + // Get new context, must exist or this will fail + if (!$newcontextid = self::get_backup_ids_record($restoreid, 'context', $oldcontextid)->newitemid) { + throw new restore_dbops_exception('unknown_context_mapping', $oldcontextid); + } + } + + // Sometimes it's possible to have not the oldcontextids stored into backup_ids_temp->parentitemid + // columns (because we have used them to store other information). This happens usually with + // all the question related backup_ids_temp records. In that case, it's safe to ignore that + // matching as far as we are always restoring for well known oldcontexts and olditemids + $parentitemctxmatchsql = ' AND i.parentitemid = f.contextid '; + if ($skipparentitemidctxmatch) { + $parentitemctxmatchsql = ''; } // Important: remember how files have been loaded to backup_files_temp @@ -262,16 +666,16 @@ public static function send_files_to_pool($basepath, $restoreid, $component, $fi // itemname not null, going to join with backup_ids to perform the old-new mapping of itemids } else { - $sql = 'SELECT f.contextid, f.component, f.filearea, f.itemid, i.newitemid, f.info + $sql = "SELECT f.contextid, f.component, f.filearea, f.itemid, i.newitemid, f.info FROM {backup_files_temp} f JOIN {backup_ids_temp} i ON i.backupid = f.backupid - AND i.parentitemid = f.contextid + $parentitemctxmatchsql AND i.itemid = f.itemid WHERE f.backupid = ? AND f.contextid = ? AND f.component = ? AND f.filearea = ? - AND i.itemname = ?'; + AND i.itemname = ?"; $params = array($restoreid, $oldcontextid, $component, $filearea, $itemname); if ($olditemid !== null) { // Just process ONE olditemid intead of the whole itemname $sql .= ' AND i.itemid = ?'; @@ -760,11 +1164,11 @@ public static function precheck_included_users($restoreid, $courseid, $userid, $ } /** - * Process the needed users in order to create / map them + * Process the needed users in order to decide + * which action to perform with them (create/map) * * Just wrap over precheck_included_users(), returning - * exception if any problem is found or performing the - * required user creations if needed + * exception if any problem is found */ public static function process_included_users($restoreid, $courseid, $userid, $samesite) { global $DB; @@ -779,6 +1183,27 @@ public static function process_included_users($restoreid, $courseid, $userid, $s } } + /** + * Process the needed question categories and questions + * to check all them, deciding about the action to perform + * (create/map) and target. + * + * Just wrap over precheck_categories_and_questions(), returning + * exception if any problem is found + */ + public static function process_categories_and_questions($restoreid, $courseid, $userid, $samesite) { + global $DB; + + // Just let precheck_included_users() to do all the hard work + $problems = self::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite); + + // With problems of type error, throw exception, shouldn't happen if prechecks were originally + // executed, so be radical here. + if (array_key_exists('errors', $problems)) { + throw new restore_dbops_exception('restore_problems_processing_questions', null, implode(', ', $problems)); + } + } + public static function set_backup_files_record($restoreid, $filerec) { global $DB; diff --git a/backup/util/helper/restore_prechecks_helper.class.php b/backup/util/helper/restore_prechecks_helper.class.php index 6361cd29937b8..04335160ef68a 100644 --- a/backup/util/helper/restore_prechecks_helper.class.php +++ b/backup/util/helper/restore_prechecks_helper.class.php @@ -120,6 +120,14 @@ public static function execute_prechecks($controller, $droptemptablesafter = fal $warnings = array_key_exists('warnings', $problems) ? array_merge($warnings, $problems['warnings']) : $warnings; } + // Check we are able to restore and the categories and questions + $file = $controller->get_plan()->get_basepath() . '/questions.xml'; + restore_dbops::load_categories_and_questions_to_tempids($restoreid, $file); + if ($problems = restore_dbops::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite)) { + $errors = array_key_exists('errors', $problems) ? array_merge($errors, $problems['errors']) : $errors; + $warnings = array_key_exists('warnings', $problems) ? array_merge($warnings, $problems['warnings']) : $warnings; + } + // Prepare results and return $results = array(); if (!empty($errors)) { diff --git a/backup/util/helper/restore_questions_parser_processor.class.php b/backup/util/helper/restore_questions_parser_processor.class.php new file mode 100644 index 0000000000000..c15a9f2d3dad6 --- /dev/null +++ b/backup/util/helper/restore_questions_parser_processor.class.php @@ -0,0 +1,87 @@ +. + +/** + * @package moodlecore + * @subpackage backup-helper + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); + +/** + * helper implementation of grouped_parser_processor that will + * load all the categories and questions (header info only) from then questions.xml file + * to the backup_ids table storing the whole structure there for later processing. + * Note: only "needed" categories are loaded (must have question_categoryref record in backup_ids) + * Note: parentitemid will contain the category->contextid for categories + * Note: parentitemid will contain the category->id for questions + * + * TODO: Complete phpdocs + */ +class restore_questions_parser_processor extends grouped_parser_processor { + + protected $restoreid; + protected $lastcatid; + + public function __construct($restoreid) { + $this->restoreid = $restoreid; + $this->lastcatid = 0; + parent::__construct(array()); + // Set the paths we are interested on + $this->add_path('/question_categories/question_category'); + $this->add_path('/question_categories/question_category/questions/question'); + } + + protected function dispatch_chunk($data) { + // Prepare question_category record + if ($data['path'] == '/question_categories/question_category') { + $info = (object)$data['tags']; + $itemname = 'question_category'; + $itemid = $info->id; + $parentitemid = $info->contextid; + $this->lastcatid = $itemid; + + // Prepare question record + } else if ($data['path'] == '/question_categories/question_category/questions/question') { + $info = (object)$data['tags']; + $itemname = 'question'; + $itemid = $info->id; + $parentitemid = $this->lastcatid; + + // Not question_category nor question, impossible. Throw exception. + } else { + throw new progressive_parser_exception('restore_questions_parser_processor_unexpected_path', $data['path']); + } + + // Only load it if needed (exist same question_categoryref itemid in table) + if (restore_dbops::get_backup_ids_record($this->restoreid, 'question_categoryref', $this->lastcatid)) { + restore_dbops::set_backup_ids_record($this->restoreid, $itemname, $itemid, 0, $parentitemid, $info); + } + } + + /** + * Provide NULL decoding + */ + public function process_cdata($cdata) { + if ($cdata === '$@NULL@$') { + return null; + } + return $cdata; + } +} diff --git a/backup/util/includes/restore_includes.php b/backup/util/includes/restore_includes.php index 69dc7c6157528..4452a146b943c 100644 --- a/backup/util/includes/restore_includes.php +++ b/backup/util/includes/restore_includes.php @@ -40,6 +40,7 @@ require_once($CFG->dirroot . '/backup/util/helper/restore_inforef_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_users_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_roles_parser_processor.class.php'); +require_once($CFG->dirroot . '/backup/util/helper/restore_questions_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_structure_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_decode_rule.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_decode_content.class.php'); diff --git a/backup/util/plan/backup_structure_step.class.php b/backup/util/plan/backup_structure_step.class.php index adf99e6fcf3f7..970e7755d6dc2 100644 --- a/backup/util/plan/backup_structure_step.class.php +++ b/backup/util/plan/backup_structure_step.class.php @@ -101,10 +101,10 @@ public function execute() { // Protected API starts here /** - * Add plugin structure to any element in the activity backup tree + * Add plugin structure to any element in the structure backup tree * * @param string $plugintype type of plugin as defined by get_plugin_types() - * @param backup_nested_element $element element in the activity backup tree that + * @param backup_nested_element $element element in the structure backup tree that * we are going to add plugin information to * @param bool $multiple to define if multiple plugins can produce information * for each instance of $element (true) or no (false) diff --git a/backup/util/plan/restore_structure_step.class.php b/backup/util/plan/restore_structure_step.class.php index 26efb6a830525..aef63fabf97a6 100644 --- a/backup/util/plan/restore_structure_step.class.php +++ b/backup/util/plan/restore_structure_step.class.php @@ -98,8 +98,8 @@ public function execute() { // And process it, dispatch to target methods in step will start automatically $xmlparser->process(); - // Have finished, call to the after_execute method - $this->after_execute(); + // Have finished, launch the after_execute method of all the processing objects + $this->launch_after_execute_methods(); } /** @@ -238,8 +238,97 @@ public function apply_date_offset($value) { return $value + $cache[$this->get_restoreid()]; } + /** + * As far as restore structure steps are implementing restore_plugin stuff, they need to + * have the parent task available for wrapping purposes (get course/context....) + */ + public function get_task() { + return $this->task; + } + // Protected API starts here + /** + * Add plugin structure to any element in the structure restore tree + * + * @param string $plugintype type of plugin as defined by get_plugin_types() + * @param restore_path_element $element element in the structure restore tree that + * we are going to add plugin information to + */ + protected function add_plugin_structure($plugintype, $element) { + + global $CFG; + + // Check the requested plugintype is a valid one + if (!array_key_exists($plugintype, get_plugin_types($plugintype))) { + throw new restore_step_exception('incorrect_plugin_type', $plugintype); + } + + // Get all the restore path elements, looking across all the plugin dirs + $pluginsdirs = get_plugin_list($plugintype); + foreach ($pluginsdirs as $name => $pluginsdir) { + // We need to add also backup plugin classes on restore, they may contain + // some stuff used both in backup & restore + $backupclassname = 'backup_' . $plugintype . '_' . $name . '_plugin'; + $backupfile = $pluginsdir . '/backup/moodle2/' . $backupclassname . '.class.php'; + if (file_exists($backupfile)) { + require_once($backupfile); + } + // Now add restore plugin classes and prepare stuff + $restoreclassname = 'restore_' . $plugintype . '_' . $name . '_plugin'; + $restorefile = $pluginsdir . '/backup/moodle2/' . $restoreclassname . '.class.php'; + if (file_exists($restorefile)) { + require_once($restorefile); + $restoreplugin = new $restoreclassname($plugintype, $name, $this); + // Add plugin paths to the step + $this->prepare_pathelements($restoreplugin->define_plugin_structure($element)); + } + } + } + + /** + * Launch all the after_execute methods present in all the processing objects + * + * This method will launch all the after_execute methods that can be defined + * both in restore_plugin and restore_structure_step classes + * + * For restore_plugin classes the name of the method to be executed will be + * "after_execute_" + connection point (as far as can be multiple connection + * points in the same class) + * + * For restore_structure_step classes is will be, simply, "after_execute". Note + * that this is executed *after* the plugin ones + */ + protected function launch_after_execute_methods() { + $alreadylaunched = array(); // To avoid multiple executions + foreach ($this->pathelements as $key => $pathelement) { + // Get the processing object + $pobject = $pathelement->get_processing_object(); + // Skip null processors (child of grouped ones for sure) + if (is_null($pobject)) { + continue; + } + // Skip restore structure step processors (this) + if ($pobject instanceof restore_structure_step) { + continue; + } + // Skip already launched processing objects + if (in_array($pobject, $alreadylaunched, true)) { + continue; + } + // Add processing object to array of launched ones + $alreadylaunched[] = $pobject; + // If the processing object has support for + // launching after_execute methods, use it + if (method_exists($pobject, 'launch_after_execute_methods')) { + $pobject->launch_after_execute_methods(); + } + } + // Finally execute own (restore_structure_step) after_execute method + $this->after_execute(); + + } + /** * This method will be executed after the whole structure step have been processed * diff --git a/lang/en/backup.php b/lang/en/backup.php index 55330e0217668..b22cccd60f431 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -134,6 +134,10 @@ $string['nomatchingcourses'] = 'There are no courses to display'; $string['originalwwwroot'] = 'URL of backup'; $string['previousstage'] = 'Previous'; +$string['qcategory2coursefallback'] = 'The questions category "{$a->name}", originally at system/course category context in backup file, will be created at course context by restore'; +$string['qcategorycannotberestored'] = 'The questions category "{$a->name}" cannot be created by restore'; +$string['question2coursefallback'] = 'The questions category "{$a->name}", originally at system/course category context in backup file, will be created at course context by restore'; +$string['questionegorycannotberestored'] = 'The questions "{$a->name}" cannot be created by restore'; $string['restoreactivity'] = 'Restore activity'; $string['restorecourse'] = 'Restore course'; $string['restorecoursesettings'] = 'Course settings'; diff --git a/mod/forum/backup/moodle2/backup_forum_stepslib.php b/mod/forum/backup/moodle2/backup_forum_stepslib.php index 36bcf97aebb05..9eb48c26132e4 100644 --- a/mod/forum/backup/moodle2/backup_forum_stepslib.php +++ b/mod/forum/backup/moodle2/backup_forum_stepslib.php @@ -76,6 +76,11 @@ protected function define_structure() { 'userid', 'discussionid', 'postid', 'firstread', 'lastread')); + $trackedprefs = new backup_nested_element('trackedprefs'); + + $track = new backup_nested_element('track', array('id'), array( + 'userid')); + // Build the tree $forum->add_child($discussions); @@ -87,6 +92,9 @@ protected function define_structure() { $forum->add_child($readposts); $readposts->add_child($read); + $forum->add_child($trackedprefs); + $trackedprefs->add_child($track); + $discussion->add_child($posts); $posts->add_child($post); @@ -115,6 +123,8 @@ protected function define_structure() { $read->set_source_table('forum_read', array('forumid' => backup::VAR_PARENTID)); + $track->set_source_table('forum_track_prefs', array('forumid' => backup::VAR_PARENTID)); + $rating->set_source_table('rating', array('contextid' => backup::VAR_CONTEXTID, 'itemid' => backup::VAR_PARENTID)); $rating->set_source_alias('rating', 'value'); @@ -136,6 +146,8 @@ protected function define_structure() { $read->annotate_ids('user', 'userid'); + $track->annotate_ids('user', 'userid'); + // Define file annotations $forum->annotate_files('mod_forum', 'intro', null); // This file area hasn't itemid diff --git a/mod/forum/backup/moodle2/restore_forum_stepslib.php b/mod/forum/backup/moodle2/restore_forum_stepslib.php index 3f78f1515ae53..17e882086441a 100644 --- a/mod/forum/backup/moodle2/restore_forum_stepslib.php +++ b/mod/forum/backup/moodle2/restore_forum_stepslib.php @@ -43,6 +43,7 @@ protected function define_structure() { $paths[] = new restore_path_element('forum_rating', '/activity/forum/discussions/discussion/posts/post/ratings/rating'); $paths[] = new restore_path_element('forum_subscription', '/activity/forum/subscriptions/subscription'); $paths[] = new restore_path_element('forum_read', '/activity/forum/readposts/read'); + $paths[] = new restore_path_element('forum_track', '/activity/forum/trackedprefs/track'); } // Return the paths wrapped into standard activity structure @@ -154,6 +155,18 @@ protected function process_forum_read($data) { $newitemid = $DB->insert_record('forum_read', $data); } + protected function process_forum_track($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + $data->forumid = $this->get_new_parentid('forum'); + $data->userid = $this->get_mappingid('user', $data->userid); + + $newitemid = $DB->insert_record('forum_track_prefs', $data); + } + protected function after_execute() { global $DB; diff --git a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php index 4cc027a15e06d..7a05c5a49c185 100644 --- a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php @@ -44,7 +44,8 @@ protected function define_structure() { 'review', 'questionsperpage', 'shufflequestions', 'shuffleanswers', 'questions', 'sumgrades', 'grade', 'timecreated', 'timemodified', 'timelimit', 'password', 'subnet', - 'popup', 'delay1', 'delay2', 'showuserpicture')); + 'popup', 'delay1', 'delay2', 'showuserpicture', + 'showblocks')); $qinstances = new backup_nested_element('question_instances'); @@ -115,7 +116,6 @@ protected function define_structure() { if ($userinfo) { $grade->set_source_table('quiz_grades', array('quiz' => backup::VAR_PARENTID)); $attempt->set_source_table('quiz_attempts', array('quiz' => backup::VAR_PARENTID)); - // TODO: states and sessions go here } // Define source alias @@ -128,7 +128,6 @@ protected function define_structure() { $override->annotate_ids('group', 'groupid'); $grade->annotate_ids('user', 'userid'); $attempt->annotate_ids('user', 'userid'); - // TODO: attempts, answers... anotations go here // Define file annotations $quiz->annotate_files('mod_quiz', 'intro', null); // This file area hasn't itemid diff --git a/mod/quiz/backup/moodle2/restore_quiz_activity_task.class.php b/mod/quiz/backup/moodle2/restore_quiz_activity_task.class.php new file mode 100644 index 0000000000000..bf79c7fae189f --- /dev/null +++ b/mod/quiz/backup/moodle2/restore_quiz_activity_task.class.php @@ -0,0 +1,77 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/restore_quiz_stepslib.php'); // Because it exists (must) + +/** + * quiz restore task that provides all the settings and steps to perform one + * complete restore of the activity + */ +class restore_quiz_activity_task extends restore_activity_task { + + /** + * Define (add) particular settings this activity can have + */ + protected function define_my_settings() { + // No particular settings for this activity + } + + /** + * Define (add) particular steps this activity can have + */ + protected function define_my_steps() { + // quiz only has one structure step + $this->add_step(new restore_quiz_activity_structure_step('quiz_structure', 'quiz.xml')); + } + + /** + * Define the contents in the activity that must be + * processed by the link decoder + */ + static public function define_decode_contents() { + $contents = array(); + + $contents[] = new restore_decode_content('quiz', array('intro'), 'quiz'); + $contents[] = new restore_decode_content('quiz_feedback', array('feedbacktext'), 'quiz_feedback'); + + return $contents; + } + + /** + * Define the decoding rules for links belonging + * to the activity to be executed by the link decoder + */ + static public function define_decode_rules() { + $rules = array(); + + $rules[] = new restore_decode_rule('QUIZVIEWBYID', '/mod/quiz/view.php?id=$1', 'course_module'); + $rules[] = new restore_decode_rule('QUIZVIEWBYQ', '/mod/quiz/view.php?q=$1', 'quiz'); + $rules[] = new restore_decode_rule('QUIZINDEX', '/mod/quiz/index.php?id=$1', 'course'); + + return $rules; + + } +} diff --git a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php new file mode 100644 index 0000000000000..41e535d7757e3 --- /dev/null +++ b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php @@ -0,0 +1,176 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Define all the restore steps that will be used by the restore_quiz_activity_task + */ + +/** + * Structure step to restore one quiz activity + */ +class restore_quiz_activity_structure_step extends restore_questions_activity_structure_step { + + protected function define_structure() { + + $paths = array(); + $userinfo = $this->get_setting_value('userinfo'); + + $paths[] = new restore_path_element('quiz', '/activity/quiz'); + $paths[] = new restore_path_element('quiz_question_instance', '/activity/quiz/question_instances/question_instance'); + $paths[] = new restore_path_element('quiz_feedback', '/activity/quiz/feedbacks/feedback'); + $paths[] = new restore_path_element('quiz_override', '/activity/quiz/overrides/override'); + if ($userinfo) { + $paths[] = new restore_path_element('quiz_grade', '/activity/quiz/grades/grade'); + $quizattempt = new restore_path_element('quiz_attempt', '/activity/quiz/attempts/attempt'); + $paths[] = $quizattempt; + // Add states and sessions + $this->add_question_attempts_states($quizattempt, $paths); + $this->add_question_attempts_sessions($quizattempt, $paths); + } + + // Return the paths wrapped into standard activity structure + return $this->prepare_activity_structure($paths); + } + + protected function process_quiz($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + $data->course = $this->get_courseid(); + + $data->timeopen = $this->apply_date_offset($data->timeopen); + $data->timeclose = $this->apply_date_offset($data->timeclose); + $data->timecreated = $this->apply_date_offset($data->timecreated); + $data->timemodified = $this->apply_date_offset($data->timemodified); + + $data->questions = $this->questions_recode_layout($data->questions); + + // insert the quiz record + $newitemid = $DB->insert_record('quiz', $data); + // immediately after inserting "activity" record, call this + $this->apply_activity_instance($newitemid); + } + + protected function process_quiz_question_instance($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + $data->quiz = $this->get_new_parentid('quiz'); + + $data->question = $this->get_mappingid('question', $data->question); + + $DB->insert_record('quiz_question_instances', $data); + } + + protected function process_quiz_feedback($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + $data->quizid = $this->get_new_parentid('quiz'); + + $newitemid = $DB->insert_record('quiz_feedback', $data); + $this->set_mapping('quiz_feedback', $oldid, $newitemid, true); // Has related files + } + + protected function process_quiz_override($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + // Based on userinfo, we'll restore user overides or no + $userinfo = $this->get_setting_value('userinfo'); + + // Skip user overrides if we are not restoring userinfo + if (!$userinfo && !is_null($data->userid)) { + return; + } + + $data->quiz = $this->get_new_parentid('quiz'); + + $data->userid = $this->get_mappingid('user', $data->userid); + $data->groupid = $this->get_mappingid('group', $data->groupid); + + $data->timeopen = $this->apply_date_offset($data->timeopen); + $data->timeclose = $this->apply_date_offset($data->timeclose); + + $DB->insert_record('quiz_overrides', $data); + } + + protected function process_quiz_grade($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + $data->quiz = $this->get_new_parentid('quiz'); + + $data->userid = $this->get_mappingid('user', $data->userid); + $data->grade = $data->gradeval; + + $data->timemodified = $this->apply_date_offset($data->timemodified); + + $DB->insert_record('quiz_grades', $data); + } + + protected function process_quiz_attempt($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + $olduniqueid = $data->uniqueid; + + $data->quiz = $this->get_new_parentid('quiz'); + $data->attempt = $data->attemptnum; + + $data->uniqueid = question_new_attempt_uniqueid('quiz'); + + $data->userid = $this->get_mappingid('user', $data->userid); + + $data->timestart = $this->apply_date_offset($data->timestart); + $data->timefinish = $this->apply_date_offset($data->timefinish); + $data->timemodified = $this->apply_date_offset($data->timemodified); + + $data->layout = $this->questions_recode_layout($data->layout); + + $newitemid = $DB->insert_record('quiz_attempts', $data); + + // Save quiz_attempt->uniqueid as quiz_attempt mapping, both question_states and + // question_sessions have Fk to it and not to quiz_attempts->id at all. In fact + // quiz_attempt->id isn't use by anybody + $this->set_mapping('quiz_attempt', $olduniqueid, $data->uniqueid, false); + } + + protected function after_execute() { + // Add quiz related files, no need to match by itemname (just internally handled context) + $this->add_related_files('mod_quiz', 'intro', null); + // Add feedback related files, matching by itemname = 'quiz_feedback' + $this->add_related_files('mod_quiz', 'feedback', 'quiz_feedback'); + } +} diff --git a/mod/quiz/restorelib.php b/mod/quiz/restorelib.php index b64053e49edd7..ad24dc052be48 100644 --- a/mod/quiz/restorelib.php +++ b/mod/quiz/restorelib.php @@ -1,627 +1,4 @@ id) - // | - // ----------------------------------------------- - // | | | - // | quiz_grades | - // | (UL,pk->id,fk->quiz) | - // | | - // quiz_attempts quiz_question_instances - // (UL,pk->id,fk->quiz) (CL,pk->id,fk->quiz,question) - // - // Meaning: pk->primary key field of the table - // fk->foreign key to link with parent - // nt->nested field (recursive data) - // SL->site level info - // CL->course level info - // UL->user level info - // files->table may have files - // - //----------------------------------------------------------- - - // When we restore a quiz we also need to restore the questions and possibly - // the data about student interaction with the questions. The functions to do - // that are included with the following library - include_once("$CFG->dirroot/question/restorelib.php"); - - function quiz_restore_mods($mod,$restore) { - global $CFG, $DB; - - $status = true; - - //Hook to call Moodle < 1.5 Quiz Restore - if ($restore->backup_version < 2005043000) { - include_once("restorelibpre15.php"); - return quiz_restore_pre15_mods($mod,$restore); - } - - //Get record from backup_ids - $data = backup_getid($restore->backup_unique_code,$mod->modtype,$mod->id); - - if ($data) { - //Now get completed xmlized object - $info = $data->info; - //if necessary, write to restorelog and adjust date/time fields - if ($restore->course_startdateoffset) { - restore_log_date_changes('Quiz', $restore, $info['MOD']['#'], array('TIMEOPEN', 'TIMECLOSE')); - } - //traverse_xmlize($info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the QUIZ record structure - $quiz = new stdClass; - $quiz->course = $restore->course_id; - $quiz->name = backup_todb($info['MOD']['#']['NAME']['0']['#']); - $quiz->intro = backup_todb($info['MOD']['#']['INTRO']['0']['#']); - $quiz->timeopen = backup_todb($info['MOD']['#']['TIMEOPEN']['0']['#']); - $quiz->timeclose = backup_todb($info['MOD']['#']['TIMECLOSE']['0']['#']); - $quiz->optionflags = backup_todb($info['MOD']['#']['OPTIONFLAGS']['0']['#']); - $quiz->penaltyscheme = backup_todb($info['MOD']['#']['PENALTYSCHEME']['0']['#']); - $quiz->attempts = backup_todb($info['MOD']['#']['ATTEMPTS_NUMBER']['0']['#']); - $quiz->attemptonlast = backup_todb($info['MOD']['#']['ATTEMPTONLAST']['0']['#']); - $quiz->grademethod = backup_todb($info['MOD']['#']['GRADEMETHOD']['0']['#']); - $quiz->decimalpoints = backup_todb($info['MOD']['#']['DECIMALPOINTS']['0']['#']); - $quiz->review = backup_todb($info['MOD']['#']['REVIEW']['0']['#']); - $quiz->questionsperpage = backup_todb($info['MOD']['#']['QUESTIONSPERPAGE']['0']['#']); - $quiz->shufflequestions = backup_todb($info['MOD']['#']['SHUFFLEQUESTIONS']['0']['#']); - $quiz->shuffleanswers = backup_todb($info['MOD']['#']['SHUFFLEANSWERS']['0']['#']); - $quiz->questions = backup_todb($info['MOD']['#']['QUESTIONS']['0']['#']); - $quiz->sumgrades = backup_todb($info['MOD']['#']['SUMGRADES']['0']['#']); - $quiz->grade = backup_todb($info['MOD']['#']['GRADE']['0']['#']); - $quiz->timecreated = backup_todb($info['MOD']['#']['TIMECREATED']['0']['#']); - $quiz->timemodified = backup_todb($info['MOD']['#']['TIMEMODIFIED']['0']['#']); - if (isset($info['MOD']['#']['TIMELIMITSECS']['0']['#'])) { - $quiz->timelimit = backup_todb($info['MOD']['#']['TIMELIMITSECS']['0']['#']); - } else { - $quiz->timelimit = backup_todb($info['MOD']['#']['TIMELIMIT']['0']['#']) * 60; - } - $quiz->password = backup_todb($info['MOD']['#']['PASSWORD']['0']['#']); - $quiz->subnet = backup_todb($info['MOD']['#']['SUBNET']['0']['#']); - $quiz->popup = backup_todb($info['MOD']['#']['POPUP']['0']['#']); - $quiz->delay1 = isset($info['MOD']['#']['DELAY1']['0']['#'])?backup_todb($info['MOD']['#']['DELAY1']['0']['#']):''; - $quiz->delay2 = isset($info['MOD']['#']['DELAY2']['0']['#'])?backup_todb($info['MOD']['#']['DELAY2']['0']['#']):''; - //We have to recode the questions field (a list of questions id and pagebreaks) - $quiz->questions = quiz_recode_layout($quiz->questions, $restore); - - //The structure is equal to the db, so insert the quiz - $newid = $DB->insert_record ("quiz",$quiz); - - //Do some output - if (!defined('RESTORE_SILENTLY')) { - echo "
  • ".get_string("modulename","quiz")." \"".format_string($quiz->name,true)."\"
  • "; - } - backup_flush(300); - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,$mod->modtype, - $mod->id, $newid); - //We have to restore the question_instances now (course level table) - $status = quiz_question_instances_restore_mods($newid,$info,$restore); - //We have to restore the feedback now (course level table) - $status = quiz_feedback_restore_mods($newid, $info, $restore, $quiz); - //We have to restore the overrides now (course level table) - $status = quiz_overrides_restore_mods($newid, $info, $restore, $quiz); - //Now check if want to restore user data and do it. - if (restore_userdata_selected($restore,'quiz',$mod->id)) { - //Restore quiz_attempts - $status = quiz_attempts_restore_mods ($newid,$info,$restore); - if ($status) { - //Restore quiz_grades - $status = quiz_grades_restore_mods ($newid,$info,$restore); - } - } - } else { - $status = false; - } - } else { - $status = false; - } - - return $status; - } - - //This function restores the quiz_question_instances - function quiz_question_instances_restore_mods($quiz_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the quiz_question_instances array - if (array_key_exists('QUESTION_INSTANCES', $info['MOD']['#'])) { - $instances = $info['MOD']['#']['QUESTION_INSTANCES']['0']['#']['QUESTION_INSTANCE']; - } else { - $instances = array(); - } - - //Iterate over question_instances - for($i = 0; $i < sizeof($instances); $i++) { - $gra_info = $instances[$i]; - //traverse_xmlize($gra_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($gra_info['#']['ID']['0']['#']); - - //Now, build the QUESTION_INSTANCES record structure - $instance = new stdClass; - $instance->quiz = $quiz_id; - $instance->question = backup_todb($gra_info['#']['QUESTION']['0']['#']); - $instance->grade = backup_todb($gra_info['#']['GRADE']['0']['#']); - - //We have to recode the question field - $question = backup_getid($restore->backup_unique_code,"question",$instance->question); - if ($question) { - $instance->question = $question->new_id; - } - - //The structure is equal to the db, so insert the quiz_question_instances - $newid = $DB->insert_record ("quiz_question_instances",$instance); - - //Do some output - if (($i+1) % 10 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 200 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"quiz_question_instances",$oldid, - $newid); - } else { - $status = false; - } - } - - return $status; - } - - //This function restores the quiz_question_instances - function quiz_feedback_restore_mods($quiz_id, $info, $restore, $quiz) { - global $DB; - - $status = true; - - //Get the quiz_feedback array - if (array_key_exists('FEEDBACKS', $info['MOD']['#'])) { - $feedbacks = $info['MOD']['#']['FEEDBACKS']['0']['#']['FEEDBACK']; - - //Iterate over the feedbacks - foreach ($feedbacks as $feedback_info) { - //traverse_xmlize($feedback_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($feedback_info['#']['ID']['0']['#']); - - //Now, build the quiz_feedback record structure - $feedback = new stdClass(); - $feedback->quizid = $quiz_id; - $feedback->feedbacktext = backup_todb($feedback_info['#']['FEEDBACKTEXT']['0']['#']); - $feedback->mingrade = backup_todb($feedback_info['#']['MINGRADE']['0']['#']); - $feedback->maxgrade = backup_todb($feedback_info['#']['MAXGRADE']['0']['#']); - - //The structure is equal to the db, so insert the quiz_question_instances - $newid = $DB->insert_record('quiz_feedback', $feedback); - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code, 'quiz_feedback', $oldid, $newid); - } else { - $status = false; - } - } - } else { - $feedback = new stdClass(); - $feedback->quizid = $quiz_id; - $feedback->feedbacktext = ''; - $feedback->mingrade = 0; - $feedback->maxgrade = $quiz->grade + 1; - $DB->insert_record('quiz_feedback', $feedback); - } - - return $status; - } - - //This function restores the quiz_overrides - function quiz_overrides_restore_mods($quiz_id, $info, $restore, $quiz) { - global $DB; - - $douserdata = restore_userdata_selected($restore,'quiz',$quiz_id); - - $status = true; - - //Get the quiz_feedback array - if (array_key_exists('OVERRIDES', $info['MOD']['#'])) { - $overrides = $info['MOD']['#']['OVERRIDES']['0']['#']['OVERRIDE']; - - //Iterate over the feedbacks - foreach ($overrides as $override_info) { - //traverse_xmlize($override_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($override_info['#']['ID']['0']['#']); - - //Now, build the quiz_overrides record structure - $override = new stdClass(); - $override->quiz = $quiz_id; - $override->groupid = backup_todb($override_info['#']['GROUPID']['0']['#']); - $override->userid = backup_todb($override_info['#']['USERID']['0']['#']); - $override->timeopen = backup_todb($override_info['#']['TIMEOPEN']['0']['#']); - $override->timeclose = backup_todb($override_info['#']['TIMECLOSE']['0']['#']); - $override->timelimit = backup_todb($override_info['#']['TIMELIMIT']['0']['#']); - $override->attempts = backup_todb($override_info['#']['ATTEMPTS']['0']['#']); - $override->password = backup_todb($override_info['#']['PASSWORD']['0']['#']); - - // Only restore user overrides if we are restoring user data - if ($douserdata || $override->userid == 0) { - - //We have to recode the userid field - if ($override->userid) { - if (!$user = backup_getid($restore->backup_unique_code,"user",$override->userid)) { - debugging("override can not be restored, user id $override->userid not present in backup"); - // do not not block the restore - continue; - } - $override->userid = $user->new_id; - - } - - //We have to recode the groupid field - if ($override->groupid) { - if (!$group = backup_getid($restore->backup_unique_code,"groups",$override->groupid)) { - debugging("override can not be restored, group id $override->groupid not present in backup"); - // do not not block the restore - continue; - } - $override->groupid = $group->new_id; - } - - //The structure is equal to the db, so insert the quiz_question_instances - $newid = $DB->insert_record('quiz_overrides', $override); - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code, 'quiz_overrides', $oldid, $newid); - } else { - $status = false; - } - } - } - } - - return $status; - } - - //This function restores the quiz_attempts - function quiz_attempts_restore_mods($quiz_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the quiz_attempts array - if (array_key_exists('ATTEMPTS', $info['MOD']['#'])) { - $attempts = $info['MOD']['#']['ATTEMPTS']['0']['#']['ATTEMPT']; - } else { - $attempts = array(); - } - - //Iterate over attempts - for($i = 0; $i < sizeof($attempts); $i++) { - $att_info = $attempts[$i]; - //traverse_xmlize($att_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($att_info['#']['ID']['0']['#']); - $olduserid = backup_todb($att_info['#']['USERID']['0']['#']); - - //Now, build the ATTEMPTS record structure - $attempt = new stdClass; - $attempt->quiz = $quiz_id; - $attempt->userid = backup_todb($att_info['#']['USERID']['0']['#']); - $attempt->attempt = backup_todb($att_info['#']['ATTEMPTNUM']['0']['#']); - $attempt->sumgrades = backup_todb($att_info['#']['SUMGRADES']['0']['#']); - $attempt->timestart = backup_todb($att_info['#']['TIMESTART']['0']['#']); - $attempt->timefinish = backup_todb($att_info['#']['TIMEFINISH']['0']['#']); - $attempt->timemodified = backup_todb($att_info['#']['TIMEMODIFIED']['0']['#']); - $attempt->layout = backup_todb($att_info['#']['LAYOUT']['0']['#']); - $attempt->preview = backup_todb($att_info['#']['PREVIEW']['0']['#']); - - //We have to recode the userid field - $user = backup_getid($restore->backup_unique_code,"user",$attempt->userid); - if ($user) { - $attempt->userid = $user->new_id; - } - - //Set the uniqueid field - $attempt->uniqueid = question_new_attempt_uniqueid(); - - //We have to recode the layout field (a list of questions id and pagebreaks) - $attempt->layout = quiz_recode_layout($attempt->layout, $restore); - - //The structure is equal to the db, so insert the quiz_attempts - $newid = $DB->insert_record ("quiz_attempts",$attempt); - - //Do some output - if (($i+1) % 10 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 200 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"quiz_attempts",$oldid, - $newid); - //Now process question_states - // This function is defined in question/restorelib.php - $status = question_states_restore_mods($attempt->uniqueid,$att_info,$restore); - } else { - $status = false; - } - } - - return $status; - } - - //This function restores the quiz_grades - function quiz_grades_restore_mods($quiz_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the quiz_grades array - if (array_key_exists('GRADES', $info['MOD']['#'])) { - $grades = $info['MOD']['#']['GRADES']['0']['#']['GRADE']; - } else { - $grades = array(); - } - - //Iterate over grades - for($i = 0; $i < sizeof($grades); $i++) { - $gra_info = $grades[$i]; - //traverse_xmlize($gra_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($gra_info['#']['ID']['0']['#']); - $olduserid = backup_todb($gra_info['#']['USERID']['0']['#']); - - //Now, build the GRADES record structure - $grade = new stdClass; - $grade->quiz = $quiz_id; - $grade->userid = backup_todb($gra_info['#']['USERID']['0']['#']); - $grade->grade = backup_todb($gra_info['#']['GRADEVAL']['0']['#']); - $grade->timemodified = backup_todb($gra_info['#']['TIMEMODIFIED']['0']['#']); - - //We have to recode the userid field - $user = backup_getid($restore->backup_unique_code,"user",$grade->userid); - if ($user) { - $grade->userid = $user->new_id; - } - - //The structure is equal to the db, so insert the quiz_grades - $newid = $DB->insert_record ("quiz_grades",$grade); - - //Do some output - if (($i+1) % 10 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 200 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"quiz_grades",$oldid, - $newid); - } else { - $status = false; - } - } - - return $status; - } - - //Return a content decoded to support interactivities linking. Every module - //should have its own. They are called automatically from - //quiz_decode_content_links_caller() function in each module - //in the restore process - function quiz_decode_content_links ($content,$restore) { - global $CFG; - - $result = $content; - - //Link to the list of quizs - - $searchstring='/\$@(QUIZINDEX)\*([0-9]+)@\$/'; - //We look for it - preg_match_all($searchstring,$content,$foundset); - //If found, then we are going to look for its new id (in backup tables) - if ($foundset[0]) { - //print_object($foundset); //Debug - //Iterate over foundset[2]. They are the old_ids - foreach($foundset[2] as $old_id) { - //We get the needed variables here (course id) - $rec = backup_getid($restore->backup_unique_code,"course",$old_id); - //Personalize the searchstring - $searchstring='/\$@(QUIZINDEX)\*('.$old_id.')@\$/'; - //If it is a link to this course, update the link to its new location - if($rec->new_id) { - //Now replace it - $result= preg_replace($searchstring,$CFG->wwwroot.'/mod/quiz/index.php?id='.$rec->new_id,$result); - } else { - //It's a foreign link so leave it as original - $result= preg_replace($searchstring,$restore->original_wwwroot.'/mod/quiz/index.php?id='.$old_id,$result); - } - } - } - - //Link to quiz view by moduleid - $searchstring='/\$@(QUIZVIEWBYID)\*([0-9]+)@\$/'; - //We look for it - preg_match_all($searchstring,$result,$foundset); - //If found, then we are going to look for its new id (in backup tables) - if ($foundset[0]) { - //print_object($foundset); //Debug - //Iterate over foundset[2]. They are the old_ids - foreach($foundset[2] as $old_id) { - //We get the needed variables here (course_modules id) - $rec = backup_getid($restore->backup_unique_code,"course_modules",$old_id); - //Personalize the searchstring - $searchstring='/\$@(QUIZVIEWBYID)\*('.$old_id.')@\$/'; - //If it is a link to this course, update the link to its new location - if($rec->new_id) { - //Now replace it - $result= preg_replace($searchstring,$CFG->wwwroot.'/mod/quiz/view.php?id='.$rec->new_id,$result); - } else { - //It's a foreign link so leave it as original - $result= preg_replace($searchstring,$restore->original_wwwroot.'/mod/quiz/view.php?id='.$old_id,$result); - } - } - } - - //Link to quiz view by quizid - $searchstring='/\$@(QUIZVIEWBYQ)\*([0-9]+)@\$/'; - //We look for it - preg_match_all($searchstring,$result,$foundset); - //If found, then we are going to look for its new id (in backup tables) - if ($foundset[0]) { - //print_object($foundset); //Debug - //Iterate over foundset[2]. They are the old_ids - foreach($foundset[2] as $old_id) { - //We get the needed variables here (course_modules id) - $rec = backup_getid($restore->backup_unique_code,'quiz',$old_id); - //Personalize the searchstring - $searchstring='/\$@(QUIZVIEWBYQ)\*('.$old_id.')@\$/'; - //If it is a link to this course, update the link to its new location - if($rec->new_id) { - //Now replace it - $result= preg_replace($searchstring,$CFG->wwwroot.'/mod/quiz/view.php?q='.$rec->new_id,$result); - } else { - //It's a foreign link so leave it as original - $result= preg_replace($searchstring,$restore->original_wwwroot.'/mod/quiz/view.php?q='.$old_id,$result); - } - } - } - - return $result; - } - - //This function makes all the necessary calls to xxxx_decode_content_links() - //function in each module, passing them the desired contents to be decoded - //from backup format to destination site/course in order to mantain inter-activities - //working in the backup/restore process. It's called from restore_decode_content_links() - //function in restore process - function quiz_decode_content_links_caller($restore) { - global $CFG, $DB; - $status = true; - - if ($quizs = $DB->get_records('quiz', array('course'=>$restore->course_id), '', "id,intro")) { - //Iterate over each quiz->intro - $i = 0; //Counter to send some output to the browser to avoid timeouts - foreach ($quizs as $quiz) { - //Increment counter - $i++; - $content = $quiz->intro; - $result = restore_decode_content_links_worker($content,$restore); - if ($result != $content) { - //Update record - $quiz->intro = $result; - $DB->update_record("quiz",$quiz); - if (debugging()) { - if (!defined('RESTORE_SILENTLY')) { - echo '

    '.s($content).'
    changed to
    '.s($result).'

    '; - } - } - } - //Do some output - if (($i+1) % 5 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 100 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - } - } - - return $status; - } - - //This function converts texts in FORMAT_WIKI to FORMAT_MARKDOWN for - //some texts in the module - function quiz_restore_wiki2markdown ($restore) { - global $CFG, $DB; - - $status = true; - - //Convert question->questiontext - if ($records = $DB->get_records_sql("SELECT q.id, q.questiontext, q.questiontextformat - FROM {question} q, - {backup_ids} b - WHERE b.backup_code = ? AND - b.table_name = 'question' AND - q.id = b.new_id AND - q.questiontextformat = ".FORMAT_WIKI, array($restore->backup_unique_code))) { - $i = 0; - foreach ($records as $record) { - //Rebuild wiki links - $record->questiontext = restore_decode_wiki_content($record->questiontext, $restore); - //Convert to Markdown - $wtm = new WikiToMarkdown(); - $record->questiontext = $wtm->convert($record->questiontext, $restore->course_id); - $record->questiontextformat = FORMAT_MARKDOWN; - $DB->update_record('question', $record); - //Do some output - $i++; - if (($i+1) % 1 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 20 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - } - } - return $status; - } //This function returns a log record with all the necessary transformations //done. It's used by restore_log_module() to restore modules log. @@ -833,20 +210,3 @@ function quiz_restore_logs($restore,$log) { } return $status; } - - function quiz_recode_layout($layout, $restore) { - //Recodes the quiz layout (a list of questions id and pagebreaks) - - //Extracts question id from sequence - if ($questionids = explode(',', $layout)) { - foreach ($questionids as $id => $questionid) { - if ($questionid) { // If it is zero then this is a pagebreak, don't translate - $newq = backup_getid($restore->backup_unique_code,"question",$questionid); - $questionids[$id] = $newq->new_id; - } - } - } - return implode(',', $questionids); - } - - diff --git a/mod/quiz/restorelibpre15.php b/mod/quiz/restorelibpre15.php deleted file mode 100644 index 78061fa76755b..0000000000000 --- a/mod/quiz/restorelibpre15.php +++ /dev/null @@ -1,1907 +0,0 @@ -id) (CL,pk->id) - // | | - // ----------------------------------------------- | - // | | | |....................................... - // | | | | . - // | | | | . - // quiz_attempts quiz_grades quiz_question_grades | ----question_datasets---- . - // (UL,pk->id, fk->quiz) (UL,pk->id,fk->quiz) (CL,pk->id,fk->quiz) | | (CL,pk->id,fk->question, | . - // | | | | fk->dataset_definition) | . - // | | | | | . - // | | | | | . - // | | | | | . - // quiz_responses | question question_dataset_definitions - // (UL,pk->id, fk->attempt)----------------------------------------------------(CL,pk->id,fk->category,files) (CL,pk->id,fk->category) - // | | - // | | - // | | - // | question_dataset_items - // | (CL,pk->id,fk->definition) - // | - // | - // | - // -------------------------------------------------------------------------------------------------------------- - // | | | | | | | - // | | | | | | | - // | | | | question_calculated | | question_randomsamatch - // question_truefalse | question_multichoice | (CL,pl->id,fk->question) | |--(CL,pl->id,fk->question) - // (CL,pl->id,fk->question) | (CL,pl->id,fk->question) | . | | - // . | . | . | | - // . question_shortanswer . question_numerical . question_multianswer. | - // . (CL,pl->id,fk->question) . (CL,pl->id,fk->question) . (CL,pl->id,fk->question) | question_match - // . . . . . . |--(CL,pl->id,fk->question) - // . . . . . . | . - // . . . . . . | . - // . . . . . . | . - // . . . . . . | question_match_sub - // . . . . . . |--(CL,pl->id,fk->question) - // ........................................................................................ | - // . | - // . | - // . | question_numerical_units - // question_answers |--(CL,pl->id,fk->question) - // (CL,pk->id,fk->question)---------------------------------------------------------- - // - // Meaning: pk->primary key field of the table - // fk->foreign key to link with parent - // nt->nested field (recursive data) - // CL->course level info - // UL->user level info - // files->table may have files - // - //----------------------------------------------------------- - - //This module is special, because we make the restore in two steps: - // 1.-We restore every category and their questions (complete structure). It includes this tables: - // - question_categories - // - question - // - question_truefalse - // - question_shortanswer - // - question_multianswer - // - question_multichoice - // - question_numerical - // - question_randomsamatch - // - question_match - // - question_match_sub - // - question_calculated - // - question_answers - // - question_numerical_units - // - question_datasets - // - question_dataset_definitions - // - question_dataset_items - // All this backup info have its own section in moodle.xml (QUESTION_CATEGORIES) and it's generated - // before every module backup standard invocation. And only if to restore quizzes has been selected !! - // It's invoked with quiz_restore_question_categories. (course independent). - - // 2.-Standard module restore (Invoked via quiz_restore_mods). It includes this tables: - // - quiz - // - quiz_question_grades - // - quiz_attempts - // - quiz_grades - // - quiz_responses - // This step is the standard mod backup. (course dependent). - - //We are going to nedd quiz libs to be able to mimic the upgrade process - require_once("$CFG->dirroot/mod/quiz/locallib.php"); - - //STEP 1. Restore categories/questions and associated structures - // (course independent) - function quiz_restore_pre15_question_categories($category,$restore) { - global $CFG, $DB; - - $status = true; - - //Get record from backup_ids - $data = backup_getid($restore->backup_unique_code,"question_categories",$category->id); - - if ($data) { - //Now get completed xmlized object - $info = $data->info; - //traverse_xmlize($info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_categories record structure - $quiz_cat->course = $restore->course_id; - $quiz_cat->name = backup_todb($info['QUESTION_CATEGORY']['#']['NAME']['0']['#']); - $quiz_cat->info = backup_todb($info['QUESTION_CATEGORY']['#']['INFO']['0']['#']); - $quiz_cat->publish = backup_todb($info['QUESTION_CATEGORY']['#']['PUBLISH']['0']['#']); - $quiz_cat->stamp = backup_todb($info['QUESTION_CATEGORY']['#']['STAMP']['0']['#']); - $quiz_cat->parent = backup_todb($info['QUESTION_CATEGORY']['#']['PARENT']['0']['#']); - $quiz_cat->sortorder = backup_todb($info['QUESTION_CATEGORY']['#']['SORTORDER']['0']['#']); - - if ($catfound = restore_get_best_question_category($quiz_cat, $restore->course_id)) { - $newid = $catfound; - } else { - if (!$quiz_cat->stamp) { - $quiz_cat->stamp = make_unique_id_code(); - } - $newid = $DB->insert_record ("question_categories",$quiz_cat); - } - - //Do some output - if ($newid) { - if (!defined('RESTORE_SILENTLY')) { - echo "
  • ".get_string('category', 'quiz')." \"".$quiz_cat->name."\"
    "; - } - } else { - if (!defined('RESTORE_SILENTLY')) { - //We must never arrive here !! - echo "
  • ".get_string('category', 'quiz')." \"".$quiz_cat->name."\" Error!
    "; - } - $status = false; - } - backup_flush(300); - - //Here category has been created or selected, so save results in backup_ids and start with questions - if ($newid and $status) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"question_categories", - $category->id, $newid); - //Now restore question - $status = quiz_restore_pre15_questions ($category->id, $newid,$info,$restore); - } else { - $status = false; - } - if (!defined('RESTORE_SILENTLY')) { - echo '
  • '; - } - } - - return $status; - } - - function quiz_restore_pre15_questions ($old_category_id,$new_category_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the questions array - $questions = $info['QUESTION_CATEGORY']['#']['QUESTIONS']['0']['#']['QUESTION']; - - //Iterate over questions - for($i = 0; $i < sizeof($questions); $i++) { - $question = new stdClass(); - $que_info = $questions[$i]; - //traverse_xmlize($que_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($que_info['#']['ID']['0']['#']); - - //Now, build the question record structure - $question->category = $new_category_id; - $question->parent = backup_todb($que_info['#']['PARENT']['0']['#']); - $question->name = backup_todb($que_info['#']['NAME']['0']['#']); - $question->questiontext = backup_todb($que_info['#']['QUESTIONTEXT']['0']['#']); - $question->questiontextformat = backup_todb($que_info['#']['QUESTIONTEXTFORMAT']['0']['#']); - $question->image = backup_todb($que_info['#']['IMAGE']['0']['#']); - $question->defaultgrade = backup_todb($que_info['#']['DEFAULTGRADE']['0']['#']); - if (isset($que_info['#']['PENALTY']['0']['#'])) { //Only if it's set, to apply DB default else. - $question->penalty = backup_todb($que_info['#']['PENALTY']['0']['#']); - } - $question->qtype = backup_todb($que_info['#']['QTYPE']['0']['#']); - if (isset($que_info['#']['LENGTH']['0']['#'])) { //Only if it's set, to apply DB default else. - $question->length = backup_todb($que_info['#']['LENGTH']['0']['#']); - } - $question->stamp = backup_todb($que_info['#']['STAMP']['0']['#']); - if (isset($que_info['#']['VERSION']['0']['#'])) { //Only if it's set, to apply DB default else. - $question->version = backup_todb($que_info['#']['VERSION']['0']['#']); - } - if (isset($que_info['#']['HIDDEN']['0']['#'])) { //Only if it's set, to apply DB default else. - $question->hidden = backup_todb($que_info['#']['HIDDEN']['0']['#']); - } - - //Although only a few backups can have questions with parent, we try to recode it - //if it contains something - if ($question->parent and $parent = backup_getid($restore->backup_unique_code,"question",$question->parent)) { - $question->parent = $parent->new_id; - } - - // If it is a random question then hide it - if ($question->qtype == RANDOM) { - $question->hidden = 1; - } - - //If it is a description question, length = 0 - if ($question->qtype == DESCRIPTION) { - $question->length = 0; - } - - //Check if the question exists - //by category and stamp - $question_exists = $DB->get_record ("question", array("category"=>$question->category, - "stamp"=>$question->stamp)); - //If the stamp doesn't exists, check if question exists - //by category, name and questiontext and calculate stamp - //Mantains pre Beta 1.1 compatibility !! - if (!$question->stamp) { - $question->stamp = make_unique_id_code(); - $question->version = 1; - $question_exists = $DB->get_record ("question", array("category"=>$question->category, - "name"=>$question->name, - "questiontext"=>$question->questiontext)); - } - - //If the question exists, only record its id - if ($question_exists) { - $newid = $question_exists->id; - $creatingnewquestion = false; - //Else, create a new question - } else { - //The structure is equal to the db, so insert the question - $newid = $DB->insert_record ("question",$question); - //If it is a random question, parent = id - if ($newid && $question->qtype == RANDOM) { - $DB->set_field ('question', 'parent', $newid, array('id'=>$newid)); - } - $creatingnewquestion = true; - } - - //Do some output - if (($i+1) % 2 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 40 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - //Save newid to backup tables - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"question",$oldid, - $newid); - } - //If it's a new question in the DB, restore it - if ($creatingnewquestion) { - //Now, restore every question_answers in this question - $status = quiz_restore_pre15_answers($oldid,$newid,$que_info,$restore); - //Now, depending of the type of questions, invoke different functions - if ($question->qtype == "1") { - $status = quiz_restore_pre15_shortanswer($oldid,$newid,$que_info,$restore); - } else if ($question->qtype == "2") { - $status = quiz_restore_pre15_truefalse($oldid,$newid,$que_info,$restore); - } else if ($question->qtype == "3") { - $status = quiz_restore_pre15_multichoice($oldid,$newid,$que_info,$restore); - } else if ($question->qtype == "4") { - //Random question. Nothing to do. - } else if ($question->qtype == "5") { - $status = quiz_restore_pre15_match($oldid,$newid,$que_info,$restore); - } else if ($question->qtype == "6") { - $status = quiz_restore_pre15_randomsamatch($oldid,$newid,$que_info,$restore); - } else if ($question->qtype == "7") { - //Description question. Nothing to do. - } else if ($question->qtype == "8") { - $status = quiz_restore_pre15_numerical($oldid,$newid,$que_info,$restore); - } else if ($question->qtype == "9") { - $status = quiz_restore_pre15_multianswer($oldid,$newid,$que_info,$restore); - } else if ($question->qtype == "10") { - $status = quiz_restore_pre15_calculated($oldid,$newid,$que_info,$restore); - } - } else { - //We are NOT creating the question, but we need to know every question_answers - //map between the XML file and the database to be able to restore the responses - //in each attempt. - $status = quiz_restore_pre15_map_answers($oldid,$newid,$que_info,$restore); - //Now, depending of the type of questions, invoke different functions - //to create the necessary mappings in backup_ids, because we are not - //creating the question, but need some records in backup table - if ($question->qtype == "1") { - //Shortanswer question. Nothing to remap - } else if ($question->qtype == "2") { - //Truefalse question. Nothing to remap - } else if ($question->qtype == "3") { - //Multichoice question. Nothing to remap - } else if ($question->qtype == "4") { - //Random question. Nothing to remap - } else if ($question->qtype == "5") { - $status = quiz_restore_pre15_map_match($oldid,$newid,$que_info,$restore); - } else if ($question->qtype == "6") { - //Randomsamatch question. Nothing to remap - } else if ($question->qtype == "7") { - //Description question. Nothing to remap - } else if ($question->qtype == "8") { - //Numerical question. Nothing to remap - } else if ($question->qtype == "9") { - //Multianswer question. Nothing to remap - } else if ($question->qtype == "10") { - //Calculated question. Nothing to remap - } - } - } - return $status; - } - - function quiz_restore_pre15_answers ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the answers array - if (isset($info['#']['ANSWERS']['0']['#']['ANSWER'])) { - $answers = $info['#']['ANSWERS']['0']['#']['ANSWER']; - - //Iterate over answers - for($i = 0; $i < sizeof($answers); $i++) { - $ans_info = $answers[$i]; - //traverse_xmlize($ans_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($ans_info['#']['ID']['0']['#']); - - //Now, build the question_answers record structure - $answer->question = $new_question_id; - $answer->answer = backup_todb($ans_info['#']['ANSWER_TEXT']['0']['#']); - $answer->fraction = backup_todb($ans_info['#']['FRACTION']['0']['#']); - $answer->feedback = backup_todb($ans_info['#']['FEEDBACK']['0']['#']); - - //The structure is equal to the db, so insert the question_answers - $newid = $DB->insert_record ("question_answers",$answer); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"question_answers",$oldid, - $newid); - } else { - $status = false; - } - } - } - - return $status; - } - - function quiz_restore_pre15_map_answers ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - if (!isset($info['#']['ANSWERS'])) { // No answers in this question (eg random) - return $status; - } - - //Get the answers array - $answers = $info['#']['ANSWERS']['0']['#']['ANSWER']; - - //Iterate over answers - for($i = 0; $i < sizeof($answers); $i++) { - $ans_info = $answers[$i]; - //traverse_xmlize($ans_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($ans_info['#']['ID']['0']['#']); - - //Now, build the question_answers record structure - $answer->question = $new_question_id; - $answer->answer = backup_todb($ans_info['#']['ANSWER_TEXT']['0']['#']); - $answer->fraction = backup_todb($ans_info['#']['FRACTION']['0']['#']); - $answer->feedback = backup_todb($ans_info['#']['FEEDBACK']['0']['#']); - - //If we are in this method is because the question exists in DB, so its - //answers must exist too. - //Now, we are going to look for that answer in DB and to create the - //mappings in backup_ids to use them later where restoring responses (user level). - - //Get the answer from DB (by question, answer and fraction) - $db_answer = $DB->get_record ("question_answers", array("question"=>$new_question_id, - "answer"=>$answer->answer, - "fraction"=>$answer->fraction)); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($db_answer) { - //We have the database answer, update backup_ids - backup_putid($restore->backup_unique_code,"question_answers",$oldid, - $db_answer->id); - } else { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_shortanswer ($old_question_id,$new_question_id,$info,$restore,$restrictto = '') { - global $CFG, $DB; - - $status = true; - - //Get the shortanswers array - $shortanswers = $info['#']['SHORTANSWER']; - - //Iterate over shortanswers - for($i = 0; $i < sizeof($shortanswers); $i++) { - $sho_info = $shortanswers[$i]; - //traverse_xmlize($sho_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_shortanswer record structure - $shortanswer->question = $new_question_id; - $shortanswer->answers = backup_todb($sho_info['#']['ANSWERS']['0']['#']); - $shortanswer->usecase = backup_todb($sho_info['#']['USECASE']['0']['#']); - - //We have to recode the answers field (a list of answers id) - //Extracts answer id from sequence - $answers_field = ""; - $in_first = true; - $tok = strtok($shortanswer->answers,","); - while ($tok) { - //Get the answer from backup_ids - $answer = backup_getid($restore->backup_unique_code,"question_answers",$tok); - if ($answer) { - if ($in_first) { - $answers_field .= $answer->new_id; - $in_first = false; - } else { - $answers_field .= ",".$answer->new_id; - } - } - //check for next - $tok = strtok(","); - } - //We have the answers field recoded to its new ids - $shortanswer->answers = $answers_field; - - //The structure is equal to the db, so insert the question_shortanswer - //Only if there aren't restrictions or there are restriction concordance - if (empty($restrictto) || (!empty($restrictto) && $shortanswer->answers == $restrictto)) { - $newid = $DB->insert_record ("question_shortanswer",$shortanswer); - } - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if (!$newid && !$restrictto) { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_truefalse ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the truefalse array - $truefalses = $info['#']['TRUEFALSE']; - - //Iterate over truefalse - for($i = 0; $i < sizeof($truefalses); $i++) { - $tru_info = $truefalses[$i]; - //traverse_xmlize($tru_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_truefalse record structure - $truefalse->question = $new_question_id; - $truefalse->trueanswer = backup_todb($tru_info['#']['TRUEANSWER']['0']['#']); - $truefalse->falseanswer = backup_todb($tru_info['#']['FALSEANSWER']['0']['#']); - - ////We have to recode the trueanswer field - $answer = backup_getid($restore->backup_unique_code,"question_answers",$truefalse->trueanswer); - if ($answer) { - $truefalse->trueanswer = $answer->new_id; - } - - ////We have to recode the falseanswer field - $answer = backup_getid($restore->backup_unique_code,"question_answers",$truefalse->falseanswer); - if ($answer) { - $truefalse->falseanswer = $answer->new_id; - } - - //The structure is equal to the db, so insert the question_truefalse - $newid = $DB->insert_record ("question_truefalse",$truefalse); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_multichoice ($old_question_id,$new_question_id,$info,$restore, $restrictto = '') { - global $CFG, $DB; - - $status = true; - - //Get the multichoices array - $multichoices = $info['#']['MULTICHOICE']; - - //Iterate over multichoices - for($i = 0; $i < sizeof($multichoices); $i++) { - $mul_info = $multichoices[$i]; - //traverse_xmlize($mul_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_multichoice record structure - $multichoice->question = $new_question_id; - $multichoice->layout = backup_todb($mul_info['#']['LAYOUT']['0']['#']); - $multichoice->answers = backup_todb($mul_info['#']['ANSWERS']['0']['#']); - $multichoice->single = backup_todb($mul_info['#']['SINGLE']['0']['#']); - - //We have to recode the answers field (a list of answers id) - //Extracts answer id from sequence - $answers_field = ""; - $in_first = true; - $tok = strtok($multichoice->answers,","); - while ($tok) { - //Get the answer from backup_ids - $answer = backup_getid($restore->backup_unique_code,"question_answers",$tok); - if ($answer) { - if ($in_first) { - $answers_field .= $answer->new_id; - $in_first = false; - } else { - $answers_field .= ",".$answer->new_id; - } - } - //check for next - $tok = strtok(","); - } - //We have the answers field recoded to its new ids - $multichoice->answers = $answers_field; - - //The structure is equal to the db, so insert the question_shortanswer - //Only if there aren't restrictions or there are restriction concordance - if (empty($restrictto) || (!empty($restrictto) && $multichoice->answers == $restrictto)) { - $newid = $DB->insert_record ("question_multichoice",$multichoice); - } - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if (!$newid && !$restrictto) { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_match ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the matchs array - $matchs = $info['#']['MATCHS']['0']['#']['MATCH']; - - //We have to build the subquestions field (a list of match_sub id) - $subquestions_field = ""; - $in_first = true; - - //Iterate over matchs - for($i = 0; $i < sizeof($matchs); $i++) { - $mat_info = $matchs[$i]; - //traverse_xmlize($mat_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($mat_info['#']['ID']['0']['#']); - - //Now, build the question_match_SUB record structure - $match_sub->question = $new_question_id; - $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']); - $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']); - - //The structure is equal to the db, so insert the question_match_sub - $newid = $DB->insert_record ("question_match_sub",$match_sub); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"question_match_sub",$oldid, - $newid); - //We have a new match_sub, append it to subquestions_field - if ($in_first) { - $subquestions_field .= $newid; - $in_first = false; - } else { - $subquestions_field .= ",".$newid; - } - } else { - $status = false; - } - } - - //We have created every match_sub, now create the match - $match->question = $new_question_id; - $match->subquestions = $subquestions_field; - - //The structure is equal to the db, so insert the question_match_sub - $newid = $DB->insert_record ("question_match",$match); - - if (!$newid) { - $status = false; - } - - return $status; - } - - function quiz_restore_pre15_map_match ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the matchs array - $matchs = $info['#']['MATCHS']['0']['#']['MATCH']; - - //We have to build the subquestions field (a list of match_sub id) - $subquestions_field = ""; - $in_first = true; - - //Iterate over matchs - for($i = 0; $i < sizeof($matchs); $i++) { - $mat_info = $matchs[$i]; - //traverse_xmlize($mat_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($mat_info['#']['ID']['0']['#']); - - //Now, build the question_match_SUB record structure - $match_sub->question = $new_question_id; - $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']); - $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']); - - //If we are in this method is because the question exists in DB, so its - //match_sub must exist too. - //Now, we are going to look for that match_sub in DB and to create the - //mappings in backup_ids to use them later where restoring responses (user level). - - //Get the match_sub from DB (by question, questiontext and answertext) - $db_match_sub = $DB->get_record ("question_match_sub", array("question"=>$new_question_id, - "questiontext"=>$match_sub->questiontext, - "answertext"=>$match_sub->answertext)); - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - //We have the database match_sub, so update backup_ids - if ($db_match_sub) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"question_match_sub",$oldid, - $db_match_sub->id); - } else { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_randomsamatch ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the randomsamatchs array - $randomsamatchs = $info['#']['RANDOMSAMATCH']; - - //Iterate over randomsamatchs - for($i = 0; $i < sizeof($randomsamatchs); $i++) { - $ran_info = $randomsamatchs[$i]; - //traverse_xmlize($ran_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_randomsamatch record structure - $randomsamatch->question = $new_question_id; - $randomsamatch->choose = backup_todb($ran_info['#']['CHOOSE']['0']['#']); - - //The structure is equal to the db, so insert the question_randomsamatch - $newid = $DB->insert_record ("question_randomsamatch",$randomsamatch); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_numerical ($old_question_id,$new_question_id,$info,$restore, $restrictto = '') { - global $CFG, $DB; - - $status = true; - - //Get the numerical array - $numericals = $info['#']['NUMERICAL']; - - //Iterate over numericals - for($i = 0; $i < sizeof($numericals); $i++) { - $num_info = $numericals[$i]; - //traverse_xmlize($num_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_numerical record structure - $numerical->question = $new_question_id; - $numerical->answer = backup_todb($num_info['#']['ANSWER']['0']['#']); - $numerical->min = backup_todb($num_info['#']['MIN']['0']['#']); - $numerical->max = backup_todb($num_info['#']['MAX']['0']['#']); - - ////We have to recode the answer field - $answer = backup_getid($restore->backup_unique_code,"question_answers",$numerical->answer); - if ($answer) { - $numerical->answer = $answer->new_id; - } - - //Answer goes to answers in 1.5 (although it continues being only one!) - //Changed 12-05 (chating with Gustav and Julian this remains = pre15 = answer) - //$numerical->answers = $numerical->answer; - - //We have to calculate the tolerance field of the numerical question - $numerical->tolerance = ($numerical->max - $numerical->min)/2; - - //The structure is equal to the db, so insert the question_numerical - //Only if there aren't restrictions or there are restriction concordance - if (empty($restrictto) || (!empty($restrictto) && in_array($numerical->answer,explode(",",$restrictto)))) { - $newid = $DB->insert_record ("question_numerical",$numerical); - } - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - //Now restore numerical_units - if ($newid) { - $status = quiz_restore_pre15_numerical_units ($old_question_id,$new_question_id,$num_info,$restore); - } - - if (!$newid && !$restrictto) { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_calculated ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the calculated-s array - $calculateds = $info['#']['CALCULATED']; - - //Iterate over calculateds - for($i = 0; $i < sizeof($calculateds); $i++) { - $cal_info = $calculateds[$i]; - //traverse_xmlize($cal_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_calculated record structure - $calculated->question = $new_question_id; - $calculated->answer = backup_todb($cal_info['#']['ANSWER']['0']['#']); - $calculated->tolerance = backup_todb($cal_info['#']['TOLERANCE']['0']['#']); - $calculated->tolerancetype = backup_todb($cal_info['#']['TOLERANCETYPE']['0']['#']); - $calculated->correctanswerlength = backup_todb($cal_info['#']['CORRECTANSWERLENGTH']['0']['#']); - $calculated->correctanswerformat = backup_todb($cal_info['#']['CORRECTANSWERFORMAT']['0']['#']); - - ////We have to recode the answer field - $answer = backup_getid($restore->backup_unique_code,"question_answers",$calculated->answer); - if ($answer) { - $calculated->answer = $answer->new_id; - } - - //If we haven't correctanswerformat, it defaults to 2 (in DB) - if (empty($calculated->correctanswerformat)) { - $calculated->correctanswerformat = 2; - } - - //The structure is equal to the db, so insert the question_calculated - $newid = $DB->insert_record ("question_calculated",$calculated); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - //Now restore numerical_units - $status = quiz_restore_pre15_numerical_units ($old_question_id,$new_question_id,$cal_info,$restore); - - //Now restore dataset_definitions - if ($status && $newid) { - $status = quiz_restore_pre15_dataset_definitions ($old_question_id,$new_question_id,$cal_info,$restore); - } - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_multianswer ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //We need some question fields here so we get the full record from DB - $parentquestion = $DB->get_record('question', array('id'=>$new_question_id)); - - //We need to store all the positions with their created questions - //to be able to calculate the sequence field - $createdquestions = array(); - - //Under 1.5, every multianswer record becomes a question itself - //with its parent set to the cloze question. And there is only - //ONE multianswer record with the sequence of questions used. - - //Get the multianswers array - $multianswers_array = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER']; - //Iterate over multianswers_array - for($i = 0; $i < sizeof($multianswers_array); $i++) { - $mul_info = $multianswers_array[$i]; - //traverse_xmlize($mul_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We need this later - $oldid = backup_todb($mul_info['#']['ID']['0']['#']); - - //Now, build the question_multianswer record structure - $multianswer->question = $new_question_id; - $multianswer->answers = backup_todb($mul_info['#']['ANSWERS']['0']['#']); - $multianswer->positionkey = backup_todb($mul_info['#']['POSITIONKEY']['0']['#']); - $multianswer->answertype = backup_todb($mul_info['#']['ANSWERTYPE']['0']['#']); - $multianswer->norm = backup_todb($mul_info['#']['NORM']['0']['#']); - - //Saving multianswer and positionkey to use them later restoring states - backup_putid ($restore->backup_unique_code,'multianswer-pos',$oldid,$multianswer->positionkey); - - //We have to recode all the answers to their new ids - $ansarr = explode(",", $multianswer->answers); - foreach ($ansarr as $key => $value) { - //Get the answer from backup_ids - $answer = backup_getid($restore->backup_unique_code,'question_answers',$value); - $ansarr[$key] = $answer->new_id; - } - $multianswer->answers = implode(",",$ansarr); - - //Build the new question structure - $question = new stdClass(); - $question->category = $parentquestion->category; - $question->parent = $parentquestion->id; - $question->name = $parentquestion->name; - $question->questiontextformat = $parentquestion->questiontextformat; - $question->defaultgrade = $multianswer->norm; - $question->penalty = $parentquestion->penalty; - $question->qtype = $multianswer->answertype; - $question->version = $parentquestion->version; - $question->hidden = $parentquestion->hidden; - $question->length = 0; - $question->questiontext = ''; - $question->stamp = make_unique_id_code(); - - //Save the new question to DB - $newid = $DB->insert_record('question', $question); - - if ($newid) { - $createdquestions[$multianswer->positionkey] = $newid; - } - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - //Remap question_answers records from the original multianswer question - //to their newly created question - if ($newid) { - $answersdb = $DB->get_records_list('question_answers','id', explode(',',$multianswer->answers)); - foreach ($answersdb as $answerdb) { - $DB->set_field('question_answers','question',$newid,array('id' =>$answerdb->id)); - } - } - - //If we have created the question record, now, depending of the - //answertype, delegate the restore to every qtype function - if ($newid) { - if ($multianswer->answertype == "1") { - $status = quiz_restore_pre15_shortanswer ($old_question_id,$newid,$mul_info,$restore,$multianswer->answers); - } else if ($multianswer->answertype == "3") { - $status = quiz_restore_pre15_multichoice ($old_question_id,$newid,$mul_info,$restore,$multianswer->answers); - } else if ($multianswer->answertype == "8") { - $status = quiz_restore_pre15_numerical ($old_question_id,$newid,$mul_info,$restore,$multianswer->answers); - } - } else { - $status = false; - } - } - - //Everything is created, just going to create the multianswer record - if ($status) { - ksort($createdquestions); - - $multianswerdb = new stdClass(); - $multianswerdb->question = $parentquestion->id; - $multianswerdb->sequence = implode(",",$createdquestions); - $mid = $DB->insert_record('question_multianswer', $multianswerdb); - - if (!$mid) { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_numerical_units ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the numerical array - $numerical_units = $info['#']['NUMERICAL_UNITS']['0']['#']['NUMERICAL_UNIT']; - - //Iterate over numerical_units - for($i = 0; $i < sizeof($numerical_units); $i++) { - $nu_info = $numerical_units[$i]; - //traverse_xmlize($nu_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_numerical_UNITS record structure - $numerical_unit->question = $new_question_id; - $numerical_unit->multiplier = backup_todb($nu_info['#']['MULTIPLIER']['0']['#']); - $numerical_unit->unit = backup_todb($nu_info['#']['UNIT']['0']['#']); - - //The structure is equal to the db, so insert the question_numerical_units - $newid = $DB->insert_record ("question_numerical_units",$numerical_unit); - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_dataset_definitions ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the dataset_definitions array - $dataset_definitions = $info['#']['DATASET_DEFINITIONS']['0']['#']['DATASET_DEFINITION']; - - //Iterate over dataset_definitions - for($i = 0; $i < sizeof($dataset_definitions); $i++) { - $dd_info = $dataset_definitions[$i]; - //traverse_xmlize($dd_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_dataset_DEFINITION record structure - $dataset_definition->category = backup_todb($dd_info['#']['CATEGORY']['0']['#']); - $dataset_definition->name = backup_todb($dd_info['#']['NAME']['0']['#']); - $dataset_definition->type = backup_todb($dd_info['#']['TYPE']['0']['#']); - $dataset_definition->options = backup_todb($dd_info['#']['OPTIONS']['0']['#']); - $dataset_definition->itemcount = backup_todb($dd_info['#']['ITEMCOUNT']['0']['#']); - - //We have to recode the category field (only if the category != 0) - if ($dataset_definition->category != 0) { - $category = backup_getid($restore->backup_unique_code,"question_categories",$dataset_definition->category); - if ($category) { - $dataset_definition->category = $category->new_id; - } - } - - //Now, we hace to decide when to create the new records or reuse an existing one - $create_definition = false; - - //If the dataset_definition->category = 0, it's a individual question dataset_definition, so we'll create it - if ($dataset_definition->category == 0) { - $create_definition = true; - } else { - //The category isn't 0, so it's a category question dataset_definition, we have to see if it exists - //Look for a definition with the same category, name and type - if ($definitionrec = $DB->get_records('question_dataset_definitions', array('category'=>$dataset_definition->category, - 'name'=>$dataset_definition->name, - 'type'=>$dataset_definition->type))) { - //Such dataset_definition exist. Now we must check if it has enough itemcount - if ($definitionrec->itemcount < $dataset_definition->itemcount) { - //We haven't enough itemcount, so we have to create the definition as an individual question one. - $dataset_definition->category = 0; - $create_definition = true; - } else { - //We have enough itemcount, so we'll reuse the existing definition - $create_definition = false; - $newid = $definitionrec->id; - } - } else { - //Such dataset_definition doesn't exist. We'll create it. - $create_definition = true; - } - } - - //If we've to create the definition, do it - if ($create_definition) { - //The structure is equal to the db, so insert the question_dataset_definitions - $newid = $DB->insert_record ("question_dataset_definitions",$dataset_definition); - if ($newid) { - //Restore question_dataset_items - $status = quiz_restore_pre15_dataset_items($newid,$dd_info,$restore); - } - } - - //Now, we must have a definition (created o reused). Its id is in newid. Create the question_datasets record - //to join the question and the dataset_definition - if ($newid) { - $question_dataset->question = $new_question_id; - $question_dataset->datasetdefinition = $newid; - $newid = $DB->insert_record ("question_datasets",$question_dataset); - } - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - function quiz_restore_pre15_dataset_items ($definitionid,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the items array - $dataset_items = $info['#']['DATASET_ITEMS']['0']['#']['DATASET_ITEM']; - - //Iterate over dataset_items - for($i = 0; $i < sizeof($dataset_items); $i++) { - $di_info = $dataset_items[$i]; - //traverse_xmlize($di_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_dataset_ITEMS record structure - $dataset_item->definition = $definitionid; - $dataset_item->itemnumber = backup_todb($di_info['#']['NUMBER']['0']['#']); - $dataset_item->value = backup_todb($di_info['#']['VALUE']['0']['#']); - - //The structure is equal to the db, so insert the question_dataset_items - $newid = $DB->insert_record ("question_dataset_items",$dataset_item); - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - //STEP 2. Restore quizzes and associated structures - // (course dependent) - function quiz_restore_pre15_mods($mod,$restore) { - global $CFG, $DB; - - $status = true; - - //Get record from backup_ids - $data = backup_getid($restore->backup_unique_code,$mod->modtype,$mod->id); - - if ($data) { - //Now get completed xmlized object - $info = $data->info; - //if necessary, write to restorelog and adjust date/time fields - if ($restore->course_startdateoffset) { - restore_log_date_changes('Quiz', $restore, $info['MOD']['#'], array('TIMEOPEN', 'TIMECLOSE')); - } - //traverse_xmlize($info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the QUIZ record structure - $quiz->course = $restore->course_id; - $quiz->name = backup_todb($info['MOD']['#']['NAME']['0']['#']); - $quiz->intro = backup_todb($info['MOD']['#']['INTRO']['0']['#']); - $quiz->timeopen = backup_todb($info['MOD']['#']['TIMEOPEN']['0']['#']); - $quiz->timeclose = backup_todb($info['MOD']['#']['TIMECLOSE']['0']['#']); - $quiz->attempts = backup_todb($info['MOD']['#']['ATTEMPTS_NUMBER']['0']['#']); - $quiz->attemptonlast = backup_todb($info['MOD']['#']['ATTEMPTONLAST']['0']['#']); - $quiz->feedback = backup_todb($info['MOD']['#']['FEEDBACK']['0']['#']); - $quiz->correctanswers = backup_todb($info['MOD']['#']['CORRECTANSWERS']['0']['#']); - $quiz->grademethod = backup_todb($info['MOD']['#']['GRADEMETHOD']['0']['#']); - if (isset($info['MOD']['#']['DECIMALPOINTS']['0']['#'])) { //Only if it's set, to apply DB default else. - $quiz->decimalpoints = backup_todb($info['MOD']['#']['DECIMALPOINTS']['0']['#']); - } - $quiz->review = backup_todb($info['MOD']['#']['REVIEW']['0']['#']); - $quiz->questionsperpage = backup_todb($info['MOD']['#']['QUESTIONSPERPAGE']['0']['#']); - $quiz->shufflequestions = backup_todb($info['MOD']['#']['SHUFFLEQUESTIONS']['0']['#']); - $quiz->shuffleanswers = backup_todb($info['MOD']['#']['SHUFFLEANSWERS']['0']['#']); - $quiz->questions = backup_todb($info['MOD']['#']['QUESTIONS']['0']['#']); - $quiz->sumgrades = backup_todb($info['MOD']['#']['SUMGRADES']['0']['#']); - $quiz->grade = backup_todb($info['MOD']['#']['GRADE']['0']['#']); - $quiz->timecreated = backup_todb($info['MOD']['#']['TIMECREATED']['0']['#']); - $quiz->timemodified = backup_todb($info['MOD']['#']['TIMEMODIFIED']['0']['#']); - $quiz->timelimit = backup_todb($info['MOD']['#']['TIMELIMIT']['0']['#']) * 60; - $quiz->password = backup_todb($info['MOD']['#']['PASSWORD']['0']['#']); - $quiz->subnet = backup_todb($info['MOD']['#']['SUBNET']['0']['#']); - $quiz->popup = backup_todb($info['MOD']['#']['POPUP']['0']['#']); - - //We have to recode the questions field (a list of questions id) - $newquestions = array(); - if ($questionsarr = explode (",",$quiz->questions)) { - foreach ($questionsarr as $key => $value) { - if ($question = backup_getid($restore->backup_unique_code,"question",$value)) { - $newquestions[] = $question->new_id; - } - } - } - $quiz->questions = implode (",", $newquestions); - - //Recalculate the questions field to include page breaks if necessary - $quiz->questions = quiz_repaginate($quiz->questions, $quiz->questionsperpage); - - //Calculate the new review field contents (logic extracted from upgrade) - $review = (QUIZ_REVIEW_IMMEDIATELY & (QUIZ_REVIEW_RESPONSES + QUIZ_REVIEW_SCORES)); - if ($quiz->feedback) { - $review += (QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_FEEDBACK); - } - if ($quiz->correctanswers) { - $review += (QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_ANSWERS); - } - if ($quiz->review & 1) { - $review += QUIZ_REVIEW_CLOSED; - } - if ($quiz->review & 2) { - $review += QUIZ_REVIEW_OPEN; - } - $quiz->review = $review; - - //The structure is equal to the db, so insert the quiz - $newid = $DB->insert_record ("quiz",$quiz); - - //Do some output - if (!defined('RESTORE_SILENTLY')) { - echo "
  • ".get_string("modulename","quiz")." \"".format_string($quiz->name,true)."\"
  • "; - } - backup_flush(300); - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,$mod->modtype, - $mod->id, $newid); - //We have to restore the quiz_question_instances now (old quiz_question_grades, course level) - $status = quiz_question_instances_restore_pre15_mods($newid,$info,$restore); - //Now check if want to restore user data and do it. - if (restore_userdata_selected($restore,'quiz',$mod->id)) { - //Restore quiz_attempts - $status = quiz_attempts_restore_pre15_mods ($newid,$info,$restore, $quiz->questions); - if ($status) { - //Restore quiz_grades - $status = quiz_grades_restore_pre15_mods ($newid,$info,$restore); - } - } - } else { - $status = false; - } - } else { - $status = false; - } - - return $status; - } - - //This function restores the quiz_question_instances (old quiz_question_grades) - function quiz_question_instances_restore_pre15_mods($quiz_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the quiz_question_grades array - $grades = $info['MOD']['#']['QUESTION_GRADES']['0']['#']['QUESTION_GRADE']; - - //Iterate over question_grades - for($i = 0; $i < sizeof($grades); $i++) { - $gra_info = $grades[$i]; - //traverse_xmlize($gra_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($gra_info['#']['ID']['0']['#']); - - //Now, build the QUESTION_GRADES record structure - $grade->quiz = $quiz_id; - $grade->question = backup_todb($gra_info['#']['QUESTION']['0']['#']); - $grade->grade = backup_todb($gra_info['#']['GRADE']['0']['#']); - - //We have to recode the question field - $question = backup_getid($restore->backup_unique_code,"question",$grade->question); - if ($question) { - $grade->question = $question->new_id; - } - - //The structure is equal to the db, so insert the quiz_question_grades - $newid = $DB->insert_record ("quiz_question_instances",$grade); - - //Do some output - if (($i+1) % 10 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 200 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"quiz_question_instances",$oldid, - $newid); - } else { - $status = false; - } - } - - return $status; - } - - //This function restores the quiz_attempts - function quiz_attempts_restore_pre15_mods($quiz_id,$info,$restore,$quizquestions) { - global $CFG, $DB; - - $status = true; - - //Get the quiz_attempts array - $attempts = $info['MOD']['#']['ATTEMPTS']['0']['#']['ATTEMPT']; - - //Iterate over attempts - for($i = 0; $i < sizeof($attempts); $i++) { - $att_info = $attempts[$i]; - //traverse_xmlize($att_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($att_info['#']['ID']['0']['#']); - $olduserid = backup_todb($att_info['#']['USERID']['0']['#']); - - //Now, build the ATTEMPTS record structure - $attempt->quiz = $quiz_id; - $attempt->userid = backup_todb($att_info['#']['USERID']['0']['#']); - $attempt->attempt = backup_todb($att_info['#']['ATTEMPTNUM']['0']['#']); - $attempt->sumgrades = backup_todb($att_info['#']['SUMGRADES']['0']['#']); - $attempt->timestart = backup_todb($att_info['#']['TIMESTART']['0']['#']); - $attempt->timefinish = backup_todb($att_info['#']['TIMEFINISH']['0']['#']); - $attempt->timemodified = backup_todb($att_info['#']['TIMEMODIFIED']['0']['#']); - - //We have to recode the userid field - $user = backup_getid($restore->backup_unique_code,"user",$attempt->userid); - if ($user) { - $attempt->userid = $user->new_id; - } - - //Set the layout field (inherited from quiz by default) - $attempt->layout = $quizquestions; - - //Set the preview field (code from upgrade) - $cm = get_coursemodule_from_instance('quiz', $quiz_id); - if (has_capability('mod/quiz:preview', get_context_instance(CONTEXT_MODULE, $cm->id))) { - $attempt->preview = 1; - } - - //Set the uniqueid field - $attempt->uniqueid = question_new_attempt_uniqueid(); - - //The structure is equal to the db, so insert the quiz_attempts - $newid = $DB->insert_record ("quiz_attempts",$attempt); - - //Do some output - if (($i+1) % 10 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 200 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"quiz_attempts",$oldid, - $newid); - //Now process question_states (old quiz_responses table) - $status = question_states_restore_pre15_mods($newid,$att_info,$restore); - } else { - $status = false; - } - } - - return $status; - } - - //This function restores the question_states (old quiz_responses) - function question_states_restore_pre15_mods($attempt_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the quiz_responses array - $responses = $info['#']['RESPONSES']['0']['#']['RESPONSE']; - //Iterate over responses - for($i = 0; $i < sizeof($responses); $i++) { - $res_info = $responses[$i]; - //traverse_xmlize($res_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($res_info['#']['ID']['0']['#']); - - //Now, build the RESPONSES record structure - $response->attempt = $attempt_id; - $response->question = backup_todb($res_info['#']['QUESTION']['0']['#']); - $response->answer = backup_todb($res_info['#']['ANSWER']['0']['#']); - $response->grade = backup_todb($res_info['#']['GRADE']['0']['#']); - - //We have to recode the question field - $question = backup_getid($restore->backup_unique_code,"question",$response->question); - if ($question) { - $response->question = $question->new_id; - } - - //Set the raw_grade field (default to the existing grade one, no penalty in pre15 backups) - $response->raw_grade = $response->grade; - - //We have to recode the answer field - //It depends of the question type !! - //We get the question first - $question = $DB->get_record("question", array("id"=>$response->question)); - //It exists - if ($question) { - //Depending of the qtype, we make different recodes - switch ($question->qtype) { - case 1: //SHORTANSWER QTYPE - //Nothing to do. The response is a text. - break; - case 2: //TRUEFALSE QTYPE - //The answer is one answer id. We must recode it - $answer = backup_getid($restore->backup_unique_code,"question_answers",$response->answer); - if ($answer) { - $response->answer = $answer->new_id; - } - break; - case 3: //MULTICHOICE QTYPE - //The answer is a comma separated list of answers. We must recode them - $answer_field = ""; - $in_first = true; - $tok = strtok($response->answer,","); - while ($tok) { - //Get the answer from backup_ids - $answer = backup_getid($restore->backup_unique_code,"question_answers",$tok); - if ($answer) { - if ($in_first) { - $answer_field .= $answer->new_id; - $in_first = false; - } else { - $answer_field .= ",".$answer->new_id; - } - } - //check for next - $tok = strtok(","); - } - $response->answer = $answer_field; - break; - case 4: //RANDOM QTYPE - //The answer links to another question id, we must recode it - $answer_link = backup_getid($restore->backup_unique_code,"question",$response->answer); - if ($answer_link) { - $response->answer = $answer_link->new_id; - } - break; - case 5: //MATCH QTYPE - //The answer is a comma separated list of hypen separated math_subs (for question and answer) - $answer_field = ""; - $in_first = true; - $tok = strtok($response->answer,","); - while ($tok) { - //Extract the match_sub for the question and the answer - $exploded = explode("-",$tok); - $match_question_id = $exploded[0]; - $match_answer_id = $exploded[1]; - //Get the match_sub from backup_ids (for the question) - $match_que = backup_getid($restore->backup_unique_code,"question_match_sub",$match_question_id); - //Get the match_sub from backup_ids (for the answer) - $match_ans = backup_getid($restore->backup_unique_code,"question_match_sub",$match_answer_id); - if ($match_que) { - //It the question hasn't response, it must be 0 - if (!$match_ans and $match_answer_id == 0) { - $match_ans->new_id = 0; - } - if ($in_first) { - $answer_field .= $match_que->new_id."-".$match_ans->new_id; - $in_first = false; - } else { - $answer_field .= ",".$match_que->new_id."-".$match_ans->new_id; - } - } - //check for next - $tok = strtok(","); - } - $response->answer = $answer_field; - break; - case 6: //RANDOMSAMATCH QTYPE - //The answer is a comma separated list of hypen separated question_id and answer_id. We must recode them - $answer_field = ""; - $in_first = true; - $tok = strtok($response->answer,","); - while ($tok) { - //Extract the question_id and the answer_id - $exploded = explode("-",$tok); - $question_id = $exploded[0]; - $answer_id = $exploded[1]; - //Get the question from backup_ids - $que = backup_getid($restore->backup_unique_code,"question",$question_id); - //Get the answer from backup_ids - $ans = backup_getid($restore->backup_unique_code,"question_answers",$answer_id); - if ($que) { - //It the question hasn't response, it must be 0 - if (!$ans and $answer_id == 0) { - $ans->new_id = 0; - } - if ($in_first) { - $answer_field .= $que->new_id."-".$ans->new_id; - $in_first = false; - } else { - $answer_field .= ",".$que->new_id."-".$ans->new_id; - } - } - //check for next - $tok = strtok(","); - } - $response->answer = $answer_field; - break; - case 7: //DESCRIPTION QTYPE - //Nothing to do (there is no awser to this qtype) - //But this case must exist !! - break; - case 8: //NUMERICAL QTYPE - //Nothing to do. The response is a text. - break; - case 9: //MULTIANSWER QTYPE - //The answer is a comma separated list of hypen separated multianswer ids and answers. We must recode them. - //We need to have the sequence of questions here to be able to detect qtypes - $multianswerdb = $DB->get_record('question_multianswer',array('question'=>$response->question)); - //Make an array of sequence to easy access - $sequencearr = explode(",",$multianswerdb->sequence); - $answer_field = ""; - $in_first = true; - $tok = strtok($response->answer,","); - $counter = 1; - while ($tok) { - //Extract the multianswer_id and the answer - $exploded = explode("-",$tok); - $multianswer_id = $exploded[0]; - $answer = $exploded[1]; - //Get position key (if it fails, next iteration) - if ($oldposrec = backup_getid($restore->backup_unique_code,'multianswer-pos',$multianswer_id)) { - $positionkey = $oldposrec->new_id; - } else { - //Next iteration - $tok = strtok(","); - continue; - } - //Calculate question type - $questiondb = $DB->get_record('question', array('id'=>$sequencearr[$counter-1])); - $questiontype = $questiondb->qtype; - //Now, depending of the answertype field in question_multianswer - //we do diferent things - if ($questiontype == "1") { - //Shortanswer - //The answer is text, do nothing - } else if ($questiontype == "3") { - //Multichoice - //The answer is an answer_id, look for it in backup_ids - $ans = backup_getid($restore->backup_unique_code,"question_answers",$answer); - $answer = $ans->new_id; - } else if ($questiontype == "8") { - //Numeric - //The answer is text, do nothing - } - - //Finaly, build the new answer field for each pair - if ($in_first) { - $answer_field .= $positionkey."-".$answer; - $in_first = false; - } else { - $answer_field .= ",".$positionkey."-".$answer; - } - //check for next - $tok = strtok(","); - $counter++; - } - $response->answer = $answer_field; - break; - case 10: //CALCULATED QTYPE - //Nothing to do. The response is a text. - break; - default: //UNMATCHED QTYPE. - //This is an error (unimplemented qtype) - $status = false; - break; - } - } else { - $status = false; - } - - //The structure is equal to the db, so insert the question_states - $newid = $DB->insert_record ("question_states",$response); - - //Do some output - if (($i+1) % 10 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 200 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"question_states",$oldid, - $newid); - } else { - $status = false; - } - } - - return $status; - } - - //This function restores the quiz_grades - function quiz_grades_restore_pre15_mods($quiz_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the quiz_grades array - $grades = $info['MOD']['#']['GRADES']['0']['#']['GRADE']; - - //Iterate over grades - for($i = 0; $i < sizeof($grades); $i++) { - $gra_info = $grades[$i]; - //traverse_xmlize($gra_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($gra_info['#']['ID']['0']['#']); - $olduserid = backup_todb($gra_info['#']['USERID']['0']['#']); - - //Now, build the GRADES record structure - $grade->quiz = $quiz_id; - $grade->userid = backup_todb($gra_info['#']['USERID']['0']['#']); - $grade->grade = backup_todb($gra_info['#']['GRADEVAL']['0']['#']); - $grade->timemodified = backup_todb($gra_info['#']['TIMEMODIFIED']['0']['#']); - - //We have to recode the userid field - $user = backup_getid($restore->backup_unique_code,"user",$grade->userid); - if ($user) { - $grade->userid = $user->new_id; - } - - //The structure is equal to the db, so insert the quiz_grades - $newid = $DB->insert_record ("quiz_grades",$grade); - - //Do some output - if (($i+1) % 10 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 200 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"quiz_grades",$oldid, - $newid); - } else { - $status = false; - } - } - - return $status; - } - - //This function converts texts in FORMAT_WIKI to FORMAT_MARKDOWN for - //some texts in the module - function quiz_restore_pre15_wiki2markdown ($restore) { - global $CFG, $DB; - - $status = true; - - //Convert question->questiontext - if ($records = $DB->get_records_sql ("SELECT q.id, q.questiontext, q.questiontextformat - FROM {question} q, - {backup_ids} b - WHERE b.backup_code = ? AND - b.table_name = 'question' AND - q.id = b.new_id AND - q.questiontextformat = ".FORMAT_WIKI, array($restore->backup_unique_code))) { - foreach ($records as $record) { - //Rebuild wiki links - $record->questiontext = restore_decode_wiki_content($record->questiontext, $restore); - //Convert to Markdown - $wtm = new WikiToMarkdown(); - $record->questiontext = $wtm->convert($record->questiontext, $restore->course_id); - $record->questiontextformat = FORMAT_MARKDOWN; - $DB->update_record('question', $record); - //Do some output - $i++; - if (($i+1) % 1 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 20 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - } - } - return $status; - } - - //This function returns a log record with all the necessary transformations - //done. It's used by restore_log_module() to restore modules log. - function quiz_restore_pre15_logs($restore,$log) { - - $status = false; - - //Depending of the action, we recode different things - switch ($log->action) { - case "add": - if ($log->cmid) { - //Get the new_id of the module (to recode the info field) - $mod = backup_getid($restore->backup_unique_code,$log->module,$log->info); - if ($mod) { - $log->url = "view.php?id=".$log->cmid; - $log->info = $mod->new_id; - $status = true; - } - } - break; - case "update": - if ($log->cmid) { - //Get the new_id of the module (to recode the info field) - $mod = backup_getid($restore->backup_unique_code,$log->module,$log->info); - if ($mod) { - $log->url = "view.php?id=".$log->cmid; - $log->info = $mod->new_id; - $status = true; - } - } - break; - case "view": - if ($log->cmid) { - //Get the new_id of the module (to recode the info field) - $mod = backup_getid($restore->backup_unique_code,$log->module,$log->info); - if ($mod) { - $log->url = "view.php?id=".$log->cmid; - $log->info = $mod->new_id; - $status = true; - } - } - break; - case "view all": - $log->url = "index.php?id=".$log->course; - $status = true; - break; - case "report": - if ($log->cmid) { - //Get the new_id of the module (to recode the info field) - $mod = backup_getid($restore->backup_unique_code,$log->module,$log->info); - if ($mod) { - $log->url = "report.php?id=".$log->cmid; - $log->info = $mod->new_id; - $status = true; - } - } - break; - case "attempt": - if ($log->cmid) { - //Get the new_id of the module (to recode the info field) - $mod = backup_getid($restore->backup_unique_code,$log->module,$log->info); - if ($mod) { - //Extract the attempt id from the url field - $attid = substr(strrchr($log->url,"="),1); - //Get the new_id of the attempt (to recode the url field) - $att = backup_getid($restore->backup_unique_code,"quiz_attempts",$attid); - if ($att) { - $log->url = "review.php?id=".$log->cmid."&attempt=".$att->new_id; - $log->info = $mod->new_id; - $status = true; - } - } - } - break; - case "submit": - if ($log->cmid) { - //Get the new_id of the module (to recode the info field) - $mod = backup_getid($restore->backup_unique_code,$log->module,$log->info); - if ($mod) { - //Extract the attempt id from the url field - $attid = substr(strrchr($log->url,"="),1); - //Get the new_id of the attempt (to recode the url field) - $att = backup_getid($restore->backup_unique_code,"quiz_attempts",$attid); - if ($att) { - $log->url = "review.php?id=".$log->cmid."&attempt=".$att->new_id; - $log->info = $mod->new_id; - $status = true; - } - } - } - break; - case "review": - if ($log->cmid) { - //Get the new_id of the module (to recode the info field) - $mod = backup_getid($restore->backup_unique_code,$log->module,$log->info); - if ($mod) { - //Extract the attempt id from the url field - $attid = substr(strrchr($log->url,"="),1); - //Get the new_id of the attempt (to recode the url field) - $att = backup_getid($restore->backup_unique_code,"quiz_attempts",$attid); - if ($att) { - $log->url = "review.php?id=".$log->cmid."&attempt=".$att->new_id; - $log->info = $mod->new_id; - $status = true; - } - } - } - break; - case "editquestions": - if ($log->cmid) { - //Get the new_id of the module (to recode the url field) - $mod = backup_getid($restore->backup_unique_code,$log->module,$log->info); - if ($mod) { - $log->url = "view.php?id=".$log->cmid; - $log->info = $mod->new_id; - $status = true; - } - } - break; - default: - if (!defined('RESTORE_SILENTLY')) { - echo "action (".$log->module."-".$log->action.") unknow. Not restored
    "; //Debug - } - break; - } - - if ($status) { - $status = $log; - } - return $status; - } - diff --git a/question/restorelib.php b/question/restorelib.php index cad37de569081..c832e37ee1f1c 100644 --- a/question/restorelib.php +++ b/question/restorelib.php @@ -1,965 +1,4 @@ id) - // | - // | - // |....................................... - // | . - // | . - // | -------question_datasets------ . - // | | (CL,pk->id,fk->question, | . - // | | fk->dataset_definition) | . - // | | | . - // | | | . - // | | | . - // | | question_dataset_definitions - // | | (CL,pk->id,fk->category) - // question | - // (CL,pk->id,fk->category,files) | - // | question_dataset_items - // | (CL,pk->id,fk->definition) - // | - // | - // | - // -------------------------------------------------------------------------------------------------------------- - // | | | | | | | - // | | | | | | | - // | | | | question_calculated | | - // question_truefalse | question_multichoice | (CL,pl->id,fk->question) | | - // (CL,pk->id,fk->question) | (CL,pk->id,fk->question) | . | | question_randomsamatch - // . | . | . | |--(CL,pk->id,fk->question) - // . question_shortanswer . question_numerical . question_multianswer. | - // . (CL,pk->id,fk->question) . (CL,pk->id,fk->question) . (CL,pk->id,fk->question) | - // . . . . . . | question_match - // . . . . . . |--(CL,pk->id,fk->question) - // . . . . . . | . - // . . . . . . | . - // . . . . . . | . - // . . . . . . | question_match_sub - // ........................................................................................ |--(CL,pk->id,fk->question) - // . | - // . | - // . | question_numerical_units - // question_answers |--(CL,pk->id,fk->question) - // (CL,pk->id,fk->question)---------------------------------------------------------- - // - // - // The following holds the information about student interaction with the questions - // - // question_sessions - // (UL,pk->id,fk->attempt,question) - // . - // . - // question_states - // (UL,pk->id,fk->attempt,question) - // - // Meaning: pk->primary key field of the table - // fk->foreign key to link with parent - // nt->nested field (recursive data) - // SL->site level info - // CL->course level info - // UL->user level info - // files->table may have files - // - //----------------------------------------------------------- - - include_once($CFG->libdir.'/questionlib.php'); - - /** - * Returns the best question category (id) found to restore one - * question category from a backup file. Works by stamp. - * - * @param object $restore preferences for restoration - * @param array $contextinfo fragment of decoded xml - * @return object best context instance for this category to be in - */ - function restore_question_get_best_category_context($restore, $contextinfo) { - global $DB; - - switch ($contextinfo['LEVEL'][0]['#']) { - case 'module': - if (!$instanceinfo = backup_getid($restore->backup_unique_code, 'course_modules', $contextinfo['INSTANCE'][0]['#'])){ - //module has not been restored, probably not selected for restore - return false; - } - $tocontext = get_context_instance(CONTEXT_MODULE, $instanceinfo->new_id); - break; - case 'course': - $tocontext = get_context_instance(CONTEXT_COURSE, $restore->course_id); - break; - case 'coursecategory': - //search COURSECATEGORYLEVEL steps up the course cat tree or - //to the top of the tree if steps are exhausted. - $catno = $contextinfo['COURSECATEGORYLEVEL'][0]['#']; - $catid = $DB->get_field('course', 'category', array('id'=>$restore->course_id)); - while ($catno > 1){ - $nextcatid = $DB->get_field('course_categories', 'parent', array('id'=>$catid)); - if ($nextcatid == 0){ - break; - } - $catid = $nextcatid; - $catno--; - } - $tocontext = get_context_instance(CONTEXT_COURSECAT, $catid); - break; - case 'system': - $tocontext = get_context_instance(CONTEXT_SYSTEM); - break; - } - return $tocontext; - } - - function restore_question_categories($info, $restore) { - $status = true; - //Iterate over each category - foreach ($info as $category) { - $status = $status && restore_question_category($category, $restore); - } - $status = $status && restore_recode_category_parents($restore); - return $status; - } - - function restore_question_category($category, $restore){ - global $DB; - - $status = true; - //Skip empty categories (some backups can contain them) - if (!empty($category->id)) { - //Get record from backup_ids - $data = backup_getid($restore->backup_unique_code, "question_categories", $category->id); - - if ($data) { - //Now get completed xmlized object - $info = $data->info; - //traverse_xmlize($info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_categories record structure - $question_cat = new stdClass; - $question_cat->name = backup_todb($info['QUESTION_CATEGORY']['#']['NAME']['0']['#']); - $question_cat->info = backup_todb($info['QUESTION_CATEGORY']['#']['INFO']['0']['#']); - $question_cat->stamp = backup_todb($info['QUESTION_CATEGORY']['#']['STAMP']['0']['#']); - //parent is fixed after all categories are restored and we know all the new ids. - $question_cat->parent = backup_todb($info['QUESTION_CATEGORY']['#']['PARENT']['0']['#']); - $question_cat->sortorder = backup_todb($info['QUESTION_CATEGORY']['#']['SORTORDER']['0']['#']); - if (!$question_cat->stamp) { - $question_cat->stamp = make_unique_id_code(); - } - if (isset($info['QUESTION_CATEGORY']['#']['PUBLISH'])) { - $course = $restore->course_id; - $publish = backup_todb($info['QUESTION_CATEGORY']['#']['PUBLISH']['0']['#']); - if ($publish){ - $tocontext = get_context_instance(CONTEXT_SYSTEM); - } else { - $tocontext = get_context_instance(CONTEXT_COURSE, $course); - } - } else { - if (!$tocontext = restore_question_get_best_category_context($restore, $info['QUESTION_CATEGORY']['#']['CONTEXT']['0']['#'])){ - return $status; // context doesn't exist - a module has not been restored - } - } - $question_cat->contextid = $tocontext->id; - - //does cat exist ?? if it does we check if the cat and questions already exist whether we have - //add permission or not if we have no permission to add questions to SYSTEM or COURSECAT context - //AND the question does not already exist then we create questions in COURSE context. - if (!$fcat = $DB->get_record('question_categories', array('contextid'=>$question_cat->contextid, 'stamp'=>$question_cat->stamp))) { - //no preexisting cat - if ((($tocontext->contextlevel == CONTEXT_SYSTEM) || ($tocontext->contextlevel == CONTEXT_COURSECAT)) - && !has_capability('moodle/question:add', $tocontext)){ - //no preexisting cat and no permission to create questions here - //must restore to course. - $tocontext = get_context_instance(CONTEXT_COURSE, $restore->course_id); - } - $question_cat->contextid = $tocontext->id; - if (!$fcat = $DB->get_record('question_categories', array('contextid'=>$question_cat->contextid, 'stamp'=>$question_cat->stamp))) { - $question_cat->id = $DB->insert_record ("question_categories", $question_cat); - } else { - $question_cat = $fcat; - } - //we'll be restoring all questions here. - backup_putid($restore->backup_unique_code, "question_categories", $category->id, $question_cat->id); - } else { - $question_cat = $fcat; - //we found an existing best category - //but later if context is above course need to check if there are questions need creating in category - //if we do need to create questions and permissions don't allow it create new category in course - } - - //Do some output - if (!defined('RESTORE_SILENTLY')) { - echo "
  • ".get_string('category', 'quiz')." \"".$question_cat->name."\"
    "; - } - - backup_flush(300); - - //start with questions - if ($question_cat->id) { - //We have the newid, update backup_ids - //Now restore question - $status = restore_questions($category->id, $question_cat, $info, $restore); - } else { - $status = false; - } - if (!defined('RESTORE_SILENTLY')) { - echo '
  • '; - } - } else { - echo 'Could not get backup info for question category'. $category->id; - } - } - return $status; - } - - function restore_recode_category_parents($restore){ - global $CFG, $DB; - $status = true; - //Now we have to recode the parent field of each restored category - $categories = $DB->get_records_sql("SELECT old_id, new_id - FROM {backup_ids} - WHERE backup_code = ? AND - table_name = 'question_categories'", array($restore->backup_unique_code)); - if ($categories) { - //recode all parents to point at their old parent cats no matter what context the parent is now in - foreach ($categories as $category) { - $restoredcategory = $DB->get_record('question_categories', array('id'=>$category->new_id)); - if ($restoredcategory && $restoredcategory->parent != 0) { - $updateobj = new stdClass(); - $updateobj->id = $restoredcategory->id; - $idcat = backup_getid($restore->backup_unique_code,'question_categories',$restoredcategory->parent); - if ($idcat->new_id) { - $updateobj->parent = $idcat->new_id; - } else { - $updateobj->parent = 0; - } - $DB->update_record('question_categories', $updateobj); - } - } - //now we have recoded all parents, check through all parents and set parent to be - //grand parent / great grandparent etc where there is one in same context - //or else set parent to 0 (top level category). - $toupdate = array(); - foreach ($categories as $category) { - $restoredcategory = $DB->get_record('question_categories', array('id'=>$category->new_id)); - if ($restoredcategory && $restoredcategory->parent != 0) { - $nextparentid = $restoredcategory->parent; - do { - if (!$parent = $DB->get_record('question_categories', array('id'=>$nextparentid))) { - if (!defined('RESTORE_SILENTLY')) { - echo 'Could not find parent for question category '. $category->id.' recoding as top category item.
    '; - } - break;//record fetch failed finish loop - } else { - $nextparentid = $parent->parent; - } - } while (($nextparentid != 0) && ($parent->contextid != $restoredcategory->contextid)); - if (!$parent || ($parent->id != $restoredcategory->parent)){ - //change needs to be made to the parent field. - if ($parent && ($parent->contextid == $restoredcategory->contextid)){ - $toupdate[$restoredcategory->id] = $parent->id; - } else { - //searched up the tree till we came to the top and did not find cat in same - //context or there was an error getting next parent record - $toupdate[$restoredcategory->id] = 0; - } - } - } - } - //now finally do the changes to parent field. - foreach ($toupdate as $id => $parent){ - $updateobj = new stdClass(); - $updateobj->id = $id; - $updateobj->parent = $parent; - $DB->update_record('question_categories', $updateobj); - } - } - return $status; - } - - function restore_questions ($old_category_id, $best_question_cat, $info, $restore) { - global $CFG, $QTYPES, $DB; - - $status = true; - $restored_questions = array(); - - //Get the questions array - if (!empty($info['QUESTION_CATEGORY']['#']['QUESTIONS'])) { - $questions = $info['QUESTION_CATEGORY']['#']['QUESTIONS']['0']['#']['QUESTION']; - } else { - $questions = array(); - } - - //Iterate over questions - for($i = 0; $i < sizeof($questions); $i++) { - $que_info = $questions[$i]; - //traverse_xmlize($que_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($que_info['#']['ID']['0']['#']); - - //Now, build the question record structure - $question = new stdClass(); - $question->parent = backup_todb($que_info['#']['PARENT']['0']['#']); - $question->name = backup_todb($que_info['#']['NAME']['0']['#']); - $question->questiontext = backup_todb($que_info['#']['QUESTIONTEXT']['0']['#']); - $question->questiontextformat = backup_todb($que_info['#']['QUESTIONTEXTFORMAT']['0']['#']); - $question->image = backup_todb($que_info['#']['IMAGE']['0']['#']); - $question->generalfeedback = backup_todb_optional_field($que_info, 'GENERALFEEDBACK', ''); - $question->defaultgrade = backup_todb($que_info['#']['DEFAULTGRADE']['0']['#']); - $question->penalty = backup_todb($que_info['#']['PENALTY']['0']['#']); - $question->qtype = backup_todb($que_info['#']['QTYPE']['0']['#']); - $question->length = backup_todb($que_info['#']['LENGTH']['0']['#']); - $question->stamp = backup_todb($que_info['#']['STAMP']['0']['#']); - $question->version = backup_todb($que_info['#']['VERSION']['0']['#']); - $question->hidden = backup_todb($que_info['#']['HIDDEN']['0']['#']); - $question->timecreated = backup_todb_optional_field($que_info, 'TIMECREATED', 0); - $question->timemodified = backup_todb_optional_field($que_info, 'TIMEMODIFIED', 0); - - // Set the createdby field, if the user was in the backup, or if we are on the same site. - $createdby = backup_todb_optional_field($que_info, 'CREATEDBY', null); - if (!empty($createdby)) { - $user = backup_getid($restore->backup_unique_code, 'user', $createdby); - if ($user) { - $question->createdby = $user->new_id; - } else if (backup_is_same_site($restore)) { - $question->createdby = $createdby; - } - } - - // Set the modifiedby field, if the user was in the backup, or if we are on the same site. - $modifiedby = backup_todb_optional_field($que_info, 'MODIFIEDBY', null); - if (!empty($createdby)) { - $user = backup_getid($restore->backup_unique_code, 'user', $modifiedby); - if ($user) { - $question->modifiedby = $user->new_id; - } else if (backup_is_same_site($restore)) { - $question->modifiedby = $modifiedby; - } - } - - if ($restore->backup_version < 2006032200) { - // The qtype was an integer that now needs to be converted to the name - $qtypenames = array(1=>'shortanswer',2=>'truefalse',3=>'multichoice',4=>'random',5=>'match', - 6=>'randomsamatch',7=>'description',8=>'numerical',9=>'multianswer',10=>'calculated', - 11=>'rqp',12=>'essay'); - $question->qtype = $qtypenames[$question->qtype]; - } - - //Check if the question exists by category, stamp, and version - //first check for the question in the context specified in backup - $existingquestion = $DB->get_record ("question", array("category"=>$best_question_cat->id, "stamp"=>$question->stamp,"version"=>$question->version)); - //If the question exists, only record its id - //always use existing question, no permissions check here - if ($existingquestion) { - $question = $existingquestion; - $creatingnewquestion = false; - } else { - //then if context above course level check permissions and if no permission - //to restore above course level then restore to cat in course context. - $bestcontext = get_context_instance_by_id($best_question_cat->contextid); - if (($bestcontext->contextlevel == CONTEXT_SYSTEM || $bestcontext->contextlevel == CONTEXT_COURSECAT) - && !has_capability('moodle/question:add', $bestcontext)){ - if (!isset($course_question_cat)) { - $coursecontext = get_context_instance(CONTEXT_COURSE, $restore->course_id); - $course_question_cat = clone($best_question_cat); - $course_question_cat->contextid = $coursecontext->id; - //create cat if it doesn't exist - if (!$fcat = $DB->get_record('question_categories', array('contextid'=>$course_question_cat->contextid, 'stamp'=>$course_question_cat->stamp))) { - $course_question_cat->id = $DB->insert_record("question_categories", $course_question_cat); - backup_putid($restore->backup_unique_code, "question_categories", $old_category_id, $course_question_cat->id); - } else { - $course_question_cat = $fcat; - } - //will fix category parents after all questions and categories restored. Will set parent to 0 if - //no parent in same context. - } - $question->category = $course_question_cat->id; - //does question already exist in course cat - $existingquestion = $DB->get_record("question", array("category"=>$question->category, "stamp"=>$question->stamp, "version"=>$question->version)); - } else { - //permissions ok, restore to best cat - $question->category = $best_question_cat->id; - } - if (!$existingquestion){ - //The structure is equal to the db, so insert the question - $question->id = $DB->insert_record ("question", $question); - $creatingnewquestion = true; - } else { - $question = $existingquestion; - $creatingnewquestion = false; - } - } - - // Fixing bug #5482: random questions have parent field set to its own id, - // see: $QTYPES['random']->get_question_options() - if ($question->qtype == 'random' && $creatingnewquestion) { - $question->parent = $question->id; - $DB->set_field('question', 'parent', $question->parent, array('id'=>$question->id)); - } - - //Save newid to backup tables - if ($question->id) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code, "question", $oldid, $question->id); - } - - $restored_questions[$i] = new stdClass; - $restored_questions[$i]->newid = $question->id; - $restored_questions[$i]->oldid = $oldid; - $restored_questions[$i]->qtype = $question->qtype; - $restored_questions[$i]->parent = $question->parent; - $restored_questions[$i]->is_new = $creatingnewquestion; - } - backup_flush(300); - - // Loop again, now all the question id mappings exist, so everything can - // be restored. - for($i = 0; $i < sizeof($questions); $i++) { - $que_info = $questions[$i]; - - $newid = $restored_questions[$i]->newid; - $oldid = $restored_questions[$i]->oldid; - - $question = new stdClass(); - $question->qtype = $restored_questions[$i]->qtype; - $question->parent = $restored_questions[$i]->parent; - - - /// If it's a new question in the DB, restore it - if ($restored_questions[$i]->is_new) { - - /// We have to recode the parent field - if ($question->parent && $question->qtype != 'random') { - /// If the parent field needs to be changed, do it here. Random questions are dealt with above. - if ($parent = backup_getid($restore->backup_unique_code,"question",$question->parent)) { - $question->parent = $parent->new_id; - if ($question->parent != $restored_questions[$i]->parent) { - $DB->set_field('question', 'parent', $question->parent, array('id'=>$newid)); - } - } else { - echo 'Could not recode parent '.$question->parent.' for question '.$oldid.'
    '; - $status = false; - } - } - - //Now, restore every question_answers in this question - $status = question_restore_answers($oldid,$newid,$que_info,$restore); - // Restore questiontype specific data - if (array_key_exists($question->qtype, $QTYPES)) { - $status = $QTYPES[$question->qtype]->restore($oldid,$newid,$que_info,$restore); - } else { - echo 'Unknown question type '.$question->qtype.' for question '.$oldid.'
    '; - $status = false; - } - } else { - //We are NOT creating the question, but we need to know every question_answers - //map between the XML file and the database to be able to restore the states - //in each attempt. - $status = question_restore_map_answers($oldid,$newid,$que_info,$restore); - // Do the questiontype specific mapping - if (array_key_exists($question->qtype, $QTYPES)) { - $status = $QTYPES[$question->qtype]->restore_map($oldid,$newid,$que_info,$restore); - } else { - echo 'Unknown question type '.$question->qtype.' for question '.$oldid.'
    '; - $status = false; - } - } - - //Do some output - if (($i+1) % 2 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 40 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - } - return $status; - } - - function backup_todb_optional_field($data, $field, $default) { - if (array_key_exists($field, $data['#'])) { - return backup_todb($data['#'][$field]['0']['#']); - } else { - return $default; - } - } - - function question_restore_answers ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - $qtype = backup_todb($info['#']['QTYPE']['0']['#']); - - //Get the answers array - if (isset($info['#']['ANSWERS']['0']['#']['ANSWER'])) { - $answers = $info['#']['ANSWERS']['0']['#']['ANSWER']; - - //Iterate over answers - for($i = 0; $i < sizeof($answers); $i++) { - $ans_info = $answers[$i]; - //traverse_xmlize($ans_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($ans_info['#']['ID']['0']['#']); - - //Now, build the question_answers record structure - $answer = new stdClass; - $answer->question = $new_question_id; - $answer->answer = backup_todb($ans_info['#']['ANSWER_TEXT']['0']['#']); - $answer->fraction = backup_todb($ans_info['#']['FRACTION']['0']['#']); - $answer->feedback = backup_todb($ans_info['#']['FEEDBACK']['0']['#']); - - // Update 'match everything' answers for numerical questions coming from old backup files. - if ($qtype == 'numerical' && $answer->answer == '') { - $answer->answer = '*'; - } - - //The structure is equal to the db, so insert the question_answers - $newid = $DB->insert_record ("question_answers",$answer); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"question_answers",$oldid, - $newid); - } else { - $status = false; - } - } - } - - return $status; - } - - function question_restore_map_answers ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - if (!isset($info['#']['ANSWERS'])) { // No answers in this question (eg random) - return $status; - } - - //Get the answers array - $answers = $info['#']['ANSWERS']['0']['#']['ANSWER']; - - //Iterate over answers - for($i = 0; $i < sizeof($answers); $i++) { - $ans_info = $answers[$i]; - //traverse_xmlize($ans_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($ans_info['#']['ID']['0']['#']); - - //Now, build the question_answers record structure - $answer->question = $new_question_id; - $answer->answer = backup_todb($ans_info['#']['ANSWER_TEXT']['0']['#']); - $answer->fraction = backup_todb($ans_info['#']['FRACTION']['0']['#']); - $answer->feedback = backup_todb($ans_info['#']['FEEDBACK']['0']['#']); - - //If we are in this method is because the question exists in DB, so its - //answers must exist too. - //Now, we are going to look for that answer in DB and to create the - //mappings in backup_ids to use them later where restoring states (user level). - - //Get the answer from DB (by question and answer) - $db_answer = $DB->get_record ("question_answers", array("question"=>$new_question_id, "answer"=>$answer->answer)); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($db_answer) { - //We have the database answer, update backup_ids - backup_putid($restore->backup_unique_code,"question_answers",$oldid, $db_answer->id); - } else { - $status = false; - } - } - - return $status; - } - - function question_restore_numerical_units($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the numerical array - if (!empty($info['#']['NUMERICAL_UNITS'])) { - $numerical_units = $info['#']['NUMERICAL_UNITS']['0']['#']['NUMERICAL_UNIT']; - } else { - $numerical_units = array(); - } - - //Iterate over numerical_units - for($i = 0; $i < sizeof($numerical_units); $i++) { - $nu_info = $numerical_units[$i]; - //traverse_xmlize($nu_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - // Check to see if this until already exists in the database, which it might, for - // Historical reasons. - $unit = backup_todb($nu_info['#']['UNIT']['0']['#']); - if (!$DB->record_exists('question_numerical_units', array('question'=>$new_question_id, 'unit'=>$unit))) { - - //Now, build the question_numerical_UNITS record structure. - $numerical_unit = new stdClass; - $numerical_unit->question = $new_question_id; - $numerical_unit->multiplier = backup_todb($nu_info['#']['MULTIPLIER']['0']['#']); - $numerical_unit->unit = $unit; - - //The structure is equal to the db, so insert the question_numerical_units - $newid = $DB->insert_record("question_numerical_units", $numerical_unit); - - if (!$newid) { - $status = false; - } - } - } - - return $status; - } - - function question_restore_numerical_options($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - //Get the numerical_options array - // need to check as old questions don't have calculated_options record - if(isset($info['#']['NUMERICAL_OPTIONS'])){ - $numerical_options = $info['#']['numerical_OPTIONS']; - - //Iterate over numerical_options - for($i = 0; $i < sizeof($numerical_options); $i++){ - $num_info = $numerical_options[$i]; - //traverse_xmlize($cal_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_numerical_options record structure - $numerical_options->questionid = $new_question_id; - $numerical_options->instructions = backup_todb($num_info['#']['INSTRUCTIONS']['0']['#']); - $numerical_options->showunits = backup_todb($num_info['#']['SHOWUNITS']['0']['#']); - $numerical_options->unitsleft = backup_todb($num_info['#']['UNITSLEFT']['0']['#']); - $numerical_options->unitgradingtype = backup_todb($num_info['#']['UNITGRADINGTYPE']['0']['#']); - $numerical_options->unitpenalty = backup_todb($num_info['#']['UNITPENALTY']['0']['#']); - - //The structure is equal to the db, so insert the question_numerical__options - $newid = $DB->insert_record ("question_numerical__options",$numerical__options); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - } - } - } - - - function question_restore_dataset_definitions ($old_question_id,$new_question_id,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the dataset_definitions array - $dataset_definitions = $info['#']['DATASET_DEFINITIONS']['0']['#']['DATASET_DEFINITION']; - - //Iterate over dataset_definitions - for($i = 0; $i < sizeof($dataset_definitions); $i++) { - $dd_info = $dataset_definitions[$i]; - //traverse_xmlize($dd_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_dataset_DEFINITION record structure - $dataset_definition = new stdClass; - $dataset_definition->category = backup_todb($dd_info['#']['CATEGORY']['0']['#']); - $dataset_definition->name = backup_todb($dd_info['#']['NAME']['0']['#']); - $dataset_definition->type = backup_todb($dd_info['#']['TYPE']['0']['#']); - $dataset_definition->options = backup_todb($dd_info['#']['OPTIONS']['0']['#']); - $dataset_definition->itemcount = backup_todb($dd_info['#']['ITEMCOUNT']['0']['#']); - - //We have to recode the category field (only if the category != 0) - if ($dataset_definition->category != 0) { - $category = backup_getid($restore->backup_unique_code,"question_categories",$dataset_definition->category); - if ($category) { - $dataset_definition->category = $category->new_id; - } else { - echo 'Could not recode category id '.$dataset_definition->category.' for dataset definition'.$dataset_definition->name.'
    '; - } - } - - //Now, we hace to decide when to create the new records or reuse an existing one - $create_definition = false; - - //If the dataset_definition->category = 0, it's a individual question dataset_definition, so we'll create it - if ($dataset_definition->category == 0) { - $create_definition = true; - } else { - //The category isn't 0, so it's a category question dataset_definition, we have to see if it exists - //Look for a definition with the same category, name and type - if ($definitionrec = $DB->get_records('question_dataset_definitions', array('category'=>$dataset_definition->category, - 'name'=>$dataset_definition->name, - 'type'=>$dataset_definition->type))) { - //Such dataset_definition exist. Now we must check if it has enough itemcount - if ($definitionrec->itemcount < $dataset_definition->itemcount) { - //We haven't enough itemcount, so we have to create the definition as an individual question one. - $dataset_definition->category = 0; - $create_definition = true; - } else { - //We have enough itemcount, so we'll reuse the existing definition - $create_definition = false; - $newid = $definitionrec->id; - } - } else { - //Such dataset_definition doesn't exist. We'll create it. - $create_definition = true; - } - } - - //If we've to create the definition, do it - if ($create_definition) { - //The structure is equal to the db, so insert the question_dataset_definitions - $newid = $DB->insert_record ("question_dataset_definitions",$dataset_definition); - if ($newid) { - //Restore question_dataset_items - $status = question_restore_dataset_items($newid,$dd_info,$restore); - } - } - - //Now, we must have a definition (created o reused). Its id is in newid. Create the question_datasets record - //to join the question and the dataset_definition - if ($newid) { - $question_dataset = new stdClass; - $question_dataset->question = $new_question_id; - $question_dataset->datasetdefinition = $newid; - $newid = $DB->insert_record ("question_datasets",$question_dataset); - } - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - function question_restore_dataset_items ($definitionid,$info,$restore) { - global $CFG, $DB; - - $status = true; - - //Get the items array - $dataset_items = $info['#']['DATASET_ITEMS']['0']['#']['DATASET_ITEM']; - - //Iterate over dataset_items - for($i = 0; $i < sizeof($dataset_items); $i++) { - $di_info = $dataset_items[$i]; - //traverse_xmlize($di_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_dataset_ITEMS record structure - $dataset_item = new stdClass; - $dataset_item->definition = $definitionid; - $dataset_item->itemnumber = backup_todb($di_info['#']['NUMBER']['0']['#']); - $dataset_item->value = backup_todb($di_info['#']['VALUE']['0']['#']); - - //The structure is equal to the db, so insert the question_dataset_items - $newid = $DB->insert_record ("question_dataset_items",$dataset_item); - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - - //This function restores the question_states - function question_states_restore_mods($attempt_id,$info,$restore) { - global $CFG, $QTYPES, $DB; - - $status = true; - - //Get the question_states array - $states = $info['#']['STATES']['0']['#']['STATE']; - //Iterate over states - for($i = 0; $i < sizeof($states); $i++) { - $res_info = $states[$i]; - //traverse_xmlize($res_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //We'll need this later!! - $oldid = backup_todb($res_info['#']['ID']['0']['#']); - - //Now, build the STATES record structure - $state = new stdClass; - $state->attempt = $attempt_id; - $state->question = backup_todb($res_info['#']['QUESTION']['0']['#']); - $state->seq_number = backup_todb($res_info['#']['SEQ_NUMBER']['0']['#']); - $state->answer = backup_todb($res_info['#']['ANSWER']['0']['#']); - $state->timestamp = backup_todb($res_info['#']['TIMESTAMP']['0']['#']); - $state->event = backup_todb($res_info['#']['EVENT']['0']['#']); - $state->grade = backup_todb($res_info['#']['GRADE']['0']['#']); - $state->raw_grade = backup_todb($res_info['#']['RAW_GRADE']['0']['#']); - $state->penalty = backup_todb($res_info['#']['PENALTY']['0']['#']); - $state->oldid = $oldid; // So it is available to restore_recode_answer. - - //We have to recode the question field - $question = backup_getid($restore->backup_unique_code,"question",$state->question); - if ($question) { - $state->question = $question->new_id; - } else { - echo 'Could not recode question id '.$state->question.' for state '.$oldid.'
    '; - } - - //We have to recode the answer field - //It depends of the question type !! - //We get the question first - if (!$question = $DB->get_record("question", array("id"=>$state->question))) { - print_error("Can't find the record for question $state->question for which I am trying to restore a state"); - } - //Depending on the qtype, we make different recodes - if ($state->answer) { - $state->answer = $QTYPES[$question->qtype]->restore_recode_answer($state, $restore); - } - - //The structure is equal to the db, so insert the question_states - $newid = $DB->insert_record ("question_states",$state); - - //Do some output - if (($i+1) % 10 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 200 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code, 'question_states', $oldid, $newid); - } else { - $status = false; - } - } - - //Get the question_sessions array - $sessions = $info['#']['NEWEST_STATES']['0']['#']['NEWEST_STATE']; - //Iterate over question_sessions - for($i = 0; $i < sizeof($sessions); $i++) { - $res_info = $sessions[$i]; - //traverse_xmlize($res_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the NEWEST_STATES record structure - $session = new stdClass; - $session->attemptid = $attempt_id; - $session->questionid = backup_todb($res_info['#']['QUESTIONID']['0']['#']); - $session->newest = backup_todb($res_info['#']['NEWEST']['0']['#']); - $session->newgraded = backup_todb($res_info['#']['NEWGRADED']['0']['#']); - $session->sumpenalty = backup_todb($res_info['#']['SUMPENALTY']['0']['#']); - - if (isset($res_info['#']['MANUALCOMMENT']['0']['#'])) { - $session->manualcomment = backup_todb($res_info['#']['MANUALCOMMENT']['0']['#']); - } else { // pre 1.7 backups - $session->manualcomment = backup_todb($res_info['#']['COMMENT']['0']['#']); - } - - //We have to recode the question field - $question = backup_getid($restore->backup_unique_code,"question",$session->questionid); - if ($question) { - $session->questionid = $question->new_id; - } else { - echo 'Could not recode question id '.$session->questionid.'
    '; - } - - //We have to recode the newest field - $state = backup_getid($restore->backup_unique_code,"question_states",$session->newest); - if ($state) { - $session->newest = $state->new_id; - } else { - echo 'Could not recode newest state id '.$session->newest.'
    '; - } - - //If the session has been graded we have to recode the newgraded field - if ($session->newgraded) { - $state = backup_getid($restore->backup_unique_code,"question_states",$session->newgraded); - if ($state) { - $session->newgraded = $state->new_id; - } else { - echo 'Could not recode newest graded state id '.$session->newgraded.'
    '; - } - } - - //The structure is equal to the db, so insert the question_sessions - $newid = $DB->insert_record ("question_sessions",$session); - - } - - return $status; - } /** * Recode content links in question texts. diff --git a/question/type/calculated/backup/moodle2/backup_qtype_calculated_plugin.class.php b/question/type/calculated/backup/moodle2/backup_qtype_calculated_plugin.class.php index 9cc224dba8c2b..b82e94d5e8908 100644 --- a/question/type/calculated/backup/moodle2/backup_qtype_calculated_plugin.class.php +++ b/question/type/calculated/backup/moodle2/backup_qtype_calculated_plugin.class.php @@ -28,17 +28,15 @@ */ class backup_qtype_calculated_plugin extends backup_qtype_plugin { - protected function get_qtype_name() { - return 'calculated'; - } - /** * Returns the qtype information to attach to question element */ protected function define_question_plugin_structure() { // Define the virtual plugin element with the condition to fulfill - $plugin = $this->get_plugin_element(null, '../../qtype', $this->get_qtype_name()); + // Note: we use $this->pluginname so for extended plugins this will work + // automatically: calculatedsimple and calculatedmulti + $plugin = $this->get_plugin_element(null, '../../qtype', $this->pluginname); // Create one standard named plugin element (the visible container) $pluginwrapper = new backup_nested_element($this->get_recommended_name()); @@ -86,4 +84,26 @@ protected function define_question_plugin_structure() { return $plugin; } + + /** + * Returns one array with filearea => mappingname elements for the qtype + * + * Used by {@link get_components_and_fileareas} to know about all the qtype + * files to be processed both in backup and restore. + */ + public static function get_qtype_fileareas() { + // TODO: Discuss. Commented below are the "in theory" correct + // mappings for those fileareas. Instead we are using question for + // them, that will cause problems in the future if we want to change + // any of them to be 1..n (i.e. we should be always pointing to own id) + return array( + //'instruction' => 'question_numerical_option', + //'correctfeedback' => 'question_calculated_option', + //'partiallycorrectfeedback' => 'question_calculated_option', + //'incorrectfeedback' => 'question_calculated_option'); + 'instruction' => 'question_created', + 'correctfeedback' => 'question_created', + 'partiallycorrectfeedback' => 'question_created', + 'incorrectfeedback' => 'question_created'); + } } diff --git a/question/type/calculated/backup/moodle2/restore_qtype_calculated_plugin.class.php b/question/type/calculated/backup/moodle2/restore_qtype_calculated_plugin.class.php new file mode 100644 index 0000000000000..5ed31f253acd9 --- /dev/null +++ b/question/type/calculated/backup/moodle2/restore_qtype_calculated_plugin.class.php @@ -0,0 +1,116 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one calculated qtype plugin + */ +class restore_qtype_calculated_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // This qtype uses question_answers, add them + $this->add_question_question_answers($paths); + + // This qtype uses question_numerical_options and question_numerical_units, add them + $this->add_question_numerical_options($paths); + $this->add_question_numerical_units($paths); + + // This qtype uses question datasets, add them + $this->add_question_datasets($paths); + + // Add own qtype stuff + $elename = 'calculated_record'; + $elepath = $this->get_pathfor('/calculated_records/calculated_record'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + $elename = 'calculated_option'; + $elepath = $this->get_pathfor('/calculated_options/calculated_option'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; // And we return the interesting paths + } + + /** + * Process the qtype/calculated_record element + */ + public function process_calculated_record($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_calculated too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + $data->answer = $this->get_mappingid('question_answer', $data->answer); + // Insert record + $newitemid = $DB->insert_record('question_calculated', $data); + // Create mapping (not needed, no files nor childs nor states here) + //$this->set_mapping('question_calculated', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } + + /** + * Process the qtype/calculated_option element + */ + public function process_calculated_option($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_calculated too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + // Insert record + $newitemid = $DB->insert_record('question_calculated_options', $data); + // Create mapping (not needed, no files nor childs nor states here) + // $this->set_mapping('question_calculated_option', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } +} diff --git a/question/type/calculated/questiontype.php b/question/type/calculated/questiontype.php index 8552ebc3facf9..b0faf5e344fdd 100644 --- a/question/type/calculated/questiontype.php +++ b/question/type/calculated/questiontype.php @@ -2042,108 +2042,6 @@ function get_virtual_qtype() { return $this->virtualqtype; } - /// RESTORE FUNCTIONS ///////////////// - - /* - * Restores the data in the question - * - * This is used in question/restorelib.php - */ - function restore($old_question_id,$new_question_id,$info,$restore) { - global $DB; - - $status = true; - - //Get the calculated-s array - $calculateds = $info['#']['CALCULATED']; - - //Iterate over calculateds - for($i = 0; $i < sizeof($calculateds); $i++) { - $cal_info = $calculateds[$i]; - //traverse_xmlize($cal_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_calculated record structure - $calculated->question = $new_question_id; - $calculated->answer = backup_todb($cal_info['#']['ANSWER']['0']['#']); - $calculated->tolerance = backup_todb($cal_info['#']['TOLERANCE']['0']['#']); - $calculated->tolerancetype = backup_todb($cal_info['#']['TOLERANCETYPE']['0']['#']); - $calculated->correctanswerlength = backup_todb($cal_info['#']['CORRECTANSWERLENGTH']['0']['#']); - $calculated->correctanswerformat = backup_todb($cal_info['#']['CORRECTANSWERFORMAT']['0']['#']); - - ////We have to recode the answer field - $answer = backup_getid($restore->backup_unique_code,"question_answers",$calculated->answer); - if ($answer) { - $calculated->answer = $answer->new_id; - } - - //The structure is equal to the db, so insert the question_calculated - $newid = $DB->insert_record ("question_calculated",$calculated); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - //Get the calculated_options array - // need to check as old questions don't have calculated_options record - if(isset($info['#']['CALCULATED_OPTIONS'])){ - $calculatedoptions = $info['#']['CALCULATED_OPTIONS']; - - //Iterate over calculated_options - for($i = 0; $i < sizeof($calculatedoptions); $i++){ - $cal_info = $calculatedoptions[$i]; - //traverse_xmlize($cal_info); //Debug - //print_object ($GLOBALS['traverse_array']); //Debug - //$GLOBALS['traverse_array']=""; //Debug - - //Now, build the question_calculated_options record structure - $calculated_options->questionid = $new_question_id; - $calculated_options->synchronize = backup_todb($cal_info['#']['SYNCHRONIZE']['0']['#']); - $calculated_options->single = backup_todb($cal_info['#']['SINGLE']['0']['#']); - $calculated_options->shuffleanswers = isset($cal_info['#']['SHUFFLEANSWERS']['0']['#'])?backup_todb($mul_info['#']['SHUFFLEANSWERS']['0']['#']):''; - $calculated_options->correctfeedback = backup_todb($cal_info['#']['CORRECTFEEDBACK']['0']['#']); - $calculated_options->partiallycorrectfeedback = backup_todb($cal_info['#']['PARTIALLYCORRECTFEEDBACK']['0']['#']); - $calculated_options->incorrectfeedback = backup_todb($cal_info['#']['INCORRECTFEEDBACK']['0']['#']); - $calculated_options->answernumbering = backup_todb($cal_info['#']['ANSWERNUMBERING']['0']['#']); - - //The structure is equal to the db, so insert the question_calculated_options - $newid = $DB->insert_record ("question_calculated_options",$calculated_options); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - } - } - //Now restore numerical_units - $status = question_restore_numerical_units ($old_question_id,$new_question_id,$cal_info,$restore); - $status = question_restore_numerical_options($old_question_id,$new_question_id,$info,$restore); - //Now restore dataset_definitions - if ($status && $newid) { - $status = question_restore_dataset_definitions ($old_question_id,$new_question_id,$cal_info,$restore); - } - - if (!$newid) { - $status = false; - } - } - - return $status; - } - /** * Runs all the code required to set up and save an essay question for testing purposes. * Alternate DB table prefix may be used to facilitate data deletion. diff --git a/question/type/calculatedmulti/backup/moodle2/backup_qtype_calculatedmulti_plugin.class.php b/question/type/calculatedmulti/backup/moodle2/backup_qtype_calculatedmulti_plugin.class.php index 26f2443a7874b..c71382d2cbf2e 100644 --- a/question/type/calculatedmulti/backup/moodle2/backup_qtype_calculatedmulti_plugin.class.php +++ b/question/type/calculatedmulti/backup/moodle2/backup_qtype_calculatedmulti_plugin.class.php @@ -28,12 +28,4 @@ /** * Provides the information to backup calculatedmulti questions */ -class backup_qtype_calculatedmulti_plugin extends backup_qtype_calculated_plugin { - - /** - * overwrite this with the current qtype - */ - protected function get_qtype_name() { - return 'calculatedmulti'; - } -} +class backup_qtype_calculatedmulti_plugin extends backup_qtype_calculated_plugin {} diff --git a/question/type/calculatedmulti/backup/moodle2/restore_qtype_calculatedmulti_plugin.class.php b/question/type/calculatedmulti/backup/moodle2/restore_qtype_calculatedmulti_plugin.class.php new file mode 100644 index 0000000000000..b2183a0a04a95 --- /dev/null +++ b/question/type/calculatedmulti/backup/moodle2/restore_qtype_calculatedmulti_plugin.class.php @@ -0,0 +1,61 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/question/type/calculated/backup/moodle2/restore_qtype_calculated_plugin.class.php'); + +/** + * restore plugin class that provides the necessary information + * needed to restore one calculatedmulti qtype plugin + */ +class restore_qtype_calculatedmulti_plugin extends restore_qtype_calculated_plugin { + + /** + * Given one question_states record, return the answer + * recoded pointing to all the restored stuff for calculatedmulti questions + * + * answer format is datasetxx-yy:zz, where xx is the itemnumber in the dataset + * (doesn't need conversion), and both yy and zz are two (hypen speparated) + * lists of comma separated question_answers, the first to specify the order + * of the answers and the second to specify the responses. + * + * in fact, this qtype behaves exactly like the multichoice one, so we'll delegate + * recoding of those yy:zz to it + */ + public function recode_state_answer($state) { + $answer = $state->answer; + $result = ''; + // datasetxx-yy:zz format + if (preg_match('~^dataset([0-9]+)-(.*)$~', $answer, $matches)) { + $itemid = $matches[1]; + $subanswer = $matches[2]; + // Delegate subanswer recode to multichoice qtype, faking one question_states record + $substate = new stdClass(); + $substate->answer = $subanswer; + $newanswer = $this->step->restore_recode_answer($substate, 'multichoice'); + $result = 'dataset' . $itemid . '-' . $newanswer; + } + return $result ? $result : $answer; + } +} diff --git a/question/type/calculatedsimple/backup/moodle2/backup_qtype_calculatedsimple_plugin.class.php b/question/type/calculatedsimple/backup/moodle2/backup_qtype_calculatedsimple_plugin.class.php index 9efbe5aebf8cc..1095bfdeedd18 100644 --- a/question/type/calculatedsimple/backup/moodle2/backup_qtype_calculatedsimple_plugin.class.php +++ b/question/type/calculatedsimple/backup/moodle2/backup_qtype_calculatedsimple_plugin.class.php @@ -28,12 +28,4 @@ /** * Provides the information to backup calculatedsimple questions */ -class backup_qtype_calculatedsimple_plugin extends backup_qtype_calculated_plugin { - - /** - * overwrite this with the current qtype - */ - protected function get_qtype_name() { - return 'calculatedsimple'; - } -} +class backup_qtype_calculatedsimple_plugin extends backup_qtype_calculated_plugin {} diff --git a/question/type/calculatedsimple/backup/moodle2/restore_qtype_calculatedsimple_plugin.class.php b/question/type/calculatedsimple/backup/moodle2/restore_qtype_calculatedsimple_plugin.class.php new file mode 100644 index 0000000000000..930c7ffc20c08 --- /dev/null +++ b/question/type/calculatedsimple/backup/moodle2/restore_qtype_calculatedsimple_plugin.class.php @@ -0,0 +1,32 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/question/type/calculated/backup/moodle2/restore_qtype_calculated_plugin.class.php'); + +/** + * restore plugin class that provides the necessary information + * needed to restore one calculatedsimple qtype plugin + */ +class restore_qtype_calculatedsimple_plugin extends restore_qtype_calculated_plugin {} diff --git a/question/type/essay/backup/moodle2/restore_qtype_essay_plugin.class.php b/question/type/essay/backup/moodle2/restore_qtype_essay_plugin.class.php new file mode 100644 index 0000000000000..554443dca3e45 --- /dev/null +++ b/question/type/essay/backup/moodle2/restore_qtype_essay_plugin.class.php @@ -0,0 +1,49 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one essay qtype plugin + */ +class restore_qtype_essay_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // This qtype uses question_answers, add them + // Crazy we use answers to store feedback! + $this->add_question_question_answers($paths); + + // Add own qtype stuff + // essay qtype has not own structures (but the question_answers use above) + + return $paths; // And we return the interesting paths + } +} diff --git a/question/type/match/backup/moodle2/backup_qtype_match_plugin.class.php b/question/type/match/backup/moodle2/backup_qtype_match_plugin.class.php index c5202d2f0f6b5..5ea9e66324e82 100644 --- a/question/type/match/backup/moodle2/backup_qtype_match_plugin.class.php +++ b/question/type/match/backup/moodle2/backup_qtype_match_plugin.class.php @@ -64,4 +64,15 @@ protected function define_question_plugin_structure() { return $plugin; } + + /** + * Returns one array with filearea => mappingname elements for the qtype + * + * Used by {@link get_components_and_fileareas} to know about all the qtype + * files to be processed both in backup and restore. + */ + public static function get_qtype_fileareas() { + return array( + 'subquestion' => 'question_match_sub'); + } } diff --git a/question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php b/question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php new file mode 100644 index 0000000000000..5560852265b1d --- /dev/null +++ b/question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php @@ -0,0 +1,171 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one match qtype plugin + */ +class restore_qtype_match_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // Add own qtype stuff + $elename = 'matchoptions'; + $elepath = $this->get_pathfor('/matchoptions'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + $elename = 'match'; + $elepath = $this->get_pathfor('/matches/match'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + + return $paths; // And we return the interesting paths + } + + /** + * Process the qtype/matchoptions element + */ + public function process_matchoptions($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_match too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + // Keep question_match->subquestions unmodified + // after_execute_question() will perform the remapping once all subquestions + // have been created + // Insert record + $newitemid = $DB->insert_record('question_match', $data); + // Create mapping + $this->set_mapping('question_match', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } + + /** + * Process the qtype/matches/match element + */ + public function process_match($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_match_sub too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + // Insert record + $newitemid = $DB->insert_record('question_match_sub', $data); + // Create mapping (there are files and states based on this) + $this->set_mapping('question_match_sub', $oldid, $newitemid); + + // match questions require mapping of question_match_sub, because + // they are used by question_states->answer + } else { + // Look for matching subquestion (by question, questiontext and answertext) + $sub = $DB->get_record_select('question_match_sub', + 'question = ? AND '.$DB->sql_compare_text('questiontext').' = '.$DB->sql_compare_text('?').' AND answertext = ?', + array($newquestionid, $data->questiontext, $data->answertext), 'id', IGNORE_MULTIPLE); + // Found, let's create the mapping + if ($sub) { + $this->set_mapping('question_match_sub', $oldid, $sub->id); + // Something went really wrong, cannot map subquestion for one match question + } else { + throw restore_step_exception('error_question_match_sub_missing_in_db', $data); + } + } + } + + /** + * This method is executed once the whole restore_structure_step, + * more exactly ({@link restore_create_categories_and_questions}) + * has ended processing the whole xml structure. Its name is: + * "after_execute_" + connectionpoint ("question") + * + * For match qtype we use it to restore the subquestions column, + * containing one list of question_match_sub ids + */ + public function after_execute_question() { + global $DB; + // Now that all the question_match_subs have been restored, let's process + // the created question_match subquestions (list of question_match_sub ids) + $rs = $DB->get_recordset_sql("SELECT qm.id, qm.subquestions + FROM {question_match} qm + JOIN {backup_ids_temp} bi ON bi.newitemid = qm.question + WHERE bi.backupid = ? + AND bi.itemname = 'question_created'", array($this->get_restoreid())); + foreach ($rs as $rec) { + $subquestionsarr = explode(',', $rec->subquestions); + foreach ($subquestionsarr as $key => $subquestion) { + $subquestionsarr[$key] = $this->get_mappingid('question_match_sub', $subquestion); + } + $subquestions = implode(',', $subquestionsarr); + $DB->set_field('question_match', 'subquestions', $subquestions, array('id' => $rec->id)); + } + $rs->close(); + } + + /** + * Given one question_states record, return the answer + * recoded pointing to all the restored stuff for match questions + * + * answer is one comma separated list of hypen separated pairs + * containing question_match_sub->id and question_match_sub->code + */ + public function recode_state_answer($state) { + $answer = $state->answer; + $resultarr = array(); + foreach (explode(',', $answer) as $pair) { + $pairarr = explode('-', $pair); + $id = $pairarr[0]; + $code = $pairarr[1]; + $newid = $this->get_mappingid('question_match_sub', $id); + $resultarr[] = implode('-', array($newid, $code)); + } + return implode(',', $resultarr); + } +} diff --git a/question/type/match/questiontype.php b/question/type/match/questiontype.php index b4ea788c67309..0756590cf7c8c 100644 --- a/question/type/match/questiontype.php +++ b/question/type/match/questiontype.php @@ -483,177 +483,6 @@ function get_random_guess_score($question) { return 1 / count($question->options->subquestions); } -/// RESTORE FUNCTIONS ///////////////// - - /* - * Restores the data in the question - * - * This is used in question/restorelib.php - */ - function restore($old_question_id,$new_question_id,$info,$restore) { - global $DB; - $status = true; - - //Get the matchs array - $matchs = $info['#']['MATCHS']['0']['#']['MATCH']; - - //We have to build the subquestions field (a list of match_sub id) - $subquestions_field = ""; - $in_first = true; - - //Iterate over matchs - for($i = 0; $i < sizeof($matchs); $i++) { - $mat_info = $matchs[$i]; - - //We'll need this later!! - $oldid = backup_todb($mat_info['#']['ID']['0']['#']); - - //Now, build the question_match_SUB record structure - $match_sub = new stdClass; - $match_sub->question = $new_question_id; - $match_sub->code = isset($mat_info['#']['CODE']['0']['#'])?backup_todb($mat_info['#']['CODE']['0']['#']):''; - if (!$match_sub->code) { - $match_sub->code = $oldid; - } - $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']); - $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']); - - //The structure is equal to the db, so insert the question_match_sub - $newid = $DB->insert_record ("question_match_sub",$match_sub); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if ($newid) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"question_match_sub",$oldid, - $newid); - //We have a new match_sub, append it to subquestions_field - if ($in_first) { - $subquestions_field .= $newid; - $in_first = false; - } else { - $subquestions_field .= ",".$newid; - } - } else { - $status = false; - } - } - - //We have created every match_sub, now create the match - $match = new stdClass; - $match->question = $new_question_id; - $match->subquestions = $subquestions_field; - - // Get the shuffleanswers option, if it is there. - if (!empty($info['#']['MATCHOPTIONS']['0']['#']['SHUFFLEANSWERS'])) { - $match->shuffleanswers = backup_todb($info['#']['MATCHOPTIONS']['0']['#']['SHUFFLEANSWERS']['0']['#']); - } else { - $match->shuffleanswers = 1; - } - - //The structure is equal to the db, so insert the question_match_sub - $newid = $DB->insert_record ("question_match",$match); - - if (!$newid) { - $status = false; - } - - return $status; - } - - function restore_map($old_question_id,$new_question_id,$info,$restore) { - global $DB; - $status = true; - - //Get the matchs array - $matchs = $info['#']['MATCHS']['0']['#']['MATCH']; - - //We have to build the subquestions field (a list of match_sub id) - $subquestions_field = ""; - $in_first = true; - - //Iterate over matchs - for($i = 0; $i < sizeof($matchs); $i++) { - $mat_info = $matchs[$i]; - - //We'll need this later!! - $oldid = backup_todb($mat_info['#']['ID']['0']['#']); - - //Now, build the question_match_SUB record structure - $match_sub->question = $new_question_id; - $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']); - $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']); - - //If we are in this method is because the question exists in DB, so its - //match_sub must exist too. - //Now, we are going to look for that match_sub in DB and to create the - //mappings in backup_ids to use them later where restoring states (user level). - - //Get the match_sub from DB (by question, questiontext and answertext) - $db_match_sub = $DB->get_record ("question_match_sub",array("question"=>$new_question_id, - "questiontext"=>$match_sub->questiontext, - "answertext"=>$match_sub->answertext)); - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - //We have the database match_sub, so update backup_ids - if ($db_match_sub) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"question_match_sub",$oldid, - $db_match_sub->id); - } else { - $status = false; - } - } - - return $status; - } - - function restore_recode_answer($state, $restore) { - - //The answer is a comma separated list of hypen separated math_subs (for question and answer) - $answer_field = ""; - $in_first = true; - $tok = strtok($state->answer,","); - while ($tok) { - //Extract the match_sub for the question and the answer - $exploded = explode("-",$tok); - $match_question_id = $exploded[0]; - $match_answer_id = $exploded[1]; - //Get the match_sub from backup_ids (for the question) - if (!$match_que = backup_getid($restore->backup_unique_code,"question_match_sub",$match_question_id)) { - echo 'Could not recode question in question_match_sub '.$match_question_id.'
    '; - } else { - if ($in_first) { - $in_first = false; - } else { - $answer_field .= ','; - } - $answer_field .= $match_que->new_id.'-'.$match_answer_id; - } - //check for next - $tok = strtok(","); - } - return $answer_field; - } - /** * Decode links in question type specific tables. * @return bool success or failure. diff --git a/question/type/multianswer/backup/moodle2/restore_qtype_multianswer_plugin.class.php b/question/type/multianswer/backup/moodle2/restore_qtype_multianswer_plugin.class.php new file mode 100644 index 0000000000000..dbfd5fe9e5e25 --- /dev/null +++ b/question/type/multianswer/backup/moodle2/restore_qtype_multianswer_plugin.class.php @@ -0,0 +1,148 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one multianswer qtype plugin + */ +class restore_qtype_multianswer_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // This qtype uses question_answers, add them + $this->add_question_question_answers($paths); + + // Add own qtype stuff + $elename = 'multianswer'; + $elepath = $this->get_pathfor('/multianswer'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + + return $paths; // And we return the interesting paths + } + + /** + * Process the qtype/multianswer element + */ + public function process_multianswer($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_multianswer too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + // Note: multianswer->sequence is a list of question->id values. We aren't + // recoding them here (because some questions can be missing yet). Instead + // we'll perform the recode in the {@link after_execute} method of the plugin + // that gets executed once all questions have been created + // Insert record + $newitemid = $DB->insert_record('question_multianswer', $data); + // Create mapping (need it for after_execute recode of sequence) + $this->set_mapping('question_multianswer', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } + + /** + * This method is executed once the whole restore_structure_step + * this step is part of ({@link restore_create_categories_and_questions}) + * has ended processing the whole xml structure. Its name is: + * "after_execute_" + connectionpoint ("question") + * + * For multianswer qtype we use it to restore the sequence column, + * containing one list of question ids + */ + public function after_execute_question() { + global $DB; + // Now that all the questions have been restored, let's process + // the created question_multianswer sequences (list of question ids) + $rs = $DB->get_recordset_sql("SELECT qma.id, qma.sequence + FROM {question_multianswer} qma + JOIN {backup_ids_temp} bi ON bi.newitemid = qma.question + WHERE bi.backupid = ? + AND bi.itemname = 'question_created'", array($this->get_restoreid())); + foreach ($rs as $rec) { + $sequencearr = explode(',', $rec->sequence); + foreach ($sequencearr as $key => $question) { + $sequencearr[$key] = $this->get_mappingid('question', $question); + } + $sequence = implode(',', $sequencearr); + $DB->set_field('question_multianswer', 'sequence', $sequence, array('id' => $rec->id)); + } + $rs->close(); + } + + /** + * Given one question_states record, return the answer + * recoded pointing to all the restored stuff for multianswer questions + * + * answer is one comma separated list of hypen separated pairs + * containing sequence (pointing to questions sequence in question_multianswer) + * and mixed answers. We'll delegate + * the recoding of answers to the proper qtype + */ + public function recode_state_answer($state) { + global $DB; + $answer = $state->answer; + $resultarr = array(); + // Get sequence of questions + $sequence = $DB->get_field('question_multianswer', 'sequence', array('question' => $state->question)); + $sequencearr = explode(',', $sequence); + // Let's process each pair + foreach (explode(',', $answer) as $pair) { + $pairarr = explode('-', $pair); + $sequenceid = $pairarr[0]; + $subanswer = $pairarr[1]; + // Calculate the questionid based on sequenceid + // Note it is already one *new* questionid that doesn't need mapping + $questionid = $sequencearr[$sequenceid-1]; + // Fetch qtype of the question (needed for delegation) + $questionqtype = $DB->get_field('question', 'qtype', array('id' => $questionid)); + // Delegate subanswer recode to proper qtype, faking one question_states record + $substate = new stdClass(); + $substate->question = $questionid; + $substate->answer = $subanswer; + $newanswer = $this->step->restore_recode_answer($substate, $questionqtype); + $resultarr[] = implode('-', array($sequenceid, $newanswer)); + } + return implode(',', $resultarr); + } + +} diff --git a/question/type/multianswer/questiontype.php b/question/type/multianswer/questiontype.php index 62e2cb653fdc6..38beac0ac2f6f 100644 --- a/question/type/multianswer/questiontype.php +++ b/question/type/multianswer/questiontype.php @@ -673,173 +673,6 @@ function get_random_guess_score($question) { return $totalfraction / count($question->options->questions); } -/// RESTORE FUNCTIONS ///////////////// - - /* - * Restores the data in the question - * - * This is used in question/restorelib.php - */ - function restore($old_question_id,$new_question_id,$info,$restore) { - global $DB; - - $status = true; - - //Get the multianswers array - $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER']; - //Iterate over multianswers - for($i = 0; $i < sizeof($multianswers); $i++) { - $mul_info = $multianswers[$i]; - - //We need this later - $oldid = backup_todb($mul_info['#']['ID']['0']['#']); - - //Now, build the question_multianswer record structure - $multianswer = new stdClass; - $multianswer->question = $new_question_id; - $multianswer->sequence = backup_todb($mul_info['#']['SEQUENCE']['0']['#']); - - //We have to recode the sequence field (a list of question ids) - //Extracts question id from sequence - $sequence_field = ""; - $in_first = true; - $tok = strtok($multianswer->sequence,","); - while ($tok) { - //Get the answer from backup_ids - $question = backup_getid($restore->backup_unique_code,"question",$tok); - if ($question) { - if ($in_first) { - $sequence_field .= $question->new_id; - $in_first = false; - } else { - $sequence_field .= ",".$question->new_id; - } - } - //check for next - $tok = strtok(","); - } - //We have the answers field recoded to its new ids - $multianswer->sequence = $sequence_field; - //The structure is equal to the db, so insert the question_multianswer - $newid = $DB->insert_record("question_multianswer", $multianswer); - - //Save ids in backup_ids - if ($newid) { - backup_putid($restore->backup_unique_code,"question_multianswer", - $oldid, $newid); - } - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - } - - return $status; - } - - function restore_map($old_question_id,$new_question_id,$info,$restore) { - global $DB; - - $status = true; - - //Get the multianswers array - $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER']; - //Iterate over multianswers - for($i = 0; $i < sizeof($multianswers); $i++) { - $mul_info = $multianswers[$i]; - - //We need this later - $oldid = backup_todb($mul_info['#']['ID']['0']['#']); - - //Now, build the question_multianswer record structure - $multianswer->question = $new_question_id; - $multianswer->answers = backup_todb($mul_info['#']['ANSWERS']['0']['#']); - $multianswer->positionkey = backup_todb($mul_info['#']['POSITIONKEY']['0']['#']); - $multianswer->answertype = backup_todb($mul_info['#']['ANSWERTYPE']['0']['#']); - $multianswer->norm = backup_todb($mul_info['#']['NORM']['0']['#']); - - //If we are in this method is because the question exists in DB, so its - //multianswer must exist too. - //Now, we are going to look for that multianswer in DB and to create the - //mappings in backup_ids to use them later where restoring states (user level). - - //Get the multianswer from DB (by question and positionkey) - $db_multianswer = $DB->get_record ("question_multianswer",array("question"=>$new_question_id, - "positionkey"=>$multianswer->positionkey)); - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - //We have the database multianswer, so update backup_ids - if ($db_multianswer) { - //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"question_multianswer",$oldid, - $db_multianswer->id); - } else { - $status = false; - } - } - - return $status; - } - - function restore_recode_answer($state, $restore) { - global $DB, $OUTPUT; - //The answer is a comma separated list of hypen separated sequence number and answers. We may have to recode the answers - $answer_field = ""; - $in_first = true; - $tok = strtok($state->answer,","); - while ($tok) { - //Extract the multianswer_id and the answer - $exploded = explode("-",$tok); - $seqnum = $exploded[0]; - $answer = $exploded[1]; - // $sequence is an ordered array of the question ids. - if (!$sequence = $DB->get_field('question_multianswer', 'sequence', array('question' => $state->question))) { - print_error('missingoption', 'question', '', $state->question); - } - $sequence = explode(',', $sequence); - // The id of the current question. - $wrappedquestionid = $sequence[$seqnum-1]; - // now we can find the question - if (!$wrappedquestion = $DB->get_record('question', array('id' => $wrappedquestionid))) { - echo $OUTPUT->notification("Can't find the subquestion $wrappedquestionid that is used as part $seqnum in cloze question $state->question"); - } - // For multichoice question we need to recode the answer - if ($answer and $wrappedquestion->qtype == 'multichoice') { - //The answer is an answer_id, look for it in backup_ids - if (!$ans = backup_getid($restore->backup_unique_code,"question_answers",$answer)) { - echo 'Could not recode cloze multichoice answer '.$answer.'
    '; - } - $answer = $ans->new_id; - } - //build the new answer field for each pair - if ($in_first) { - $answer_field .= $seqnum."-".$answer; - $in_first = false; - } else { - $answer_field .= ",".$seqnum."-".$answer; - } - //check for next - $tok = strtok(","); - } - return $answer_field; - } - /** * Runs all the code required to set up and save an essay question for testing purposes. * Alternate DB table prefix may be used to facilitate data deletion. diff --git a/question/type/multichoice/backup/moodle2/backup_qtype_multichoice_plugin.class.php b/question/type/multichoice/backup/moodle2/backup_qtype_multichoice_plugin.class.php index d9dd3e123bb74..0134a1ca42210 100644 --- a/question/type/multichoice/backup/moodle2/backup_qtype_multichoice_plugin.class.php +++ b/question/type/multichoice/backup/moodle2/backup_qtype_multichoice_plugin.class.php @@ -62,4 +62,24 @@ protected function define_question_plugin_structure() { return $plugin; } + + /** + * Returns one array with filearea => mappingname elements for the qtype + * + * Used by {@link get_components_and_fileareas} to know about all the qtype + * files to be processed both in backup and restore. + */ + public static function get_qtype_fileareas() { + // TODO: Discuss. Commented below are the "in theory" correct + // mappings for those fileareas. Instead we are using question for + // them, that will cause problems in the future if we want to change + // any of them to be 1..n (i.e. we should be always pointing to own id) + return array( + //'correctfeedback' => 'question_multichoice', + //'partiallycorrectfeedback' => 'question_multichoice', + //'incorrectfeedback' => 'question_multichoice'); + 'correctfeedback' => 'question_created', + 'partiallycorrectfeedback' => 'question_created', + 'incorrectfeedback' => 'question_created'); + } } diff --git a/question/type/multichoice/backup/moodle2/restore_qtype_multichoice_plugin.class.php b/question/type/multichoice/backup/moodle2/restore_qtype_multichoice_plugin.class.php new file mode 100644 index 0000000000000..2950b49a74f9c --- /dev/null +++ b/question/type/multichoice/backup/moodle2/restore_qtype_multichoice_plugin.class.php @@ -0,0 +1,124 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one multichoice qtype plugin + */ +class restore_qtype_multichoice_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // This qtype uses question_answers, add them + $this->add_question_question_answers($paths); + + // Add own qtype stuff + $elename = 'multichoice'; + $elepath = $this->get_pathfor('/multichoice'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + + return $paths; // And we return the interesting paths + } + + /** + * Process the qtype/multichoice element + */ + public function process_multichoice($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_multichoice too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + // Map sequence of question_answer ids + $answersarr = explode(',', $data->answers); + foreach ($answersarr as $key => $answer) { + $answersarr[$key] = $this->get_mappingid('question_answer', $answer); + } + $data->answers = implode(',', $answersarr); + // Insert record + $newitemid = $DB->insert_record('question_multichoice', $data); + // Create mapping (not needed, no files nor childs nor states here) + //$this->set_mapping('question_multichoice', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } + + /** + * Given one question_states record, return the answer + * recoded pointing to all the restored stuff for multichoice questions + * + * answer are two (hypen speparated) lists of comma separated question_answers + * the first to specify the order of the answers and the second to specify the + * responses. Note the order list (the first one) can be optional + */ + public function recode_state_answer($state) { + $answer = $state->answer; + $orderarr = array(); + $responsesarr = array(); + $lists = explode(':', $answer); + // if only 1 list, answer is missing the order list, adjust + if (count($lists) == 1) { + $lists[1] = $lists[0]; // here we have the responses + $lists[0] = ''; // here we have the order + } + // Map order + foreach (explode(',', $lists[0]) as $id) { + if ($newid = $this->get_mappingid('question_answer', $id)) { + $orderarr[] = $newid; + } + } + // Map responses + foreach (explode(',', $lists[1]) as $id) { + if ($newid = $this->get_mappingid('question_answer', $id)) { + $responsesarr[] = $newid; + } + } + // Build the final answer, if not order, only responses + $result = ''; + if (empty($orderarr)) { + $result = implode(',', $responsesarr); + } else { + $result = implode(',', $orderarr) . ':' . implode(',', $responsesarr); + } + return $result; + } +} diff --git a/question/type/multichoice/questiontype.php b/question/type/multichoice/questiontype.php index 772b37ace0c1b..49c669c272011 100644 --- a/question/type/multichoice/questiontype.php +++ b/question/type/multichoice/questiontype.php @@ -420,134 +420,6 @@ function get_random_guess_score($question) { return $totalfraction / count($question->options->answers); } - /// RESTORE FUNCTIONS ///////////////// - - /* - * Restores the data in the question - * - * This is used in question/restorelib.php - */ - function restore($old_question_id,$new_question_id,$info,$restore) { - global $DB; - - $status = true; - - //Get the multichoices array - $multichoices = $info['#']['MULTICHOICE']; - - //Iterate over multichoices - for($i = 0; $i < sizeof($multichoices); $i++) { - $mul_info = $multichoices[$i]; - - //Now, build the question_multichoice record structure - $multichoice = new stdClass; - $multichoice->question = $new_question_id; - $multichoice->layout = backup_todb($mul_info['#']['LAYOUT']['0']['#']); - $multichoice->answers = backup_todb($mul_info['#']['ANSWERS']['0']['#']); - $multichoice->single = backup_todb($mul_info['#']['SINGLE']['0']['#']); - $multichoice->shuffleanswers = isset($mul_info['#']['SHUFFLEANSWERS']['0']['#'])?backup_todb($mul_info['#']['SHUFFLEANSWERS']['0']['#']):''; - if (array_key_exists("CORRECTFEEDBACK", $mul_info['#'])) { - $multichoice->correctfeedback = backup_todb($mul_info['#']['CORRECTFEEDBACK']['0']['#']); - } else { - $multichoice->correctfeedback = ''; - } - if (array_key_exists("PARTIALLYCORRECTFEEDBACK", $mul_info['#'])) { - $multichoice->partiallycorrectfeedback = backup_todb($mul_info['#']['PARTIALLYCORRECTFEEDBACK']['0']['#']); - } else { - $multichoice->partiallycorrectfeedback = ''; - } - if (array_key_exists("INCORRECTFEEDBACK", $mul_info['#'])) { - $multichoice->incorrectfeedback = backup_todb($mul_info['#']['INCORRECTFEEDBACK']['0']['#']); - } else { - $multichoice->incorrectfeedback = ''; - } - if (array_key_exists("ANSWERNUMBERING", $mul_info['#'])) { - $multichoice->answernumbering = backup_todb($mul_info['#']['ANSWERNUMBERING']['0']['#']); - } else { - $multichoice->answernumbering = 'abc'; - } - - //We have to recode the answers field (a list of answers id) - //Extracts answer id from sequence - $answers_field = ""; - $in_first = true; - $tok = strtok($multichoice->answers,","); - while ($tok) { - //Get the answer from backup_ids - $answer = backup_getid($restore->backup_unique_code,"question_answers",$tok); - if ($answer) { - if ($in_first) { - $answers_field .= $answer->new_id; - $in_first = false; - } else { - $answers_field .= ",".$answer->new_id; - } - } - //check for next - $tok = strtok(","); - } - //We have the answers field recoded to its new ids - $multichoice->answers = $answers_field; - - //The structure is equal to the db, so insert the question_shortanswer - $newid = $DB->insert_record ("question_multichoice",$multichoice); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - function restore_recode_answer($state, $restore) { - $pos = strpos($state->answer, ':'); - $order = array(); - $responses = array(); - if (false === $pos) { // No order of answers is given, so use the default - if ($state->answer) { - $responses = explode(',', $state->answer); - } - } else { - $order = explode(',', substr($state->answer, 0, $pos)); - if ($responsestring = substr($state->answer, $pos + 1)) { - $responses = explode(',', $responsestring); - } - } - if ($order) { - foreach ($order as $key => $oldansid) { - $answer = backup_getid($restore->backup_unique_code,"question_answers",$oldansid); - if ($answer) { - $order[$key] = $answer->new_id; - } else { - echo 'Could not recode multichoice answer id '.$oldansid.' for state '.$state->oldid.'
    '; - } - } - } - if ($responses) { - foreach ($responses as $key => $oldansid) { - $answer = backup_getid($restore->backup_unique_code,"question_answers",$oldansid); - if ($answer) { - $responses[$key] = $answer->new_id; - } else { - echo 'Could not recode multichoice response answer id '.$oldansid.' for state '.$state->oldid.'
    '; - } - } - } - return implode(',', $order).':'.implode(',', $responses); - } - /** * Decode links in question type specific tables. * @return bool success or failure. diff --git a/question/type/numerical/backup/moodle2/backup_qtype_numerical_plugin.class.php b/question/type/numerical/backup/moodle2/backup_qtype_numerical_plugin.class.php index 06fff61b72987..c2f3426bcc8b3 100644 --- a/question/type/numerical/backup/moodle2/backup_qtype_numerical_plugin.class.php +++ b/question/type/numerical/backup/moodle2/backup_qtype_numerical_plugin.class.php @@ -68,4 +68,20 @@ protected function define_question_plugin_structure() { return $plugin; } + + /** + * Returns one array with filearea => mappingname elements for the qtype + * + * Used by {@link get_components_and_fileareas} to know about all the qtype + * files to be processed both in backup and restore. + */ + public static function get_qtype_fileareas() { + // TODO: Discuss. Commented below are the "in theory" correct + // mappings for those fileareas. Instead we are using question for + // them, that will cause problems in the future if we want to change + // any of them to be 1..n (i.e. we should be always pointing to own id) + return array( + //'instruction' => 'question_numerical_option'); + 'instruction' => 'question_created'); + } } diff --git a/question/type/numerical/backup/moodle2/restore_qtype_numerical_plugin.class.php b/question/type/numerical/backup/moodle2/restore_qtype_numerical_plugin.class.php new file mode 100644 index 0000000000000..fdda5f5c00289 --- /dev/null +++ b/question/type/numerical/backup/moodle2/restore_qtype_numerical_plugin.class.php @@ -0,0 +1,83 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one numerical qtype plugin + */ +class restore_qtype_numerical_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // This qtype uses question_answers, add them + $this->add_question_question_answers($paths); + + // This qtype uses question_numerical_options and question_numerical_units, add them + $this->add_question_numerical_options($paths); + $this->add_question_numerical_units($paths); + + // Add own qtype stuff + $elename = 'numerical'; + $elepath = $this->get_pathfor('/numerical_records/numerical_record'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + + return $paths; // And we return the interesting paths + } + + /** + * Process the qtype/numerical element + */ + public function process_numerical($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_numerical too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + $data->answer = $this->get_mappingid('question_answer', $data->answer); + // Insert record + $newitemid = $DB->insert_record('question_numerical', $data); + // Create mapping (not needed, no files nor childs nor states here) + //$this->set_mapping('question_numerical', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } +} diff --git a/question/type/numerical/questiontype.php b/question/type/numerical/questiontype.php index 2dc1413687020..02ec82c41a59b 100644 --- a/question/type/numerical/questiontype.php +++ b/question/type/numerical/questiontype.php @@ -1204,69 +1204,6 @@ function valid_unit($rawresponse, $units) { return false; } - /// RESTORE FUNCTIONS ///////////////// - - /** - * Restores the data in the question - * - * This is used in question/restorelib.php - */ - function restore($old_question_id,$new_question_id,$info,$restore) { - global $DB; - - $status = true; - - //Get the numerical array - if (isset($info['#']['NUMERICAL'])) { - $numericals = $info['#']['NUMERICAL']; - } else { - $numericals = array(); - } - - //Iterate over numericals - for($i = 0; $i < sizeof($numericals); $i++) { - $num_info = $numericals[$i]; - - //Now, build the question_numerical record structure - $numerical = new stdClass; - $numerical->question = $new_question_id; - $numerical->answer = backup_todb($num_info['#']['ANSWER']['0']['#']); - $numerical->tolerance = backup_todb($num_info['#']['TOLERANCE']['0']['#']); - - //We have to recode the answer field - $answer = backup_getid($restore->backup_unique_code,"question_answers",$numerical->answer); - if ($answer) { - $numerical->answer = $answer->new_id; - } - - //The structure is equal to the db, so insert the question_numerical - $newid = $DB->insert_record ("question_numerical", $numerical); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - //Now restore numerical_units - $status = question_restore_numerical_units ($old_question_id,$new_question_id,$num_info,$restore); - - //Now restore numerical_options - $status = question_restore_numerical_options ($old_question_id,$new_question_id,$num_info,$restore); - - if (!$newid) { - $status = false; - } - } - - return $status; - } - /** * Runs all the code required to set up and save an essay question for testing purposes. * Alternate DB table prefix may be used to facilitate data deletion. diff --git a/question/type/questiontype.php b/question/type/questiontype.php index aac3e196ad691..9281a7fa71071 100644 --- a/question/type/questiontype.php +++ b/question/type/questiontype.php @@ -1632,46 +1632,6 @@ function error_link($cmoptions) { } } -/// RESTORE FUNCTIONS ///////////////// - - /* - * Restores the data in the question - * - * This is used in question/restorelib.php - */ - function restore($old_question_id,$new_question_id,$info,$restore) { - global $DB; - - $status = true; - $extraquestionfields = $this->extra_question_fields(); - - if (is_array($extraquestionfields)) { - $questionextensiontable = array_shift($extraquestionfields); - $tagname = strtoupper($this->name()); - $recordinfo = $info['#'][$tagname][0]; - - $record = new stdClass; - $qidcolname = $this->questionid_column_name(); - $record->$qidcolname = $new_question_id; - foreach ($extraquestionfields as $field) { - $record->$field = backup_todb($recordinfo['#'][strtoupper($field)]['0']['#']); - } - $DB->insert_record($questionextensiontable, $record); - } - //TODO restore extra data in answers - return $status; - } - - function restore_map($old_question_id,$new_question_id,$info,$restore) { - // There is nothing to decode - return true; - } - - function restore_recode_answer($state, $restore) { - // There is nothing to decode - return $state->answer; - } - /// IMPORT/EXPORT FUNCTIONS ///////////////// /* diff --git a/question/type/random/backup/moodle2/restore_qtype_random_plugin.class.php b/question/type/random/backup/moodle2/restore_qtype_random_plugin.class.php new file mode 100644 index 0000000000000..aaaec820389c3 --- /dev/null +++ b/question/type/random/backup/moodle2/restore_qtype_random_plugin.class.php @@ -0,0 +1,69 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one random qtype plugin + */ +class restore_qtype_random_plugin extends restore_qtype_plugin { + + /** + * Given one question_states record, return the answer + * recoded pointing to all the restored stuff for random questions + * + * answer format is randomxx-yy, with xx being question->id and + * yy the actual response to the question. We'll delegate the recode + * to the corresponding qtype + * + * also, some old states can contain, simply, one question->id, + * support them, just in case + */ + public function recode_state_answer($state) { + global $DB; + + $answer = $state->answer; + $result = ''; + // randomxx-yy answer format + if (preg_match('~^random([0-9]+)-(.*)$~', $answer, $matches)) { + $questionid = $matches[1]; + $subanswer = $matches[2]; + $newquestionid = $this->get_mappingid('question', $questionid); + $questionqtype = $DB->get_field('question', 'qtype', array('id' => $newquestionid)); + // Delegate subanswer recode to proper qtype, faking one question_states record + $substate = new stdClass(); + $substate->question = $newquestionid; + $substate->answer = $subanswer; + $newanswer = $this->step->restore_recode_answer($substate, $questionqtype); + $result = 'random' . $newquestionid . '-' . $newanswer; + + // simple question id format + } else { + $newquestionid = $this->get_mappingid('question', $answer); + $result = $newquestionid; + } + return $result; + } +} diff --git a/question/type/random/questiontype.php b/question/type/random/questiontype.php index fd85751c30e8f..84bcba7734720 100644 --- a/question/type/random/questiontype.php +++ b/question/type/random/questiontype.php @@ -363,49 +363,6 @@ function compare_responses(&$question, $state, $teststate) { ->compare_responses($wrappedquestion, $state, $teststate); } - function restore_recode_answer($state, $restore) { - // The answer looks like 'randomXX-ANSWER', where XX is - // the id of the used question and ANSWER the actual - // response to that question. - // However, there may still be old-style states around, - // which store the id of the wrapped question in the - // state of the random question and store the response - // in a separate state for the wrapped question - - global $QTYPES, $DB; - $answer_field = ""; - - if (preg_match('~^random([0-9]+)-(.*)$~', $state->answer, $answerregs)) { - // Recode the question id in $answerregs[1] - // Get the question from backup_ids - if(!$wrapped = backup_getid($restore->backup_unique_code,"question",$answerregs[1])) { - echo 'Could not recode question in random-'.$answerregs[1].'
    '; - return($answer_field); - } - // Get the question type for recursion - if (!$wrappedquestion->qtype = $DB->get_field('question', 'qtype', array('id' => $wrapped->new_id))) { - echo 'Could not get qtype while recoding question random-'.$answerregs[1].'
    '; - return($answer_field); - } - $newstate = $state; - $newstate->question = $wrapped->new_id; - $newstate->answer = $answerregs[2]; - $answer_field = 'random'.$wrapped->new_id.'-'; - - // Recode the answer field in $answerregs[2] depending on - // the qtype of question with id $answerregs[1] - $answer_field .= $QTYPES[$wrappedquestion->qtype]->restore_recode_answer($newstate, $restore); - } else { - // Handle old-style states - $answer_link = backup_getid($restore->backup_unique_code,"question",$state->answer); - if ($answer_link) { - $answer_field = $answer_link->new_id; - } - } - - return $answer_field; - } - /** * For random question type return empty string which means won't calculate. * @param object $question diff --git a/question/type/randomsamatch/backup/moodle2/restore_qtype_randomsamatch_plugin.class.php b/question/type/randomsamatch/backup/moodle2/restore_qtype_randomsamatch_plugin.class.php new file mode 100644 index 0000000000000..9e4be36279d85 --- /dev/null +++ b/question/type/randomsamatch/backup/moodle2/restore_qtype_randomsamatch_plugin.class.php @@ -0,0 +1,96 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one randomsamatch qtype plugin + */ +class restore_qtype_randomsamatch_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // Add own qtype stuff + $elename = 'randomsamatch'; + $elepath = $this->get_pathfor('/randomsamatch'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + + return $paths; // And we return the interesting paths + } + + /** + * Process the qtype/randomsamatch element + */ + public function process_randomsamatch($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_randomsamatch too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + // Insert record + $newitemid = $DB->insert_record('question_randomsamatch', $data); + // Create mapping + $this->set_mapping('question_randomsamatch', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } + + /** + * Given one question_states record, return the answer + * recoded pointing to all the restored stuff for randomsamatch questions + * + * answer is one comma separated list of hypen separated pairs + * containing question->id and question_answers->id + */ + public function recode_state_answer($state) { + $answer = $state->answer; + $resultarr = array(); + foreach (explode(',', $answer) as $pair) { + $pairarr = explode('-', $pair); + $questionid = $pairarr[0]; + $answerid = $pairarr[1]; + $newquestionid = $questionid ? $this->get_mappingid('question', $questionid) : 0; + $newanswerid = $answerid ? $this->get_mappingid('question_answer', $answerid) : 0; + $resultarr[] = implode('-', array($newquestionid, $newanswerid)); + } + return implode(',', $resultarr); + } +} diff --git a/question/type/randomsamatch/questiontype.php b/question/type/randomsamatch/questiontype.php index 177fde4cf7c25..230d799fa1a3a 100644 --- a/question/type/randomsamatch/questiontype.php +++ b/question/type/randomsamatch/questiontype.php @@ -339,88 +339,6 @@ function get_possible_responses(&$question) { function get_random_guess_score($question) { return 1/$question->options->choose; } - -/// RESTORE FUNCTIONS ///////////////// - - /* - * Restores the data in the question - * - * This is used in question/restorelib.php - */ - function restore($old_question_id,$new_question_id,$info,$restore) { - global $DB; - - $status = true; - - //Get the randomsamatchs array - $randomsamatchs = $info['#']['RANDOMSAMATCH']; - - //Iterate over randomsamatchs - for($i = 0; $i < sizeof($randomsamatchs); $i++) { - $ran_info = $randomsamatchs[$i]; - - //Now, build the question_randomsamatch record structure - $randomsamatch->question = $new_question_id; - $randomsamatch->choose = backup_todb($ran_info['#']['CHOOSE']['0']['#']); - - //The structure is equal to the db, so insert the question_randomsamatch - $newid = $DB->insert_record ("question_randomsamatch",$randomsamatch); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - function restore_recode_answer($state, $restore) { - - //The answer is a comma separated list of hypen separated question_id and answer_id. We must recode them - $answer_field = ""; - $in_first = true; - $tok = strtok($state->answer,","); - while ($tok) { - //Extract the question_id and the answer_id - $exploded = explode("-",$tok); - $question_id = $exploded[0]; - $answer_id = $exploded[1]; - //Get the question from backup_ids - if (!$que = backup_getid($restore->backup_unique_code,"question",$question_id)) { - echo 'Could not recode randomsamatch question '.$question_id.'
    '; - } - - if ($answer_id == 0) { // no response yet - $ans->new_id = 0; - } else { - //Get the answer from backup_ids - if (!$ans = backup_getid($restore->backup_unique_code,"question_answers",$answer_id)) { - echo 'Could not recode randomsamatch answer '.$answer_id.'
    '; - } - } - if ($in_first) { - $answer_field .= $que->new_id."-".$ans->new_id; - $in_first = false; - } else { - $answer_field .= ",".$que->new_id."-".$ans->new_id; - } - //check for next - $tok = strtok(","); - } - return $answer_field; - } - } //// END OF CLASS //// diff --git a/question/type/shortanswer/backup/moodle2/restore_qtype_shortanswer_plugin.class.php b/question/type/shortanswer/backup/moodle2/restore_qtype_shortanswer_plugin.class.php new file mode 100644 index 0000000000000..03202c5f7d0a2 --- /dev/null +++ b/question/type/shortanswer/backup/moodle2/restore_qtype_shortanswer_plugin.class.php @@ -0,0 +1,84 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one shortanswer qtype plugin + */ +class restore_qtype_shortanswer_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // This qtype uses question_answers, add them + $this->add_question_question_answers($paths); + + // Add own qtype stuff + $elename = 'shortanswer'; + $elepath = $this->get_pathfor('/shortanswer'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + + return $paths; // And we return the interesting paths + } + + /** + * Process the qtype/shortanswer element + */ + public function process_shortanswer($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_shortanswer too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + // Map sequence of question_answer ids + $answersarr = explode(',', $data->answers); + foreach ($answersarr as $key => $answer) { + $answersarr[$key] = $this->get_mappingid('question_answer', $answer); + } + $data->answers = implode(',', $answersarr); + // Insert record + $newitemid = $DB->insert_record('question_shortanswer', $data); + // Create mapping + $this->set_mapping('question_shortanswer', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } +} diff --git a/question/type/shortanswer/questiontype.php b/question/type/shortanswer/questiontype.php index 4404bd89a1394..19ee8d277fd05 100644 --- a/question/type/shortanswer/questiontype.php +++ b/question/type/shortanswer/questiontype.php @@ -280,52 +280,6 @@ function get_random_guess_score($question) { return 0; } -/// RESTORE FUNCTIONS ///////////////// - - /* - * Restores the data in the question - * - * This is used in question/restorelib.php - */ - function restore($old_question_id,$new_question_id,$info,$restore) { - global $DB; - - $status = parent::restore($old_question_id, $new_question_id, $info, $restore); - - if ($status) { - $extraquestionfields = $this->extra_question_fields(); - $questionextensiontable = array_shift($extraquestionfields); - - //We have to recode the answers field (a list of answers id) - $questionextradata = $DB->get_record($questionextensiontable, array($this->questionid_column_name() => $new_question_id)); - if (isset($questionextradata->answers)) { - $answers_field = ""; - $in_first = true; - $tok = strtok($questionextradata->answers, ","); - while ($tok) { - // Get the answer from backup_ids - $answer = backup_getid($restore->backup_unique_code,"question_answers",$tok); - if ($answer) { - if ($in_first) { - $answers_field .= $answer->new_id; - $in_first = false; - } else { - $answers_field .= ",".$answer->new_id; - } - } - // Check for next - $tok = strtok(","); - } - // We have the answers field recoded to its new ids - $questionextradata->answers = $answers_field; - // Update the question - $DB->update_record($questionextensiontable, $questionextradata); - } - } - - return $status; - } - /** * Prints the score obtained and maximum score available plus any penalty * information diff --git a/question/type/truefalse/backup/moodle2/restore_qtype_truefalse_plugin.class.php b/question/type/truefalse/backup/moodle2/restore_qtype_truefalse_plugin.class.php new file mode 100644 index 0000000000000..7db3fcb6b2ee1 --- /dev/null +++ b/question/type/truefalse/backup/moodle2/restore_qtype_truefalse_plugin.class.php @@ -0,0 +1,95 @@ +. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one truefalse qtype plugin + */ +class restore_qtype_truefalse_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // This qtype uses question_answers, add them + $this->add_question_question_answers($paths); + + // Add own qtype stuff + $elename = 'truefalse'; + $elepath = $this->get_pathfor('/truefalse'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + + return $paths; // And we return the interesting paths + } + + /** + * Process the qtype/truefalse element + */ + public function process_truefalse($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_truefalse too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + $data->trueanswer = $this->get_mappingid('question_answer', $data->trueanswer); + $data->falseanswer = $this->get_mappingid('question_answer', $data->falseanswer); + // Insert record + $newitemid = $DB->insert_record('question_truefalse', $data); + // Create mapping + $this->set_mapping('question_truefalse', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } + + /** + * Given one question_states record, return the answer + * recoded pointing to all the restored stuff for truefalse questions + * + * if not empty, answer is one question_answers->id + */ + public function recode_state_answer($state) { + $answer = $state->answer; + $result = ''; + if ($answer) { + $result = $this->get_mappingid('question_answer', $answer); + } + return $result; + } +} diff --git a/question/type/truefalse/questiontype.php b/question/type/truefalse/questiontype.php index c2fb46984fa0e..a0935cd5de01c 100644 --- a/question/type/truefalse/questiontype.php +++ b/question/type/truefalse/questiontype.php @@ -292,81 +292,6 @@ function get_random_guess_score($question) { return 0.5; } -/// RESTORE FUNCTIONS ///////////////// - - /* - * Restores the data in the question - * - * This is used in question/restorelib.php - */ - function restore($old_question_id,$new_question_id,$info,$restore) { - global $DB; - - $status = true; - - //Get the truefalse array - if (array_key_exists('TRUEFALSE', $info['#'])) { - $truefalses = $info['#']['TRUEFALSE']; - } else { - $truefalses = array(); - } - - //Iterate over truefalse - for($i = 0; $i < sizeof($truefalses); $i++) { - $tru_info = $truefalses[$i]; - - //Now, build the question_truefalse record structure - $truefalse = new stdClass; - $truefalse->question = $new_question_id; - $truefalse->trueanswer = backup_todb($tru_info['#']['TRUEANSWER']['0']['#']); - $truefalse->falseanswer = backup_todb($tru_info['#']['FALSEANSWER']['0']['#']); - - ////We have to recode the trueanswer field - $answer = backup_getid($restore->backup_unique_code,"question_answers",$truefalse->trueanswer); - if ($answer) { - $truefalse->trueanswer = $answer->new_id; - } - - ////We have to recode the falseanswer field - $answer = backup_getid($restore->backup_unique_code,"question_answers",$truefalse->falseanswer); - if ($answer) { - $truefalse->falseanswer = $answer->new_id; - } - - //The structure is equal to the db, so insert the question_truefalse - $newid = $DB->insert_record ("question_truefalse", $truefalse); - - //Do some output - if (($i+1) % 50 == 0) { - if (!defined('RESTORE_SILENTLY')) { - echo "."; - if (($i+1) % 1000 == 0) { - echo "
    "; - } - } - backup_flush(300); - } - - if (!$newid) { - $status = false; - } - } - - return $status; - } - - function restore_recode_answer($state, $restore) { - //answer may be empty - if ($state->answer) { - $answer = backup_getid($restore->backup_unique_code,"question_answers",$state->answer); - if ($answer) { - return $answer->new_id; - } else { - echo 'Could not recode truefalse answer id '.$state->answer.' for state '.$state->oldid.'
    '; - } - } - } - /** * Runs all the code required to set up and save an essay question for testing purposes. * Alternate DB table prefix may be used to facilitate data deletion.