MDL-22151 & MDL-22138 - Quiz activity backup & questions banks too!
authorEloy Lafuente <stronk7@moodle.org>
Tue, 21 Sep 2010 01:28:30 +0000 (01:28 +0000)
committerEloy Lafuente <stronk7@moodle.org>
Tue, 21 Sep 2010 01:28:30 +0000 (01:28 +0000)
CV S: ----------------------------------------------------------------------

18 files changed:
backup/backup.class.php
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_custom_fields.php
backup/moodle2/backup_final_task.class.php
backup/moodle2/backup_plan_builder.class.php
backup/moodle2/backup_plugin.class.php [new file with mode: 0644]
backup/moodle2/backup_qtype_plugin.class.php [new file with mode: 0644]
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/backup_question_dbops.class.php [new file with mode: 0644]
backup/util/dbops/backup_structure_dbops.class.php
backup/util/helper/backup_helper.class.php
backup/util/includes/backup_includes.php
backup/util/plan/backup_structure_step.class.php
mod/data/field/textarea/field.class.php
mod/quiz/backup/moodle2/backup_quiz_activity_task.class.php [new file with mode: 0644]
mod/quiz/backup/moodle2/backup_quiz_stepslib.php [new file with mode: 0644]
mod/quiz/lib.php

index ba7bce8..75efd31 100644 (file)
@@ -106,8 +106,8 @@ abstract class backup implements checksumable {
     const OPERATION_RESTORE ='restore';// We are performing one restore
 
     // Version (to keep CFG->backup_version (and release) updated automatically)
-    const VERSION = 2010072300;
-    const RELEASE = '2.0 Preview 5';
+    const VERSION = 2010092100;
+    const RELEASE = '2.0 RC1';
 }
 
 /*
index da2f21a..1076dfe 100644 (file)
@@ -78,6 +78,9 @@ class backup_course_task extends backup_task {
         // Annotate the groups used in already annotated groupings
         $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups'));
 
+        // Annotate the question_categories belonging to the course context
+        $this->add_step(new backup_calculate_question_categories('course_question_categories'));
+
         // Generate the roles file (optionally role assignments and always role overrides)
         $this->add_step(new backup_roles_structure_step('course_roles', 'roles.xml'));
 
index af7e07d..d899a65 100644 (file)
@@ -102,6 +102,12 @@ class file_nested_element extends backup_nested_element {
     }
 }
 
+/**
+ * Implementation of backup_optigroup_element to be used by plugins stuff.
+ * Split just for better separation and future specialisation
+ */
+class backup_plugin_element extends backup_optigroup_element { }
+
 /**
  * Implementation of backup_optigroup_element to be used by subplugins stuff.
  * Split just for better separation and future specialisation
index 320442b..93a07c0 100644 (file)
@@ -48,6 +48,16 @@ class backup_final_task extends backup_task {
         // including membership based on setting
         $this->add_step(new backup_groups_structure_step('groups', 'groups.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
+        // complete question banks we don't need to restrict at all and can be
+        // 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
index 9066338..a1cb25c 100644 (file)
@@ -32,6 +32,8 @@ require_once($CFG->dirroot . '/backup/moodle2/backup_final_task.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_block_task.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_default_block_task.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_xml_transformer.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/backup_subplugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_settingslib.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_stepslib.php');
diff --git a/backup/moodle2/backup_plugin.class.php b/backup/moodle2/backup_plugin.class.php
new file mode 100644 (file)
index 0000000..bd10498
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @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 backups
+ *
+ * TODO: Finish phpdocs
+ */
+abstract class backup_plugin {
+
+    protected $plugintype;
+    protected $pluginname;
+    protected $connectionpoint;
+    protected $optigroup; // Optigroup, parent of all optigroup elements
+
+    public function __construct($plugintype, $pluginname, $optigroup) {
+        $this->plugintype = $plugintype;
+        $this->pluginname = $pluginname;
+        $this->optigroup     = $optigroup;
+        $this->connectionpoint = '';
+    }
+
+    public function define_plugin_structure($connectionpoint) {
+
+        $this->connectionpoint = $connectionpoint;
+
+        $methodname = 'define_' . $connectionpoint . '_plugin_structure';
+
+        if (method_exists($this, $methodname)) {
+            $this->$methodname();
+        }
+    }
+
+    /**
+     * Factory method that will return one backup_plugin_element (backup_optigroup_element)
+     * with its name automatically calculated, based one the plugin being handled (type, name)
+     */
+    protected function get_plugin_element($final_elements = null, $conditionparam = null, $conditionvalue = null) {
+        // Something exclusive for this backup_plugin_element (backup_optigroup_element)
+        // because it hasn't XML representation
+        $name = 'optigroup_' . $this->plugintype . '_' . $this->pluginname . '_' . $this->connectionpoint;
+        $optigroup_element = new backup_plugin_element($name, $final_elements, $conditionparam, $conditionvalue);
+        $this->optigroup->add_child($optigroup_element);  // Add optigroup_element to stay connected since beginning
+        return $optigroup_element;
+    }
+
+    /**
+     * Simple helper function that suggests one name for the main nested element in plugins
+     * It's not mandatory to use it but recommended ;-)
+     */
+    protected function get_recommended_name() {
+        return 'plugin_' . $this->plugintype . '_' . $this->pluginname . '_' . $this->connectionpoint;
+    }
+
+}
diff --git a/backup/moodle2/backup_qtype_plugin.class.php b/backup/moodle2/backup_qtype_plugin.class.php
new file mode 100644 (file)
index 0000000..e44cf39
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @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 backup_plugin in order to implement some
+ * helper methods related with the questions (qtype plugin)
+ *
+ * TODO: Finish phpdocs
+ */
+abstract class backup_qtype_plugin extends backup_plugin {
+
+    /**
+     * Attach to $element (usually questions) the needed backup structures
+     * for question_answers for a given question
+     * Used by various qtypes (calculated, essay, multianswer,
+     * multichoice, numerical, shortanswer, truefalse)
+     */
+    protected function add_question_question_answers($element) {
+        // Check $element is one nested_backup_element
+        if (! $element instanceof backup_nested_element) {
+            throw new backup_step_exception('question_answers_bad_parent_element', $element);
+        }
+
+        // Define the elements
+        $answers = new backup_nested_element('answers');
+        $answer = new backup_nested_element('answer', array('id'), array(
+            'answertext', 'answerformat', 'fraction', 'feedback',
+            'feedbackformat'));
+
+        // Build the tree
+        $element->add_child($answers);
+        $answers->add_child($answer);
+
+        // Set the sources
+        $answer->set_source_table('question_answers', array('question' => backup::VAR_PARENTID));
+
+        // Aliases
+        $answer->set_source_alias('answer', 'answertext');
+
+        // don't need to annotate ids nor files
+    }
+
+    /**
+     * Attach to $element (usually questions) the needed backup structures
+     * for question_numerical_units for a given question
+     * Used both by calculated and numerical qtypes
+     */
+    protected function add_question_numerical_units($element) {
+        // Check $element is one nested_backup_element
+        if (! $element instanceof backup_nested_element) {
+            throw new backup_step_exception('question_numerical_units_bad_parent_element', $element);
+        }
+
+        // Define the elements
+        $units = new backup_nested_element('numerical_units');
+        $unit = new backup_nested_element('numerical_unit', array('id'), array(
+            'multiplier', 'unit'));
+
+        // Build the tree
+        $element->add_child($units);
+        $units->add_child($unit);
+
+        // Set the sources
+        $unit->set_source_table('question_numerical_units', array('question' => backup::VAR_PARENTID));
+
+        // don't need to annotate ids nor files
+    }
+
+    /**
+     * Attach to $element (usually questions) the needed backup structures
+     * for question_numerical_options for a given question
+     * Used both by calculated and numerical qtypes
+     */
+    protected function add_question_numerical_options($element) {
+        // Check $element is one nested_backup_element
+        if (! $element instanceof backup_nested_element) {
+            throw new backup_step_exception('question_numerical_options_bad_parent_element', $element);
+        }
+
+        // Define the elements
+        $options = new backup_nested_element('numerical_options');
+        $option = new backup_nested_element('numerical_option', array('id'), array(
+            'instructions', 'instructionsformat', 'showunits', 'unitsleft',
+            'unitgradingtype', 'unitpenalty'));
+
+        // Build the tree
+        $element->add_child($options);
+        $options->add_child($option);
+
+        // Set the sources
+        $option->set_source_table('question_numerical_options', array('question' => backup::VAR_PARENTID));
+
+        // don't need to annotate ids nor files
+    }
+
+    /**
+     * Attach to $element (usually questions) the needed backup structures
+     * for question_datasets for a given question
+     * Used by calculated qtypes
+     */
+    protected function add_question_datasets($element) {
+        // Check $element is one nested_backup_element
+        if (! $element instanceof backup_nested_element) {
+            throw new backup_step_exception('question_datasets_bad_parent_element', $element);
+        }
+
+        // Define the elements
+        $definitions = new backup_nested_element('dataset_definitions');
+        $definition = new backup_nested_element('dataset_definition', array('id'), array(
+            'category', 'name', 'type', 'options',
+            'itemcount'));
+
+        $items = new backup_nested_element('dataset_items');
+        $item = new backup_nested_element('dataset_item', array('id'), array(
+            'number', 'value'));
+
+        // Build the tree
+        $element->add_child($definitions);
+        $definitions->add_child($definition);
+
+        $definition->add_child($items);
+        $items->add_child($item);
+
+        // Set the sources
+        $definition->set_source_sql('SELECT *
+                                       FROM {question_dataset_definitions} qdd
+                                       JOIN {question_datasets} qd ON qd.datasetdefinition = qdd.id
+                                      WHERE qd.question = ?', array(backup::VAR_PARENTID));
+
+        $item->set_source_table('question_dataset_items', array('definition' => backup::VAR_PARENTID));
+
+        // Aliases
+        $item->set_source_alias('itemnumber', 'number');
+
+        // don't need to annotate ids nor files
+    }
+}
index e46634f..c8536dc 100644 (file)
@@ -148,6 +148,109 @@ abstract class backup_activity_structure_step extends backup_structure_step {
     }
 }
 
