MDL-20636 Finished backup and restore of attempt data. Yay

This commit is contained in:
Tim Hunt
2011-05-05 21:26:25 +01:00
parent 3b3d5e799d
commit c749527bec
15 changed files with 226 additions and 292 deletions
+4 -2
View File
@@ -205,9 +205,11 @@ abstract class backup_qtype_plugin extends backup_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.
* 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 wil- return array();
// By default, return empty array, only qtypes having own fileareas will override this
return array();
}
}
+24 -10
View File
@@ -189,7 +189,7 @@ abstract class backup_questions_activity_structure_step extends backup_activity_
}
$quba = new backup_nested_element('question_usage', array('id'),
array('preferredbehaviour'));
array('component', 'preferredbehaviour'));
$qas = new backup_nested_element('question_attempts');
$qa = new backup_nested_element('question_attempt', array('id'), array(
@@ -201,8 +201,8 @@ abstract class backup_questions_activity_structure_step extends backup_activity_
$step = new backup_nested_element('step', array('id'), array(
'sequencenumber', 'state', 'fraction', 'timecreated', 'userid'));
$data = new backup_nested_element('data');
$value = new backup_nested_element('value', array('name', 'value'));
$response = new backup_nested_element('response');
$variable = new backup_nested_element('variable', null, array('name', 'value'));
// Build the tree
$element->add_child($quba);
@@ -210,24 +210,33 @@ abstract class backup_questions_activity_structure_step extends backup_activity_
$qas->add_child($qa);
$qa->add_child($steps);
$steps->add_child($step);
$step->add_child($data);
$data->add_child($value);
$step->add_child($response);
$response->add_child($variable);
// Set the sources
$quba->set_source_table('question_usages',
array('id' => '../' . $usageidname));
$qa->set_source_table('question_attempts',
$qa->set_source_sql('
SELECT *
FROM {question_attempts}
WHERE questionusageid = :questionusageid
ORDER BY slot',
array('questionusageid' => backup::VAR_PARENTID));
$step->set_source_table('question_attempt_steps',
$step->set_source_sql('
SELECT *
FROM {question_attempt_steps}
WHERE questionattemptid = :questionattemptid
ORDER BY sequencenumber',
array('questionattemptid' => backup::VAR_PARENTID));
$value->set_source_table('question_attempt_step_data',
$variable->set_source_table('question_attempt_step_data',
array('attemptstepid' => backup::VAR_PARENTID));
// Annotate ids
$qa->annotate_ids('question', 'questionid');
$step->annotate_ids('user', 'userid');
// Annotate files
$fileareas = question_engine_data_mapper::get_all_response_file_areas();
$fileareas = question_engine::get_all_response_file_areas();
foreach ($fileareas as $filearea) {
$step->annotate_files('question', $filearea, 'id');
}
@@ -1677,7 +1686,12 @@ class backup_questions_structure_step extends backup_structure_step {
$question->set_source_table('question', array('category' => backup::VAR_PARENTID));
$qhint->set_source_table('question_hints', array('questionid' => backup::VAR_PARENTID));
$qhint->set_source_sql('
SELECT *
FROM {question_hints}
WHERE questionid = :questionid
ORDER BY id',
array('questionid' => backup::VAR_PARENTID));
// don't need to annotate ids nor files
// (already done by {@link backup_annotate_all_question_files}
@@ -315,11 +315,14 @@ abstract class restore_qtype_plugin extends restore_plugin {
}
/**
* Decode one question_states for this qtype (default impl)
* Do any re-coding necessary in the student response.
* @param int $questionid the new id of the question
* @param int $sequencenumber of the step within the qusetion attempt.
* @param array the response data from the backup.
* @return array the recoded response.
*/
public function recode_state_answer($state) {
// By default, return answer unmodified, qtypes needing recode will override this
return $state->answer;
public function recode_response($questionid, $sequencenumber, array $response) {
return $response;
}
/**
+105 -33
View File
@@ -2522,6 +2522,10 @@ class restore_create_question_files extends restore_execution_step {
* (like the quiz module), to support qtype plugins, states and sessions
*/
abstract class restore_questions_activity_structure_step extends restore_activity_structure_step {
/** @var array question_attempt->id to qtype. */
protected $qtypes = array();
/** @var array question_attempt->id to questionid. */
protected $newquestionids = array();
/**
* Attach below $element (usually attempts) the needed restore_path_elements
@@ -2541,9 +2545,14 @@ abstract class restore_questions_activity_structure_step extends restore_activit
$paths[] = new restore_path_element('question_attempt',
$element->get_path() . '/question_usage/question_attempts/question_attempt');
$paths[] = new restore_path_element('question_attempt_step',
$element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step');
$element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step',
true);
$paths[] = new restore_path_element('question_attempt_step_data',
$element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step/data/value');
$element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step/response/variable');
// TODO Put back code for restoring legacy 2.0 backups.
// $paths[] = new restore_path_element('question_state', $element->get_path() . '/states/state');
// $paths[] = new restore_path_element('question_session', $element->get_path() . '/sessions/session');
}
/**
@@ -2551,46 +2560,52 @@ abstract class restore_questions_activity_structure_step extends restore_activit
*/
protected function process_question_usage($data) {
global $DB;
// TODO
// Clear our caches.
$this->qtypes = array();
$this->newquestionids = array();
$data = (object)$data;
$oldid = $data->id;
// Get complete question mapping, we'll need info
$question = $this->get_mapping('question', $data->question);
$oldcontextid = $this->get_task()->get_old_contextid();
$data->contextid = $this->get_mappingid('context', $this->task->get_old_contextid());
// 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 (no mapping needed)
$newitemid = $DB->insert_record('question_usages', $data);
// Everything ready, insert and create mapping (needed by question_sessions)
$newitemid = $DB->insert_record('question_states', $data);
$this->set_mapping('question_state', $oldid, $newitemid);
$this->inform_new_usage_id($newitemid);
$this->set_mapping('question_usage', $oldid, $newitemid, false);
}
/**
* When process_question_usage creates the new usage, it calls this method
* to let the activity link to the new usage. For example, the quiz uses
* this method to set quiz_attempts.uniqueid to the new usage id.
* @param integer $newusageid
*/
abstract protected function inform_new_usage_id($newusageid);
/**
* Process question_attempts
*/
protected function process_question_attempt($data) {
global $DB;
// TODO
$data = (object)$data;
$oldid = $data->id;
$question = $this->get_mapping('question', $data->questionid);
// 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);
$data->questionusageid = $this->get_new_parentid('question_usage');
$data->questionid = $question->newitemid;
$data->timemodified = $this->apply_date_offset($data->timemodified);
// Everything ready, insert (no mapping needed)
$newitemid = $DB->insert_record('question_sessions', $data);
$newitemid = $DB->insert_record('question_attempts', $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.
$this->set_mapping('question_attempt', $oldid, $newitemid);
$this->qtypes[$newitemid] = $question->info->qtype;
$this->newquestionids[$newitemid] = $data->questionid;
}
/**
@@ -2598,15 +2613,41 @@ abstract class restore_questions_activity_structure_step extends restore_activit
*/
protected function process_question_attempt_step($data) {
global $DB;
// TODO
}
/**
* Process question_attempt_step_data
*/
protected function process_question_attempt_step_data($data) {
global $DB;
// TODO
$data = (object)$data;
$oldid = $data->id;
// Pull out the response data.
$response = array();
if (!empty($data->response['variable'])) {
foreach ($data->response['variable'] as $variable) {
$response[$variable['name']] = $variable['value'];
}
}
unset($data->response);
$data->questionattemptid = $this->get_new_parentid('question_attempt');
$data->timecreated = $this->apply_date_offset($data->timecreated);
$data->userid = $this->get_mappingid('user', $data->userid);
// Everything ready, insert and create mapping (needed by question_sessions)
$newitemid = $DB->insert_record('question_attempt_steps', $data);
$this->set_mapping('question_attempt_step', $oldid, $newitemid, true);
// Now process the response data.
$qtyperestorer = $this->get_qtype_restorer($this->qtypes[$data->questionattemptid]);
if ($qtyperestorer) {
$response = $qtyperestorer->recode_response(
$this->newquestionids[$data->questionattemptid],
$data->sequencenumber, $response);
}
foreach ($response as $name => $value) {
$row = new stdClass();
$row->attemptstepid = $newitemid;
$row->name = $name;
$row->value = $value;
$DB->insert_record('question_attempt_step_data', $row, false);
}
}
/**
@@ -2627,4 +2668,35 @@ abstract class restore_questions_activity_structure_step extends restore_activit
}
return implode(',', $questionids);
}
/**
* Get the restore_qtype_plugin subclass for a specific question type.
* @param string $qtype e.g. multichoice.
* @return restore_qtype_plugin instance.
*/
public function get_qtype_restorer($qtype) {
// Build one static cache to store {@link restore_qtype_plugin}
// while we are needing them, just to save zillions of instantiations
// or using static stuff that will break our nice API
static $qtypeplugins = array();
if (!isset($qtypeplugins[$qtype])) {
$classname = 'restore_qtype_' . $qtype . '_plugin';
if (class_exists($classname)) {
$qtypeplugins[$qtype] = new $classname('qtype', $qtype, $this);
} else {
$qtypeplugins[$qtype] = null;
}
}
return $qtypeplugins[$qtype];
}
protected function after_execute() {
parent::after_execute();
// Restore any files belonging to responses.
foreach (question_engine::get_all_response_file_areas() as $filearea) {
$this->add_related_files('question', $filearea, 'question_attempt_step');
}
}
}
@@ -122,7 +122,11 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
// 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));
$attempt->set_source_sql('
SELECT *
FROM {quiz_attempts}
WHERE quiz = :quiz AND preview = 0',
array('quiz' => backup::VAR_PARENTID));
}
// Define source alias
@@ -265,7 +265,7 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
$data->quiz = $this->get_new_parentid('quiz');
$data->attempt = $data->attemptnum;
$data->uniqueid = question_new_attempt_uniqueid('quiz');
$data->uniqueid = 0; // filled in later by {@link inform_new_usage_id()}
$data->userid = $this->get_mappingid('user', $data->userid);
@@ -275,14 +275,18 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
$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.
$this->set_mapping('quiz_attempt', $olduniqueid, $data->uniqueid, false);
// Also save quiz_attempt->id mapping, because logs use it
$this->set_mapping('quiz_attempt_id', $oldid, $newitemid, false);
// Save quiz_attempt->id mapping, because logs use it
$this->set_mapping('quiz_attempt', $oldid, $newitemid, false);
}
protected function inform_new_usage_id($newusageid) {
global $DB;
$DB->set_field('quiz_attempts', 'uniqueid', $newusageid, array('id' =>
$this->get_new_parentid('quiz_attempt')));
}
protected function after_execute() {
parent::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'
+1 -17
View File
@@ -701,7 +701,7 @@ ORDER BY
*/
protected function delete_response_files($contextid, $itemidstest, $params) {
$fs = get_file_storage();
foreach ($this->get_all_response_file_areas() as $filearea) {
foreach (question_engine::get_all_response_file_areas() as $filearea) {
$fs->delete_area_files_select($contextid, 'question', $filearea,
$itemidstest, $params);
}
@@ -853,22 +853,6 @@ ORDER BY
'questionid ' . $test . ' AND questionusageid ' .
$qubaids->usage_id_in(), $params + $qubaids->usage_id_in_params());
}
/**
* @return array all the file area names that may contain response files.
*/
public static function get_all_response_file_areas() {
$variables = array();
foreach (question_bank::get_all_qtypes() as $qtype) {
$variables += $qtype->response_file_areas();
}
$areas = array();
foreach (array_unique($variables) as $variable) {
$areas[] = 'response_' . $variable;
}
return $areas;
}
}
+16
View File
@@ -315,6 +315,22 @@ abstract class question_engine {
return get_string('pluginname', 'qbehaviour_' . $behaviour);
}
/**
* @return array all the file area names that may contain response files.
*/
public static function get_all_response_file_areas() {
$variables = array();
foreach (question_bank::get_all_qtypes() as $qtype) {
$variables += $qtype->response_file_areas();
}
$areas = array();
foreach (array_unique($variables) as $variable) {
$areas[] = 'response_' . $variable;
}
return $areas;
}
/**
* Returns the valid choices for the number of decimal places for showing
* question marks. For use in the user interface.
@@ -78,46 +78,6 @@ class restore_qtype_ddwtos_plugin extends restore_qtype_plugin {
}
}
/**
* Given one question_states record, return the answer
* recoded pointing to all the restored stuff for ddwtos 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 (!empty($id) && $newid = $this->get_mappingid('question_answer', $id)) {
$orderarr[] = $newid;
}
}
// Map responses
foreach (explode(',', $lists[1]) as $id) {
if (!empty($id) && $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;
}
/**
* Return the contents of this qtype to be processed by the links decoder
*/
@@ -79,46 +79,6 @@ class restore_qtype_gapselect_plugin extends restore_qtype_plugin {
}
}
/**
* Given one question_states record, return the answer
* recoded pointing to all the restored stuff for gapselect 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 (!empty($id) && $newid = $this->get_mappingid('question_answer', $id)) {
$orderarr[] = $newid;
}
}
// Map responses
foreach (explode(',', $lists[1]) as $id) {
if (!empty($id) && $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;
}
/**
* Return the contents of this qtype to be processed by the links decoder
*/
@@ -162,24 +162,29 @@ class restore_qtype_match_plugin extends restore_qtype_plugin {
$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));
public function recode_response($questionid, $sequencenumber, array $response) {
if (array_key_exists('_stemorder', $response)) {
$response['_stemorder'] = $this->recode_match_sub_order($response['_stemorder']);
}
return implode(',', $resultarr);
if (array_key_exists('_choiceorder', $response)) {
$response['_choiceorder'] = $this->recode_match_sub_order($response['_choiceorder']);
}
return $response;
}
/**
* Recode the choice order as stored in the response.
* @param string $order the original order.
* @return string the recoded order.
*/
protected function recode_match_sub_order($order) {
$neworder = array();
foreach (explode(',', $order) as $id) {
if ($newid = $this->get_mappingid('question_match_sub', $id)) {
$neworder[] = $newid;
}
}
return implode(',', $neworder);
}
/**
@@ -86,44 +86,26 @@ class restore_qtype_multichoice_plugin extends restore_qtype_plugin {
}
}
public function recode_response($questionid, $sequencenumber, array $response) {
if (array_key_exists('_order', $response)) {
$response['_order'] = $this->recode_choice_order($response['_order']);
}
return $response;
}
/**
* 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
* Recode the choice order as stored in the response.
* @param string $order the original order.
* @return string the recoded order.
*/
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 (!empty($id) && $newid = $this->get_mappingid('question_answer', $id)) {
$orderarr[] = $newid;
protected function recode_choice_order($order) {
$neworder = array();
foreach (explode(',', $order) as $id) {
if ($newid = $this->get_mappingid('question_answer', $id)) {
$neworder[] = $newid;
}
}
// Map responses
foreach (explode(',', $lists[1]) as $id) {
if (!empty($id) && $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;
return implode(',', $neworder);
}
/**
@@ -80,44 +80,26 @@ class restore_qtype_oumultiresponse_plugin extends restore_qtype_plugin {
}
}
public function recode_response($questionid, $sequencenumber, array $response) {
if (array_key_exists('_order', $response)) {
$response['_order'] = $this->recode_choice_order($response['_order']);
}
return $response;
}
/**
* Given one question_states record, return the answer
* recoded pointing to all the restored stuff for oumultiresponse 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
* Recode the choice order as stored in the response.
* @param string $order the original order.
* @return string the recoded order.
*/
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 (!empty($id) && $newid = $this->get_mappingid('question_answer', $id)) {
$orderarr[] = $newid;
protected function recode_choice_order($order) {
$neworder = array();
foreach (explode(',', $order) as $id) {
if ($newid = $this->get_mappingid('question_answer', $id)) {
$neworder[] = $newid;
}
}
// Map responses
foreach (explode(',', $lists[1]) as $id) {
if (!empty($id) && $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;
return implode(',', $neworder);
}
/**
@@ -34,43 +34,4 @@ defined('MOODLE_INTERNAL') || die();
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
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 = '';
if (preg_match('~^random([0-9]+)-~', $answer, $matches)) {
// randomxx-yy answer format
$questionid = $matches[1];
$subanswer = substr($answer, strlen('random' . $questionid . '-'));
$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;
} else {
// simple question id format
$newquestionid = $this->get_mappingid('question', $answer);
$result = $newquestionid;
}
return $result;
}
}
@@ -79,19 +79,4 @@ class restore_qtype_truefalse_plugin extends restore_qtype_plugin {
$this->set_mapping('question_truefalse', $oldid, $newitemid);
}
}
/**
* 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;
}
}