Skip to content

Commit

Permalink
MDL-20636 Finished backup and restore of attempt data. Yay
Browse files Browse the repository at this point in the history
  • Loading branch information
timhunt committed May 5, 2011
1 parent 3b3d5e7 commit c749527
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 290 deletions.
6 changes: 4 additions & 2 deletions backup/moodle2/backup_qtype_plugin.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,11 @@ public static function get_components_and_fileareas($filter = null) {
/**
* 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();
}
}
34 changes: 24 additions & 10 deletions backup/moodle2/backup_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ protected function add_question_usages($element, $usageidname) {
}

$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(
Expand All @@ -201,33 +201,42 @@ protected function add_question_usages($element, $usageidname) {
$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);
$quba->add_child($qas);
$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');
}
Expand Down Expand Up @@ -1677,7 +1686,12 @@ protected function define_structure() {

$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}
Expand Down
11 changes: 7 additions & 4 deletions backup/moodle2/restore_qtype_plugin.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,14 @@ public function process_question_dataset_item($data) {
}

/**
* 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;
}

/**
Expand Down
138 changes: 105 additions & 33 deletions backup/moodle2/restore_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -2522,6 +2522,10 @@ protected function define_execution() {
* (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
Expand All @@ -2541,72 +2545,109 @@ protected function add_question_usages($element, &$paths) {
$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');
}

/**
* Process question_usages
*/
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;
}

/**
* Process question_attempt_steps
*/
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);
}
}

/**
Expand All @@ -2627,4 +2668,35 @@ protected function questions_recode_layout($layout) {
}
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');
}
}
}
6 changes: 5 additions & 1 deletion mod/quiz/backup/moodle2/backup_quiz_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,11 @@ protected function define_structure() {
// 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
Expand Down
16 changes: 10 additions & 6 deletions mod/quiz/backup/moodle2/restore_quiz_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ protected function process_quiz_attempt($data) {
$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);

Expand All @@ -275,14 +275,18 @@ protected function process_quiz_attempt($data) {

$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'
Expand Down
18 changes: 1 addition & 17 deletions question/engine/datalib.php
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ public function delete_steps_for_question_attempts($qaids, $context) {
*/
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);
}
Expand Down Expand Up @@ -853,22 +853,6 @@ public function questions_in_use(array $questionids, qubaid_condition $qubaids)
'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;
}
}


Expand Down
16 changes: 16 additions & 0 deletions question/engine/lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,22 @@ public static function get_behaviour_name($behaviour) {
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.
Expand Down
Loading

0 comments on commit c749527

Please sign in to comment.