+/**
+ * Abstract structure step, to be used by all the activities using core questions stuff
+ * (namelu quiz module), supporting question plugins, states and sessions
+ */
+abstract class backup_questions_activity_structure_step extends backup_activity_structure_step {
+
+    /**
+     * Attach to $element (usually attempts) the needed backup structures
+     * for question_states for a given question_attempt
+     */
+    protected function add_question_attempts_states($element, $questionattemptname) {
+        // Check $element is one nested_backup_element
+        if (! $element instanceof backup_nested_element) {
+            throw new backup_step_exception('question_states_bad_parent_element', $element);
+        }
+        // Check that the $questionattemptname is final element in $element
+        if (! $element->get_final_element($questionattemptname)) {
+            throw new backup_step_exception('question_states_bad_question_attempt_element', $questionattemptname);
+        }
+
+        // TODO: Some day we should stop these "encrypted" state->answers and
+        // TODO: delegate to qtypes plugin to proper XML writting the needed info on each question
+
+        // TODO: Should be doing here some introspection in the "answer" element, based on qtype,
+        // TODO: to know which real questions are being used (for randoms and other qtypes...)
+        // TODO: Not needed if consistency is guaranteed, but it isn't right now :-(
+
+        // Define the elements
+        $states = new backup_nested_element('states');
+        $state = new backup_nested_element('state', array('id'), array(
+            'question', 'seq_number', 'answer', 'timestamp',
+            'event', 'grade', 'raw_grade', 'penalty'));
+
+        // Build the tree
+        $element->add_child($states);
+        $states->add_child($state);
+
+        // Set the sources
+        $state->set_source_table('question_states', array('attempt' => '../../' . $questionattemptname));
+
+        // Annotate ids
+        $state->annotate_ids('question', 'question');
+    }
+
+    /**
+     * Attach to $element (usually attempts) the needed backup structures
+     * for question_sessions for a given question_attempt
+     */
+    protected function add_question_attempts_sessions($element, $questionattemptname) {
+        // Check $element is one nested_backup_element
+        if (! $element instanceof backup_nested_element) {
+            throw new backup_step_exception('question_sessions_bad_parent_element', $element);
+        }
+        // Check that the $questionattemptname is final element in $element
+        if (! $element->get_final_element($questionattemptname)) {
+            throw new backup_step_exception('question_sessions_bad_question_attempt_element', $questionattemptname);
+        }
+
+        // Define the elements
+        $sessions = new backup_nested_element('sessions');
+        $session = new backup_nested_element('session', array('id'), array(
+            'questionid', 'newest', 'newgraded', 'sumpenalty',
+            'manualcomment', 'manualcommentformat', 'flagged'));
+
+        // Build the tree
+        $element->add_child($sessions);
+        $sessions->add_child($session);
+
+        // Set the sources
+        $session->set_source_table('question_sessions', array('attemptid' => '../../' . $questionattemptname));
+
+        // Annotate ids
+        $session->annotate_ids('question', 'questionid');
+
+        // Annotate files
+        // Note: question_sessions haven't files associated. On purpose manualcomment is lacking
+        // support for them, so we don't need to annotated them here.
+    }
+}
+
+/**
+ * backup structure step in charge of calculating the categories to be
+ * included in backup, based in the context being backuped (module/course)
+ * and the already annotated questions present in backup_ids_temp
+ */
+class backup_calculate_question_categories extends backup_execution_step {
+
+    protected function define_execution() {
+        backup_question_dbops::calculate_question_categories($this->get_backupid(), $this->task->get_contextid());
+    }
+}
+
+/**
+ * backup structure step in charge of deleting all the questions annotated
+ * in the backup_ids_temp table
+ */
+class backup_delete_temp_questions extends backup_execution_step {
+
+    protected function define_execution() {
+        backup_question_dbops::delete_temp_questions($this->get_backupid());
+    }
+}
+
 /**
  * Abstract structure step, parent of all the block structure steps. Used to wrap the
  * block structure definition within the main <block ...> tag
@@ -1429,6 +1532,91 @@ class backup_annotate_scales_from_outcomes extends backup_execution_step {
     }
 }
 
+/**
+ * This step will generate all the file annotations for the already
+ * annotated (final) question_categories. It calculates the different
+ * contexts that are being backup and, annotates all the files
+ * on every context belonging to the "question" component. As far as
+ * we are always including *complete* question banks it is safe and
+ * optimal to do that in this (one pass) way
+ */
+class backup_annotate_all_question_files extends backup_execution_step {
+
+    protected function define_execution() {
+        global $DB;
+
+        // Get all the different contexts for the final question_categories
+        // annotated along the whole backup
+        $rs = $DB->get_recordset_sql("SELECT DISTINCT qc.contextid
+                                        FROM {question_categories} qc
+                                        JOIN {backup_ids_temp} bi ON bi.itemid = qc.id
+                                       WHERE bi.backupid = ?
+                                         AND bi.itemname = 'question_categoryfinal'", array($this->get_backupid()));
+        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
+            backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', null, null);
+        }
+        $rs->close();
+    }
+}
+
+/**
+ * structure step in charge of constructing the questions.xml file for all the
+ * question categories and questions required by the backup
+ * and letters related to one activity
+ */
+class backup_questions_structure_step extends backup_structure_step {
+
+    protected function define_structure() {
+
+        // Define each element separated
+
+        $qcategories = new backup_nested_element('question_categories');
+
+        $qcategory = new backup_nested_element('question_category', array('id'), array(
+            'name', 'contextid', 'contextlevel', 'contextinstanceid',
+            'info', 'infoformat', 'stamp', 'parent',
+            'sortorder'));
+
+        $questions = new backup_nested_element('questions');
+
+        $question = new backup_nested_element('question', array('id'), array(
+            'parent', 'name', 'questiontext', 'questiontextformat',
+            'generalfeedback', 'generalfeedbackformat', 'defaultgrade', 'penalty',
+            'qtype', 'length', 'stamp', 'version',
+            'hidden', 'timecreated', 'timemodified', 'createdby', 'modifiedby'));
+
+        // attach qtype plugin structure to $question element, only one allowed
+        $this->add_plugin_structure('qtype', $question, false);
+
+        // Build the tree
+
+        $qcategories->add_child($qcategory);
+        $qcategory->add_child($questions);
+
+        $questions->add_child($question);
+
+        // Define the sources
+
+        $qcategory->set_source_sql("
+            SELECT gc.*, contextlevel, instanceid AS contextinstanceid
+              FROM {question_categories} gc
+              JOIN {backup_ids_temp} bi ON bi.itemid = gc.id
+              JOIN {context} co ON co.id = gc.contextid
+             WHERE bi.backupid = ?
+               AND bi.itemname = 'question_categoryfinal'", array(backup::VAR_BACKUPID));
+
+        $question->set_source_table('question', array('category' => backup::VAR_PARENTID));
+
+        // don't need to annotate ids nor files
+
+        return $qcategories;
+    }
+}
+
+
+
 /**
  * This step will generate all the file  annotations for the already
  * annotated (final) users. Need to do this here because each user
@@ -1622,4 +1810,4 @@ class backup_course_completion_structure_step extends backup_structure_step {
         return $cc;
 
     }
-}
\ No newline at end of file
+}
index cdb0729..a827613 100644 (file)
@@ -1605,23 +1605,11 @@ class restore_block_instance_structure_step extends restore_structure_step {
             throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid);
         }
 
-        // get instance of block object, we need to query it
-        $data->blockname = clean_param($data->blockname, PARAM_SAFEDIR);
-        $blockfile = $CFG->dirroot.'/blocks/'.$data->blockname.'/block_'.$data->blockname.'.php';
-        if (!file_exists($blockfile)) {
-            return false;
-        }
-        include_once($blockfile);
-        $classname = 'block_'.$data->blockname;
-        if (!class_exists($classname)) {
-            return false;
-        }
-        $blockobject = new $classname();
-
-        //TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
+        // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
         // If there is already one block of that type in the parent context
         // and the block is not multiple, stop processing
-        if (!$blockobject->instance_allow_multiple()) {
+        // Use blockslib loader / method executor
+        if (!block_method_result($data->blockname, 'instance_allow_multiple')) {
             if ($DB->record_exists_sql("SELECT bi.id
                                           FROM {block_instances} bi
                                           JOIN {block} b ON b.name = bi.blockname
diff --git a/backup/util/dbops/backup_question_dbops.class.php b/backup/util/dbops/backup_question_dbops.class.php
new file mode 100644 (file)
index 0000000..67ff54f
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @package    moodlecore
+ * @subpackage backup-dbops
+ * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Non instantiable helper class providing DB support to the questions backup stuff
+ *
+ * This class contains various static methods available for all the DB operations
+ * performed by questions stuff
+ *
+ * TODO: Finish phpdocs
+ */
+abstract class backup_question_dbops extends backup_dbops {
+
+    /**
+     * Calculates all the question_categories to be included
+     * in backup, based in a given context (course/module) and
+     * the already annotated questions present in backup_ids_temp
+     */
+    public static function calculate_question_categories($backupid, $contextid) {
+        global $DB;
+
+        // First step, annotate all the categories for the given context (course/module)
+        // i.e. the whole context questions bank
+        $DB->execute("INSERT INTO {backup_ids_temp} (backupid, itemname, itemid)
+                      SELECT ?, 'question_category', id
+                        FROM {question_categories}
+                       WHERE contextid = ?", array($backupid, $contextid));
+
+        // Now, based in the annotated questions, annotate all the categories they
+        // belong to (whole context question banks too)
+        // First, get all the contexts we are going to save their question bank (no matter
+        // where they are in the contexts hierarchy, transversals... whatever)
+        $contexts = $DB->get_fieldset_sql("SELECT DISTINCT qc2.contextid
+                                             FROM {question_categories} qc2
+                                             JOIN {question} q ON q.category = qc2.id
+                                             JOIN {backup_ids_temp} bi ON bi.itemid = q.id
+                                            WHERE bi.backupid = ?
+                                              AND bi.itemname = 'question'
+                                              AND qc2.contextid != ?", array($backupid, $contextid));
+        // And now, simply insert all the question categories (complete question bank)
+        // for those contexts if we have found any
+        if ($contexts) {
+            list($contextssql, $contextparams) = $DB->get_in_or_equal($contexts);
+            $params = array_merge(array($backupid), $contextparams);
+            $DB->execute("INSERT INTO {backup_ids_temp} (backupid, itemname, itemid)
+                          SELECT ?, 'question_category', id
+                            FROM {question_categories}
+                           WHERE contextid $contextssql", $params);
+        }
+    }
+
+    /**
+     * Delete all the annotated questions present in backup_ids_temp
+     */
+    public static function delete_temp_questions($backupid) {
+        global $DB;
+        $DB->delete_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => 'question'));
+    }
+}
index ee2cdd1..00134da 100644 (file)
@@ -108,9 +108,13 @@ abstract class backup_structure_dbops extends backup_dbops {
         $sql = 'SELECT id
                   FROM {files}
                  WHERE contextid = ?
-                   AND component = ?
-                   AND filearea = ?';
-        $params = array($contextid, $component, $filearea);
+                   AND component = ?';
+        $params = array($contextid, $component);
+
+        if (!is_null($filearea)) { // Add filearea to query and params if necessary
+            $sql .= ' AND filearea = ?';
+            $params[] = $filearea;
+        }
 
         if (!is_null($itemid)) { // Add itemid to query and params if necessary
             $sql .= ' AND itemid = ?';
@@ -139,7 +143,7 @@ abstract class backup_structure_dbops extends backup_dbops {
             }
         }
         $rs->close();
-        // All the remaining 'user' annotations can be safely deleted
+        // All the remaining $itemname annotations can be safely deleted
         $DB->delete_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname));
     }
 
