MDL-80191 backup: delegate section backup and restore
This commit adds all the logic to allow delegate sections backup and restore. The backup and restore process is quite complex and it is not designed to have task hierarchy. To solve the subsection problem, the backup/restore planners do not include delegated sections at a course level, but they are included when the activity with delegated section is processed. To allow restoing, the activity is responsible to store the component/itemid mapping in the backup_structure_dbops. This way, when the delegated section is restored (delegated sections are always processed right after the parent activity) it can use the itemid mapping.
This commit is contained in:
@@ -133,6 +133,9 @@ abstract class backup_plan_builder {
|
||||
try {
|
||||
$plan->add_task(backup_factory::get_backup_activity_task($controller->get_format(), $id));
|
||||
|
||||
// Some activities may have delegated section integrations.
|
||||
self::build_delegated_section_plan($controller, $id);
|
||||
|
||||
// For the given activity, add as many block tasks as necessary
|
||||
$blockids = backup_plan_dbops::get_blockids_from_moduleid($id);
|
||||
foreach ($blockids as $blockid) {
|
||||
@@ -150,6 +153,44 @@ abstract class backup_plan_builder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a course module delegated section backup plan.
|
||||
* @param backup_controller $controller
|
||||
* @param int $cmid the parent course module id.
|
||||
*/
|
||||
protected static function build_delegated_section_plan($controller, $cmid) {
|
||||
global $CFG, $DB;
|
||||
|
||||
// Check moduleid exists.
|
||||
if (!$coursemodule = get_coursemodule_from_id(false, $cmid)) {
|
||||
$controller->log(get_string('error_course_module_not_found', 'backup', $cmid), backup::LOG_WARNING);
|
||||
}
|
||||
$classname = 'mod_' . $coursemodule->modname . '\courseformat\sectiondelegate';
|
||||
if (!class_exists($classname)) {
|
||||
return;
|
||||
}
|
||||
$sectionid = null;
|
||||
try {
|
||||
$sectionid = $classname::delegated_section_id($coursemodule);
|
||||
} catch (dml_exception $error) {
|
||||
$controller->log(get_string('error_delegate_section_not_found', 'backup', $cmid), backup::LOG_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
$plan = $controller->get_plan();
|
||||
$sectiontask = backup_factory::get_backup_section_task($controller->get_format(), $sectionid);
|
||||
$sectiontask->set_delegated_cm($cmid);
|
||||
$plan->add_task($sectiontask);
|
||||
|
||||
// For the given section, add as many activity tasks as necessary.
|
||||
$coursemodules = backup_plan_dbops::get_modules_from_sectionid($sectionid);
|
||||
foreach ($coursemodules as $coursemodule) {
|
||||
if (plugin_supports('mod', $coursemodule->modname, FEATURE_BACKUP_MOODLE2)) {
|
||||
self::build_activity_plan($controller, $coursemodule->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build one 1-section backup
|
||||
*/
|
||||
@@ -185,8 +226,13 @@ abstract class backup_plan_builder {
|
||||
|
||||
// For the given course, add as many section tasks as necessary
|
||||
$sections = backup_plan_dbops::get_sections_from_courseid($id);
|
||||
foreach ($sections as $section) {
|
||||
self::build_section_plan($controller, $section);
|
||||
foreach ($sections as $sectionid) {
|
||||
// Delegated sections are not course responsability.
|
||||
$sectiondata = backup_plan_dbops::get_section_from_id($sectionid);
|
||||
if (!empty($sectiondata->component)) {
|
||||
continue;
|
||||
}
|
||||
self::build_section_plan($controller, $sectionid);
|
||||
}
|
||||
|
||||
// For the given course, add as many block tasks as necessary
|
||||
|
||||
@@ -42,6 +42,11 @@ class backup_section_task extends backup_task {
|
||||
*/
|
||||
protected stdClass $section;
|
||||
|
||||
/**
|
||||
* @var int|null $delegatedcmid the course module that is delegating this section (if any)
|
||||
*/
|
||||
protected ?int $delegatedcmid = null;
|
||||
|
||||
/**
|
||||
* Constructor - instantiates one object of this class
|
||||
*/
|
||||
@@ -59,6 +64,39 @@ class backup_section_task extends backup_task {
|
||||
parent::__construct($name, $plan);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the course module that is delegating this section.
|
||||
*
|
||||
* Delegated section can belong to any kind of plugin. However, when a delegated
|
||||
* section belongs to a course module, the UI will present all settings according.
|
||||
*
|
||||
* @param int $cmid the course module id that is delegating this section
|
||||
*/
|
||||
public function set_delegated_cm(int $cmid) {
|
||||
$this->delegatedcmid = $cmid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the course module that is delegating this section.
|
||||
*
|
||||
* @return int|null the course module id that is delegating this section
|
||||
*/
|
||||
public function get_delegated_cm(): ?int {
|
||||
return $this->delegatedcmid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the delegate activity modname (if any).
|
||||
*
|
||||
* @return string|null the modname of the delegated activity
|
||||
*/
|
||||
public function get_modname(): ?string {
|
||||
if (empty($this->section->component)) {
|
||||
return null;
|
||||
}
|
||||
return core_component::normalize_component($this->section->component)[1];
|
||||
}
|
||||
|
||||
public function get_sectionid() {
|
||||
return $this->sectionid;
|
||||
}
|
||||
|
||||
@@ -119,6 +119,20 @@ abstract class backup_activity_structure_step extends backup_structure_step {
|
||||
// Return the root element (activity)
|
||||
return $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a delegate section itemid mapping.
|
||||
*
|
||||
* @param string $pluginname the name of the plugin that is delegating the section.
|
||||
* @param int $itemid the itemid of the section being delegated.
|
||||
*/
|
||||
protected function set_delegated_section_mapping(string $pluginname, int $itemid) {
|
||||
backup_structure_dbops::insert_backup_ids_record(
|
||||
$this->get_backupid(),
|
||||
"course_section::$pluginname::$itemid",
|
||||
$this->task->get_moduleid()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2128,8 +2142,11 @@ class backup_main_structure_step extends backup_structure_step {
|
||||
|
||||
$sections = new backup_nested_element('sections');
|
||||
|
||||
$section = new backup_nested_element('section', null, array(
|
||||
'sectionid', 'title', 'directory'));
|
||||
$section = new backup_nested_element(
|
||||
'section',
|
||||
null,
|
||||
['sectionid', 'title', 'directory', 'parentcmid', 'modname']
|
||||
);
|
||||
|
||||
$course = new backup_nested_element('course', null, array(
|
||||
'courseid', 'title', 'directory'));
|
||||
|
||||
@@ -144,6 +144,9 @@ abstract class restore_plan_builder {
|
||||
$plan->add_task($task);
|
||||
$controller->get_progress()->progress();
|
||||
|
||||
// Some activities may have delegated section integrations.
|
||||
self::build_delegated_section_plan($controller, $infoactivity->moduleid);
|
||||
|
||||
// For the given activity path, add as many block tasks as necessary
|
||||
// TODO: Add blocks, we need to introspect xml here
|
||||
$blocks = backup_general_helper::get_blocks_from_path($task->get_taskbasepath());
|
||||
@@ -161,6 +164,30 @@ abstract class restore_plan_builder {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a course module delegated section backup plan.
|
||||
* @param restore_controller $controller
|
||||
* @param int $cmid the parent course module id.
|
||||
*/
|
||||
protected static function build_delegated_section_plan($controller, $cmid) {
|
||||
$info = $controller->get_info();
|
||||
|
||||
// Find if some section depends on that course module.
|
||||
$delegatedsectionid = null;
|
||||
foreach ($info->sections as $sectionid => $section) {
|
||||
// Delegated sections are not course responsability.
|
||||
if (isset($section->parentcmid) && $section->parentcmid == $cmid) {
|
||||
$delegatedsectionid = $sectionid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$delegatedsectionid) {
|
||||
return;
|
||||
}
|
||||
self::build_section_plan($controller, $delegatedsectionid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore one 1-section backup
|
||||
*/
|
||||
@@ -215,6 +242,10 @@ abstract class restore_plan_builder {
|
||||
|
||||
// For the given course, add as many section tasks as necessary
|
||||
foreach ($info->sections as $sectionid => $section) {
|
||||
// Delegated sections are not course responsability.
|
||||
if (isset($section->parentcmid) && !empty($section->parentcmid)) {
|
||||
continue;
|
||||
}
|
||||
self::build_section_plan($controller, $sectionid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1612,8 +1612,28 @@ class restore_section_structure_step extends restore_structure_step {
|
||||
$section->course = $this->get_courseid();
|
||||
$section->section = $data->number;
|
||||
$section->timemodified = $data->timemodified ?? 0;
|
||||
$section->component = null;
|
||||
$section->itemid = null;
|
||||
|
||||
$secrec = $DB->get_record(
|
||||
'course_sections',
|
||||
['course' => $this->get_courseid(), 'section' => $data->number, 'component' => null]
|
||||
);
|
||||
$createsection = empty($secrec);
|
||||
|
||||
// Delegated sections are always restored as new sections.
|
||||
if (!empty($data->component)) {
|
||||
$section->itemid = $this->get_delegated_section_mapping($data->component, $data->itemid);
|
||||
// If the delegate component does not set the mapping id, the section must be converted
|
||||
// into a regular section. Otherwise, it won't be accessible.
|
||||
$createsection = $createsection || $section->itemid !== null;
|
||||
$section->component = ($section->itemid !== null) ? $data->component : null;
|
||||
// The section number will be always the last of the course, no matter the case.
|
||||
$section->section = $this->get_last_section_number($this->get_courseid()) + 1;
|
||||
|
||||
}
|
||||
// Section doesn't exist, create it with all the info from backup
|
||||
if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
|
||||
if ($createsection) {
|
||||
$section->name = $data->name;
|
||||
$section->summary = $data->summary;
|
||||
$section->summaryformat = $data->summaryformat;
|
||||
@@ -1629,8 +1649,10 @@ class restore_section_structure_step extends restore_structure_step {
|
||||
$data, true);
|
||||
}
|
||||
}
|
||||
$section->component = $data->component ?? null;
|
||||
$section->itemid = $data->itemid ?? null;
|
||||
|
||||
// Delegated sections should be always after the normal sections.
|
||||
$this->displace_delegated_sections_after($section->section);
|
||||
|
||||
$newitemid = $DB->insert_record('course_sections', $section);
|
||||
$section->id = $newitemid;
|
||||
|
||||
@@ -1802,6 +1824,57 @@ class restore_section_structure_step extends restore_structure_step {
|
||||
// Add section related files, with 'course_section' itemid to match
|
||||
$this->add_related_files('course', 'section', 'course_section');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a delegate section mapping.
|
||||
*
|
||||
* @param string $component the component name (frankenstyle)
|
||||
* @param int $oldsectionid The old section id.
|
||||
* @return int|null The new section id or null if not found.
|
||||
*/
|
||||
protected function get_delegated_section_mapping($component, $oldsectionid): ?int {
|
||||
$result = $this->get_mappingid("course_section::$component", $oldsectionid, null);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displace delegated sections after the given section number.
|
||||
*
|
||||
* @param int $sectionnum The section number.
|
||||
*/
|
||||
protected function displace_delegated_sections_after(int $sectionnum): void {
|
||||
global $DB;
|
||||
|
||||
$sectionstomove = $DB->get_records_select(
|
||||
'course_sections',
|
||||
'course = ? AND component IS NOT NULL',
|
||||
[$this->get_courseid()],
|
||||
'section DESC', 'id, section'
|
||||
);
|
||||
foreach ($sectionstomove as $section) {
|
||||
$sectionnum++;
|
||||
$section->section = $sectionnum;
|
||||
$DB->update_record('course_sections', $section);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last section number in the course.
|
||||
*
|
||||
* @param int $courseid The course id.
|
||||
* @param bool $includedelegated If true, include delegated sections in the count.
|
||||
* @return int The last section number.
|
||||
*/
|
||||
protected function get_last_section_number(int $courseid, bool $includedelegated = false): int {
|
||||
global $DB;
|
||||
|
||||
$delegtadefilter = $includedelegated ? '' : ' AND component IS NULL';
|
||||
|
||||
return (int) $DB->get_field_sql(
|
||||
'SELECT max(section) from {course_sections} WHERE course = ?' . $delegtadefilter,
|
||||
[$courseid]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4905,6 +4978,17 @@ abstract class restore_activity_structure_step extends restore_structure_step {
|
||||
$oldid = $this->task->get_old_activityid();
|
||||
$this->set_mapping($modulename, $oldid, $newitemid, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a delegate section mapping.
|
||||
*
|
||||
* @param string $component The component name (frankenstyle)
|
||||
* @param int $olditemid The old section id.
|
||||
* @param int $newitemid The new section id.
|
||||
*/
|
||||
protected function set_delegated_section_mapping($component, $olditemid, $newitemid) {
|
||||
$this->set_mapping("course_section::$component", $olditemid, $newitemid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -103,7 +103,7 @@ class restore_stepslib_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for the section structure step included elements.
|
||||
* Test for delegate section behaviour.
|
||||
*
|
||||
* @covers \restore_section_structure_step::process_section
|
||||
*/
|
||||
@@ -114,7 +114,7 @@ class restore_stepslib_test extends \advanced_testcase {
|
||||
$this->setAdminUser();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 2, 'format' => 'topics']);
|
||||
// Section 2 has an existing delegate class.
|
||||
// Section 2 has an existing delegate class for component that is not an activity.
|
||||
course_update_section(
|
||||
$course,
|
||||
$DB->get_record('course_sections', ['course' => $course->id, 'section' => 2]),
|
||||
@@ -130,16 +130,18 @@ class restore_stepslib_test extends \advanced_testcase {
|
||||
$originalsections = get_fast_modinfo($course->id)->get_section_info_all();
|
||||
$restoredsections = get_fast_modinfo($newcourseid)->get_section_info_all();
|
||||
|
||||
$this->assertEquals(count($originalsections), count($restoredsections));
|
||||
// Delegated sections depends on the plugin to be backuped and restored.
|
||||
// In this case, the plugin is not backuped and restored, so the section is not restored.
|
||||
$this->assertEquals(3, count($originalsections));
|
||||
$this->assertEquals(2, count($restoredsections));
|
||||
|
||||
$validatefields = ['name', 'summary', 'summaryformat', 'visible', 'component', 'itemid'];
|
||||
|
||||
$this->assertEquals($originalsections[1]->name, $restoredsections[1]->name);
|
||||
|
||||
foreach ($validatefields as $field) {
|
||||
$this->assertEquals($originalsections[0]->$field, $restoredsections[0]->$field);
|
||||
$this->assertEquals($originalsections[1]->$field, $restoredsections[1]->$field);
|
||||
$this->assertEquals($originalsections[2]->$field, $restoredsections[2]->$field);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +268,10 @@ abstract class backup_controller_dbops extends backup_dbops {
|
||||
$contentinfo = array(
|
||||
'sectionid' => $task->get_sectionid(),
|
||||
'title' => $task->get_name(),
|
||||
'directory' => 'sections/' . 'section_' . $task->get_sectionid());
|
||||
'directory' => 'sections/' . 'section_' . $task->get_sectionid(),
|
||||
'parentcmid' => $task->get_delegated_cm() ?? '',
|
||||
'modname' => $task->get_modname() ?? '',
|
||||
);
|
||||
|
||||
// Now get section settings
|
||||
// Calculate prefix to find valid settings
|
||||
|
||||
@@ -120,6 +120,17 @@ abstract class backup_plan_dbops extends backup_dbops {
|
||||
return $sectionsarr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given one section id, returns the full section record.
|
||||
*
|
||||
* @param int $sectionid
|
||||
* @return stdClass
|
||||
*/
|
||||
public static function get_section_from_id($sectionid): stdClass {
|
||||
global $DB;
|
||||
return $DB->get_record('course_sections', ['id' => $sectionid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given one course id, return its format in DB
|
||||
*/
|
||||
|
||||
@@ -52,6 +52,29 @@ abstract class sectiondelegatemodule extends sectiondelegate {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the delegated section id controlled by a specific cm.
|
||||
*
|
||||
* This method is used when reverse search is needed bu we cannot access the database.
|
||||
* This happens mostly on backup and restore. Do NOT use for normal operations.
|
||||
*
|
||||
* @param stdClass|cm_info $cm a course module compatible data structure.
|
||||
* @return int the section id.
|
||||
*/
|
||||
public static function delegated_section_id(stdClass|cm_info $cm): int {
|
||||
global $DB;
|
||||
return $DB->get_field(
|
||||
'course_sections',
|
||||
'id',
|
||||
[
|
||||
'course' => $cm->course,
|
||||
'component' => explode('\\', static::class)[0],
|
||||
'itemid' => $cm->instance,
|
||||
],
|
||||
MUST_EXIST
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent section of the current delegated section.
|
||||
*
|
||||
|
||||
@@ -198,6 +198,7 @@ $string['enableasyncbackup_help'] = 'If enabled, backup and restore operations w
|
||||
$string['enterasearch'] = 'Enter a search';
|
||||
$string['error_block_for_module_not_found'] = 'Orphan block instance (id: {$a->bid}) for course module (id: {$a->mid}) found. This block will not be backed up';
|
||||
$string['error_course_module_not_found'] = 'Orphan course module (id: {$a}) found. This module will not be backed up.';
|
||||
$string['error_delegate_section_not_found'] = 'Missing delegate section form course module (id: {$a}. The section will not be backed up.';
|
||||
$string['errorcopyingbackupfile'] = "Failed to copy the backup file to the temporary folder before restoring.";
|
||||
$string['errorfilenamerequired'] = 'You must enter a valid filename for this backup';
|
||||
$string['errorfilenametoolong'] = 'The filename must be less than 255 characters in length.';
|
||||
|
||||
Reference in New Issue
Block a user