diff --git a/backup/moodle2/backup_activity_task.class.php b/backup/moodle2/backup_activity_task.class.php index 2dc935347d4..e39c858bee7 100644 --- a/backup/moodle2/backup_activity_task.class.php +++ b/backup/moodle2/backup_activity_task.class.php @@ -181,6 +181,9 @@ abstract class backup_activity_task extends backup_task { // Generate the grading file (conditionally) $this->add_step(new backup_activity_grading_structure_step('activity_grading', 'grading.xml')); + // Generate the grade history file. The setting 'grade_histories' is handled in the step. + $this->add_step(new backup_activity_grade_history_structure_step('activity_grade_history', 'grade_history.xml')); + // Annotate the scales used in already annotated outcomes $this->add_step(new backup_annotate_scales_from_outcomes('annotate_scales')); diff --git a/backup/moodle2/backup_final_task.class.php b/backup/moodle2/backup_final_task.class.php index 2cd375416ef..6f69c59f2b9 100644 --- a/backup/moodle2/backup_final_task.class.php +++ b/backup/moodle2/backup_final_task.class.php @@ -89,6 +89,9 @@ class backup_final_task extends backup_task { // execute_condition() so only will be excuted if ALL module grade_items in course have been exported $this->add_step(new backup_gradebook_structure_step('course_gradebook','gradebook.xml')); + // Generate the grade history file, conditionally. + $this->add_step(new backup_grade_history_structure_step('course_grade_history','grade_history.xml')); + // Generate the course completion $this->add_step(new backup_course_completion_structure_step('course_completion', 'completion.xml')); diff --git a/backup/moodle2/backup_root_task.class.php b/backup/moodle2/backup_root_task.class.php index 3e97f689ff4..01a9f624d4c 100644 --- a/backup/moodle2/backup_root_task.class.php +++ b/backup/moodle2/backup_root_task.class.php @@ -152,6 +152,9 @@ class backup_root_task extends backup_task { $gradehistories->set_ui(new backup_setting_ui_checkbox($gradehistories, get_string('rootsettinggradehistories', 'backup'))); $this->add_setting($gradehistories); $users->add_dependency($gradehistories); + // The restore does not process the grade histories when some activities are ignored. + // So let's define a dependency to prevent false expectations from our users. + $activities->add_dependency($gradehistories); // Define question bank inclusion setting. $questionbank = new backup_generic_setting('questionbank', base_setting::IS_BOOLEAN, true); diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index a6e3ac642fd..e79c667b07a 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -1022,6 +1022,65 @@ class backup_gradebook_structure_step extends backup_structure_step { } } +/** + * Step in charge of constructing the grade_history.xml file containing the grade histories. + */ +class backup_grade_history_structure_step extends backup_structure_step { + + /** + * Limit the execution. + * + * This applies the same logic than the one applied to {@link backup_gradebook_structure_step}, + * because we do not want to save the history of items which are not backed up. At least for now. + */ + protected function execute_condition() { + return backup_plan_dbops::require_gradebook_backup($this->get_courseid(), $this->get_backupid()); + } + + protected function define_structure() { + + // Settings to use. + $userinfo = $this->get_setting_value('users'); + $history = $this->get_setting_value('grade_histories'); + + // Create the nested elements. + $bookhistory = new backup_nested_element('grade_history'); + $grades = new backup_nested_element('grade_grades'); + $grade = new backup_nested_element('grade_grade', array('id'), array( + 'action', 'oldid', 'source', 'loggeduser', 'itemid', 'userid', + 'rawgrade', 'rawgrademax', 'rawgrademin', 'rawscaleid', + 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime', 'exported', 'overridden', + 'excluded', 'feedback', 'feedbackformat', 'information', + 'informationformat', 'timemodified')); + + // Build the tree. + $bookhistory->add_child($grades); + $grades->add_child($grade); + + // This only happens if we are including user info and history. + if ($userinfo && $history) { + // Only keep the history of grades related to items which have been backed up, The query is + // similar (but not identical) to the one used in backup_gradebook_structure_step::define_structure(). + $gradesql = "SELECT ggh.* + FROM {grade_grades_history} ggh + JOIN {grade_items} gi ON ggh.itemid = gi.id + WHERE gi.courseid = :courseid + AND (gi.itemtype = 'manual' OR gi.itemtype = 'course' OR gi.itemtype = 'category')"; + $grade->set_source_sql($gradesql, array('courseid' => backup::VAR_COURSEID)); + } + + // Annotations. (Final annotations as this step is part of the final task). + $grade->annotate_ids('scalefinal', 'rawscaleid'); + $grade->annotate_ids('userfinal', 'loggeduser'); + $grade->annotate_ids('userfinal', 'userid'); + $grade->annotate_ids('userfinal', 'usermodified'); + + // Return the root element. + return $bookhistory; + } + +} + /** * structure step in charge if constructing the completion.xml file for all the users completion * information in a given activity @@ -2162,6 +2221,56 @@ class backup_activity_grades_structure_step extends backup_structure_step { } } +/** + * Structure step in charge of constructing the grade history of an activity. + * + * This step is added to the task regardless of the setting 'grade_histories'. + * The reason is to allow for a more flexible step in case the logic needs to be + * split accross different settings to control the history of items and/or grades. + */ +class backup_activity_grade_history_structure_step extends backup_structure_step { + + protected function define_structure() { + + // Settings to use. + $userinfo = $this->get_setting_value('userinfo'); + $history = $this->get_setting_value('grade_histories'); + + // Create the nested elements. + $bookhistory = new backup_nested_element('grade_history'); + $grades = new backup_nested_element('grade_grades'); + $grade = new backup_nested_element('grade_grade', array('id'), array( + 'action', 'oldid', 'source', 'loggeduser', 'itemid', 'userid', + 'rawgrade', 'rawgrademax', 'rawgrademin', 'rawscaleid', + 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime', 'exported', 'overridden', + 'excluded', 'feedback', 'feedbackformat', 'information', + 'informationformat', 'timemodified')); + + // Build the tree. + $bookhistory->add_child($grades); + $grades->add_child($grade); + + // This only happens if we are including user info and history. + if ($userinfo && $history) { + // Define sources. Only select the history related to existing activity items. + $grade->set_source_sql("SELECT ggh.* + FROM {grade_grades_history} ggh + JOIN {backup_ids_temp} bi ON ggh.itemid = bi.itemid + WHERE bi.backupid = ? + AND bi.itemname = 'grade_item'", array(backup::VAR_BACKUPID)); + } + + // Annotations. + $grade->annotate_ids('scalefinal', 'rawscaleid'); // Straight as scalefinal because it's > 0. + $grade->annotate_ids('user', 'loggeduser'); + $grade->annotate_ids('user', 'userid'); + $grade->annotate_ids('user', 'usermodified'); + + // Return the root element. + return $bookhistory; + } +} + /** * Backups up the course completion information for the course. */ diff --git a/backup/moodle2/restore_activity_task.class.php b/backup/moodle2/restore_activity_task.class.php index cac682493ae..24a045687cf 100644 --- a/backup/moodle2/restore_activity_task.class.php +++ b/backup/moodle2/restore_activity_task.class.php @@ -163,6 +163,9 @@ abstract class restore_activity_task extends restore_task { // Advanced grading methods attached to the module $this->add_step(new restore_activity_grading_structure_step('activity_grading', 'grading.xml')); + // Grade history. The setting 'grade_history' is handled in the step. + $this->add_step(new restore_activity_grade_history_structure_step('activity_grade_history', 'grade_history.xml')); + // Userscompletion (conditionally) if ($this->get_setting_value('userscompletion')) { $this->add_step(new restore_userscompletion_structure_step('activity_userscompletion', 'completion.xml')); diff --git a/backup/moodle2/restore_final_task.class.php b/backup/moodle2/restore_final_task.class.php index 985cd723271..e355871a622 100644 --- a/backup/moodle2/restore_final_task.class.php +++ b/backup/moodle2/restore_final_task.class.php @@ -58,6 +58,7 @@ class restore_final_task extends restore_task { // Gradebook. Don't restore the gradebook unless activities are being restored. if ($this->get_setting_value('activities')) { $this->add_step(new restore_gradebook_structure_step('gradebook_step','gradebook.xml')); + $this->add_step(new restore_grade_history_structure_step('grade_history', 'grade_history.xml')); } // Course completion, executed conditionally if restoring to new course diff --git a/backup/moodle2/restore_root_task.class.php b/backup/moodle2/restore_root_task.class.php index 0239d01df2e..61e2c5c8f74 100644 --- a/backup/moodle2/restore_root_task.class.php +++ b/backup/moodle2/restore_root_task.class.php @@ -246,5 +246,9 @@ class restore_root_task extends restore_task { $gradehistories->get_ui()->set_changeable($changeable); $this->add_setting($gradehistories); $users->add_dependency($gradehistories); + + // The restore does not process the grade histories when some activities are ignored. + // So let's define a dependency to prevent false expectations from our users. + $activities->add_dependency($gradehistories); } } diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 5d82e3af060..e6251150c4c 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -224,6 +224,7 @@ class restore_gradebook_structure_step extends restore_structure_step { global $DB; $data = (object)$data; + $oldid = $data->id; $olduserid = $data->userid; $data->itemid = $this->get_new_parentid('grade_item'); @@ -243,6 +244,7 @@ class restore_gradebook_structure_step extends restore_structure_step { $this->log($message, backup::LOG_DEBUG); } else { $newitemid = $DB->insert_record('grade_grades', $data); + $this->set_mapping('grade_grades', $oldid, $newitemid); } } else { $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'"; @@ -445,6 +447,85 @@ class restore_gradebook_structure_step extends restore_structure_step { } } +/** + * Step in charge of restoring the grade history of a course. + * + * The execution conditions are itendical to {@link restore_gradebook_structure_step} because + * we do not want to restore the history if the gradebook and its content has not been + * restored. At least for now. + */ +class restore_grade_history_structure_step extends restore_structure_step { + + protected function execute_condition() { + global $CFG, $DB; + + // No gradebook info found, don't execute. + $fullpath = $this->task->get_taskbasepath(); + $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; + if (!file_exists($fullpath)) { + return false; + } + + // Some module present in backup file isn't available to restore in this site, don't execute. + if ($this->task->is_missing_modules()) { + return false; + } + + // Some activity has been excluded to be restored, don't execute. + if ($this->task->is_excluding_activities()) { + return false; + } + + // There should only be one grade category (the 1 associated with the course itself). + $category = new stdclass(); + $category->courseid = $this->get_courseid(); + $catcount = $DB->count_records('grade_categories', (array)$category); + if ($catcount > 1) { + return false; + } + + // Arrived here, execute the step. + return true; + } + + protected function define_structure() { + $paths = array(); + + // Settings to use. + $userinfo = $this->get_setting_value('users'); + $history = $this->get_setting_value('grade_histories'); + + if ($userinfo && $history) { + $paths[] = new restore_path_element('grade_grade', + '/grade_history/grade_grades/grade_grade'); + } + + return $paths; + } + + protected function process_grade_grade($data) { + global $DB; + + $data = (object)($data); + $olduserid = $data->userid; + unset($data->id); + + $data->userid = $this->get_mappingid('user', $data->userid, null); + if (!empty($data->userid)) { + // Do not apply the date offsets as this is history. + $data->itemid = $this->get_mappingid('grade_item', $data->itemid); + $data->oldid = $this->get_mappingid('grade_grades', $data->oldid); + $data->usermodified = $this->get_mappingid('user', $data->usermodified, null); + $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid); + $DB->insert_record('grade_grades_history', $data); + } else { + $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'"; + $this->log($message, backup::LOG_DEBUG); + } + } + +} + /** * decode all the interlinks present in restored content * relying 100% in the restore_decode_processor that handles @@ -2898,6 +2979,7 @@ class restore_activity_grades_structure_step extends restore_structure_step { protected function process_grade_grade($data) { $data = (object)($data); $olduserid = $data->userid; + $oldid = $data->id; unset($data->id); $data->itemid = $this->get_new_parentid('grade_item'); @@ -2911,7 +2993,7 @@ class restore_activity_grades_structure_step extends restore_structure_step { $grade = new grade_grade($data, false); $grade->insert('restore'); - // no need to save any grade_grade mapping + $this->set_mapping('grade_grades', $oldid, $grade->id); } else { debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'"); } @@ -2944,6 +3026,52 @@ class restore_activity_grades_structure_step extends restore_structure_step { } } +/** + * Step in charge of restoring the grade history of an activity. + * + * This step is added to the task regardless of the setting 'grade_histories'. + * The reason is to allow for a more flexible step in case the logic needs to be + * split accross different settings to control the history of items and/or grades. + */ +class restore_activity_grade_history_structure_step extends restore_structure_step { + + protected function define_structure() { + $paths = array(); + + // Settings to use. + $userinfo = $this->get_setting_value('userinfo'); + $history = $this->get_setting_value('grade_histories'); + + if ($userinfo && $history) { + $paths[] = new restore_path_element('grade_grade', + '/grade_history/grade_grades/grade_grade'); + } + + return $paths; + } + + protected function process_grade_grade($data) { + global $DB; + + $data = (object) $data; + $olduserid = $data->userid; + unset($data->id); + + $data->userid = $this->get_mappingid('user', $data->userid, null); + if (!empty($data->userid)) { + // Do not apply the date offsets as this is history. + $data->itemid = $this->get_mappingid('grade_item', $data->itemid); + $data->oldid = $this->get_mappingid('grade_grades', $data->oldid); + $data->usermodified = $this->get_mappingid('user', $data->usermodified, null); + $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid); + $DB->insert_record('grade_grades_history', $data); + } else { + $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'"; + $this->log($message, backup::LOG_DEBUG); + } + } + +} /** * This structure steps restores one instance + positions of one block