index b388b1a..759142d 100644 (file)
@@ -287,7 +287,7 @@ abstract class backup_helper {
      * inforef.xml files. Used both by backup and restore
      */
     public static function get_inforef_itemnames() {
-        return array('user', 'grouping', 'group', 'role', 'file', 'scale', 'outcome', 'grade_item');
+        return array('user', 'grouping', 'group', 'role', 'file', 'scale', 'outcome', 'grade_item', 'question_category');
     }
 }
 
index 4fc42a2..b70e6c7 100644 (file)
@@ -40,6 +40,7 @@ require_once($CFG->dirroot . '/backup/util/dbops/backup_dbops.class.php');
 require_once($CFG->dirroot . '/backup/util/dbops/backup_structure_dbops.class.php');
 require_once($CFG->dirroot . '/backup/util/dbops/backup_controller_dbops.class.php');
 require_once($CFG->dirroot . '/backup/util/dbops/backup_plan_dbops.class.php');
+require_once($CFG->dirroot . '/backup/util/dbops/backup_question_dbops.class.php');
 require_once($CFG->dirroot . '/backup/util/checks/backup_check.class.php');
 require_once($CFG->dirroot . '/backup/util/structure/base_atom.class.php');
 require_once($CFG->dirroot . '/backup/util/structure/base_attribute.class.php');
index 63fca5c..adf99e6 100644 (file)
@@ -100,6 +100,43 @@ abstract class backup_structure_step extends backup_step {
 
 // Protected API starts here
 
+    /**
+     * Add plugin structure to any element in the activity 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
+     *                                       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)
+     */
+    protected function add_plugin_structure($plugintype, $element, $multiple) {
+
+        global $CFG;
+
+        // Check the requested plugintype is a valid one
+        if (!array_key_exists($plugintype, get_plugin_types($plugintype))) {
+             throw new backup_step_exception('incorrect_plugin_type', $plugintype);
+        }
+
+        // Arrived here, plugin is correct, let's create the optigroup
+        $optigroupname = $plugintype . '_' . $element->get_name() . '_plugin';
+        $optigroup = new backup_optigroup($optigroupname, null, $multiple);
+        $element->add_child($optigroup); // Add optigroup to stay connected since beginning
+
+        // Get all the optigroup_elements, looking across all the plugin dirs
+        $pluginsdirs = get_plugin_list($plugintype);
+        foreach ($pluginsdirs as $name => $plugindir) {
+            $classname = 'backup_' . $plugintype . '_' . $name . '_plugin';
+            $backupfile = $plugindir . '/backup/moodle2/' . $classname . '.class.php';
+            if (file_exists($backupfile)) {
+                require_once($backupfile);
+                $backupplugin = new $classname($plugintype, $name, $optigroup);
+                // Add plugin returned structure to optigroup
+                $backupplugin->define_plugin_structure($element->get_name());
+            }
+        }
+    }
+
     /**
      * To conditionally decide if one step will be executed or no
      *
index 2488345..3a28d57 100755 (executable)
@@ -67,7 +67,7 @@ class data_field_textarea extends data_field_base {
             $formats[$fid] = $strformats[$fid];
         }
         $editor->use_editor($field, $options);
-        $str .= '<div><textarea id="'.$field.'" name="'.$field.'" rows="15" cols="80">'.s($text).'</textarea></div>';
+        $str .= '<div><textarea id="'.$field.'" name="'.$field.'" rows="'.$this->field->param3.'" cols="'.$this->field->param2.'">'.s($text).'</textarea></div>';
         $str .= '<div><select name="'.$field.'_content1">';
         foreach ($formats as $key=>$desc) {
             $selected = ($format == $key) ? 'selected="selected"' : '';
diff --git a/mod/quiz/backup/moodle2/backup_quiz_activity_task.class.php b/mod/quiz/backup/moodle2/backup_quiz_activity_task.class.php
new file mode 100644 (file)
index 0000000..3f286dc
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @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
+ */
+
+require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/backup_quiz_stepslib.php'); // Because it exists (must)
+
+/**
+ * quiz backup task that provides all the settings and steps to perform one
+ * complete backup of the activity
+ */
+class backup_quiz_activity_task extends backup_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() {
+        // Generate the quiz.xml file containing all the quiz information
+        // and annotating used questions
+        $this->add_step(new backup_quiz_activity_structure_step('quiz_structure', 'quiz.xml'));
+
+        // Note: Following  steps must be present
+        // in all the activities using question banks (only quiz for now)
+        // TODO: Specialise these step to a new subclass of backup_activity_task
+
+        // Process all the annotated questions to calculate the question
+        // categories needing to be included in backup for this activity
+        // plus the categories belonging to the activity context itself
+        $this->add_step(new backup_calculate_question_categories('activity_question_categories'));
+
+        // Clean backup_temp_ids table from questions. We already
+        // have used them to detect question_categories and aren't
+        // needed anymore
+        $this->add_step(new backup_delete_temp_questions('clean_temp_questions'));
+    }
+
+    /**
+     * Code the transformations to perform in the activity in
+     * order to get transportable (encoded) links
+     */
+    static public function encode_content_links($content) {
+        global $CFG;
+
+        $base = preg_quote($CFG->wwwroot,"/");
+
+        // Link to the list of quizzes
+        $search="/(".$base."\/mod\/quiz\/index.php\?id\=)([0-9]+)/";
+        $content= preg_replace($search, '$@QUIZINDEX*$2@$', $content);
+
+        // Link to quiz view by moduleid
+        $search="/(".$base."\/mod\/quiz\/view.php\?id\=)([0-9]+)/";
+        $content= preg_replace($search, '$@QUIZVIEWBYID*$2@$', $content);
+
+        // Link to quiz view by quizid
+        $search="/(".$base."\/mod\/quiz\/view.php\?q\=)([0-9]+)/";
+        $content= preg_replace($search, '$@QUIZVIEWBYQ*$2@$', $content);
+
+        return $content;
+    }
+}
diff --git a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php
new file mode 100644 (file)
index 0000000..4cc027a
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @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 backup steps that will be used by the backup_quiz_activity_task
+ */
+
+/**
+ * Define the complete quiz structure for backup, with file and id annotations
+ */
+class backup_quiz_activity_structure_step extends backup_questions_activity_structure_step {
+
+    protected function define_structure() {
+
+        // To know if we are including userinfo
+        $userinfo = $this->get_setting_value('userinfo');
+
+        // Define each element separated
+        $quiz = new backup_nested_element('quiz', array('id'), array(
+            'name', 'intro', 'introformat', 'timeopen',
+            'timeclose', 'optionflags', 'penaltyscheme', 'attempts',
+            'attemptonlast', 'grademethod', 'decimalpoints', 'questiondecimalpoints',
+            'review', 'questionsperpage', 'shufflequestions', 'shuffleanswers',
+            'questions', 'sumgrades', 'grade', 'timecreated',
+            'timemodified', 'timelimit', 'password', 'subnet',
+            'popup', 'delay1', 'delay2', 'showuserpicture'));
+
+        $qinstances = new backup_nested_element('question_instances');
+
+        $qinstance = new backup_nested_element('question_instance', array('id'), array(
+            'question', 'grade'));
+
+        $feedbacks = new backup_nested_element('feedbacks');
+
+        $feedback = new backup_nested_element('feedback', array('id'), array(
+            'feedbacktext', 'feedbacktextformat', 'mingrade', 'maxgrade'));
+
+        $overrides = new backup_nested_element('overrides');
+
+        $override = new backup_nested_element('override', array('id'), array(
+            'userid', 'groupid', 'timeopen', 'timeclose',
+            'timelimit', 'attempts', 'password'));
+
+        $grades = new backup_nested_element('grades');
+
+        $grade = new backup_nested_element('grade', array('id'), array(
+            'userid', 'gradeval', 'timemodified'));
+
+        $attempts = new backup_nested_element('attempts');
+
+        $attempt = new backup_nested_element('attempt', array('id'), array(
+            'uniqueid', 'userid', 'attemptnum', 'sumgrades',
+            'timestart', 'timefinish', 'timemodified', 'layout',
+            'preview'));
+
+        // This module is using questions, so produce the related question states and sessions
+        // attaching them to the $attempt element based in 'uniqueid' matching
+        $this->add_question_attempts_states($attempt, 'uniqueid');
+        $this->add_question_attempts_sessions($attempt, 'uniqueid');
+
+        // Build the tree
+
+        $quiz->add_child($qinstances);
+        $qinstances->add_child($qinstance);
+
+        $quiz->add_child($feedbacks);
+        $feedbacks->add_child($feedback);
+
+        $quiz->add_child($overrides);
+        $overrides->add_child($override);
+
+        $quiz->add_child($grades);
+        $grades->add_child($grade);
+
+        $quiz->add_child($attempts);
+        $attempts->add_child($attempt);
+
+        // Define sources
+        $quiz->set_source_table('quiz', array('id' => backup::VAR_ACTIVITYID));
+
+        $qinstance->set_source_table('quiz_question_instances', array('quiz' => backup::VAR_PARENTID));
+
+        $feedback->set_source_table('quiz_feedback', array('quizid' => backup::VAR_PARENTID));
+
+        // Quiz overrides to backup are different depending of user info
+        $overrideparams = array('quiz' => backup::VAR_PARENTID);
+        if (!$userinfo) { //  Without userinfo, skip user overrides
+            $overrideparams['userid'] = backup_helper::is_sqlparam(null);
+
+        }
+        $override->set_source_table('quiz_overrides', $overrideparams);
+
+        // All the rest of elements only happen if we are including user info
+        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
+        $grade->set_source_alias('grade', 'gradeval');
+        $attempt->set_source_alias('attempt', 'attemptnum');
+
+        // Define id annotations
+        $qinstance->annotate_ids('question', 'question');
+        $override->annotate_ids('user', 'userid');
+        $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
+        $feedback->annotate_files('mod_quiz', 'feedback', 'id');
+
+        // Return the root element (quiz), wrapped into standard activity structure
+        return $this->prepare_activity_structure($quiz);
+    }
+}
index 4728c3b..50d66b7 100644 (file)
@@ -1613,6 +1613,7 @@ function quiz_supports($feature) {
         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
         case FEATURE_GRADE_HAS_GRADE:         return true;
         case FEATURE_GRADE_OUTCOMES:          return true;
+        case FEATURE_BACKUP_MOODLE2:          return true;
 
         default: return null;
     }