From 7a7b8a1f194fbfbb1b10d8460b3d275bef7faf9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Fri, 31 Aug 2012 14:15:04 +0200 Subject: [PATCH] MDL-35071 redesign enrol restore Includes option to convert all enrolments to enrol_manual instances, support for mapping of custom fields and fixes for several other problems. This does not include support for custom enrol tables, it will be addressed in another issue. --- backup/moodle2/backup_stepslib.php | 7 +- backup/moodle2/restore_course_task.class.php | 7 +- backup/moodle2/restore_root_task.class.php | 6 + backup/moodle2/restore_stepslib.php | 139 +++++++++++------- .../plan/restore_structure_step.class.php | 1 + enrol/category/lib.php | 11 ++ enrol/guest/lib.php | 26 ++-- enrol/manual/lib.php | 93 ++++++++++-- enrol/self/lib.php | 72 +++++++-- enrol/upgrade.txt | 2 + lang/en/backup.php | 1 + lib/enrollib.php | 55 ++++++- 12 files changed, 316 insertions(+), 104 deletions(-) diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index 850bb2ec2a6..3578063751f 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -520,7 +520,7 @@ class backup_enrolments_structure_step extends backup_structure_step { $enrols = new backup_nested_element('enrols'); $enrol = new backup_nested_element('enrol', array('id'), array( - 'enrol', 'status', 'sortorder', 'name', 'enrolperiod', 'enrolstartdate', + 'enrol', 'status', 'name', 'enrolperiod', 'enrolstartdate', 'enrolenddate', 'expirynotify', 'expirytreshold', 'notifyall', 'password', 'cost', 'currency', 'roleid', 'customint1', 'customint2', 'customint3', 'customint4', 'customint5', 'customint6', 'customint7', 'customint8', @@ -541,9 +541,8 @@ class backup_enrolments_structure_step extends backup_structure_step { $enrol->add_child($userenrolments); $userenrolments->add_child($enrolment); - // Define sources - - $enrol->set_source_table('enrol', array('courseid' => backup::VAR_COURSEID)); + // Define sources - the instances are restored using the same sortorder, we do not need to store it in xml and deal with it afterwards. + $enrol->set_source_sql("SELECT * FROM {enrol} WHERE courseid = :courseid ORDER BY sortorder", array('courseid' => backup::VAR_COURSEID)); // User enrolments only added only if users included if ($users) { diff --git a/backup/moodle2/restore_course_task.class.php b/backup/moodle2/restore_course_task.class.php index f39e6c63607..a43909b8bf3 100644 --- a/backup/moodle2/restore_course_task.class.php +++ b/backup/moodle2/restore_course_task.class.php @@ -71,14 +71,15 @@ class restore_course_task extends restore_task { $this->add_step(new restore_course_structure_step('course_info', 'course.xml')); } - // Restore course role assignments and overrides (internally will observe the role_assignments setting) - $this->add_step(new restore_ras_and_caps_structure_step('course_ras_and_caps', 'roles.xml')); - // Restore course enrolments (plugins and membership). Conditionally prevented for any IMPORT/HUB operation if ($this->plan->get_mode() != backup::MODE_IMPORT && $this->plan->get_mode() != backup::MODE_HUB) { $this->add_step(new restore_enrolments_structure_step('course_enrolments', 'enrolments.xml')); } + // Restore course role assignments and overrides (internally will observe the role_assignments setting), + // this must be done after all users are enrolled. + $this->add_step(new restore_ras_and_caps_structure_step('course_ras_and_caps', 'roles.xml')); + // Restore course filters (conditionally) if ($this->get_setting_value('filters')) { $this->add_step(new restore_filters_structure_step('course_filters', 'filters.xml')); diff --git a/backup/moodle2/restore_root_task.class.php b/backup/moodle2/restore_root_task.class.php index 6917f1cfb00..f95a5b9971f 100644 --- a/backup/moodle2/restore_root_task.class.php +++ b/backup/moodle2/restore_root_task.class.php @@ -112,6 +112,12 @@ class restore_root_task extends restore_task { $users->get_ui()->set_changeable($changeable); $this->add_setting($users); + $rootenrolmanual = new restore_users_setting('enrol_migratetomanual', base_setting::IS_BOOLEAN, false); + $rootenrolmanual->set_ui(new backup_setting_ui_checkbox($rootenrolmanual, get_string('rootenrolmanual', 'backup'))); + $rootenrolmanual->get_ui()->set_changeable(enrol_is_enabled('manual')); + $this->add_setting($rootenrolmanual); + $users->add_dependency($rootenrolmanual); + // Define role_assignments (dependent of users) $defaultvalue = false; // Safer default $changeable = false; diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 8742f0a25b4..2f597a693c4 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -1407,13 +1407,14 @@ class restore_course_structure_step extends restore_structure_step { /* * Structure step that will read the roles.xml file (at course/activity/block levels) - * containig all the role_assignments and overrides for that context. If corresponding to + * containing all the role_assignments and overrides for that context. If corresponding to * one mapped role, they will be applied to target context. Will observe the role_assignments * setting to decide if ras are restored. - * Note: only ras with component == null are restored as far as the any ra with component - * is handled by one enrolment plugin, hence it will createt the ras later + * + * Note: this needs to be executed after all users are enrolled. */ class restore_ras_and_caps_structure_step extends restore_structure_step { + protected $plugins = null; protected function define_structure() { @@ -1462,15 +1463,15 @@ class restore_ras_and_caps_structure_step extends restore_structure_step { role_assign($newroleid, $newuserid, $contextid); } else if ((strpos($data->component, 'enrol_') === 0)) { - // Deal with enrolment roles + // Deal with enrolment roles - ignore the component and just find out the instance via new id, + // it is possible that enrolment was restored using different plugin type. + if (!isset($this->plugins)) { + $this->plguins = enrol_get_plugins(true); + } if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) { - if ($component = $DB->get_field('enrol', 'component', array('id'=>$enrolid))) { - //note: we have to verify component because it might have changed - if ($component === 'enrol_manual') { - // manual is a special case, we do not use components - this owudl happen when converting from other plugin - role_assign($newroleid, $newuserid, $contextid); //TODO: do we need modifierid? - } else { - role_assign($newroleid, $newuserid, $contextid, $component, $enrolid); //TODO: do we need modifierid? + if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) { + if (isset($this->plguins[$instance->enrol])) { + $this->plguins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid); } } } @@ -1496,6 +1497,9 @@ class restore_ras_and_caps_structure_step extends restore_structure_step { * enrolments, performing all the mappings and/or movements required */ class restore_enrolments_structure_step extends restore_structure_step { + protected $enrolsynced = false; + protected $plugins = null; + protected $originalstatus = array(); /** * Conditionally decide if this step should be executed. @@ -1542,82 +1546,103 @@ class restore_enrolments_structure_step extends restore_structure_step { global $DB; $data = (object)$data; - $oldid = $data->id; // We'll need this later + $oldid = $data->id; // We'll need this later. + unset($data->id); - $restoretype = plugin_supports('enrol', $data->enrol, ENROL_RESTORE_TYPE, null); + $this->originalstatus[$oldid] = $data->status; - if ($restoretype !== ENROL_RESTORE_EXACT and $restoretype !== ENROL_RESTORE_NOUSERS) { - // TODO: add complex restore support via custom class - debugging("Skipping '{$data->enrol}' enrolment plugin. Will be implemented before 2.0 release", DEBUG_DEVELOPER); + if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) { $this->set_mapping('enrol', $oldid, 0); return; } - // Perform various checks to decide what to do with the enrol plugin - if (!array_key_exists($data->enrol, enrol_get_plugins(false))) { - // TODO: decide if we want to switch to manual enrol - we need UI for this - debugging("Enrol plugin data can not be restored because it is not installed"); - $this->set_mapping('enrol', $oldid, 0); - return; - - } - if (!enrol_is_enabled($data->enrol)) { - // TODO: decide if we want to switch to manual enrol - we need UI for this - debugging("Enrol plugin data can not be restored because it is not enabled"); - $this->set_mapping('enrol', $oldid, 0); - return; + if (!isset($this->plugins)) { + $this->plugins = enrol_get_plugins(true); } - // map standard fields - plugin has to process custom fields from own restore class - $data->roleid = $this->get_mappingid('role', $data->roleid); - //TODO: should we move the enrol start and end date here? - - // always add instance, if the course does not support multiple instances it just returns NULL - $enrol = enrol_get_plugin($data->enrol); - $courserec = $DB->get_record('course', array('id' => $this->get_courseid())); // Requires object, uses only id!! - if ($newitemid = $enrol->add_instance($courserec, (array)$data)) { - // ok - } else { - if ($instances = $DB->get_records('enrol', array('courseid'=>$courserec->id, 'enrol'=>$data->enrol))) { - // most probably plugin that supports only one instance - $newitemid = key($instances); - } else { - debugging('Can not create new enrol instance or reuse existing'); - $newitemid = 0; + if (!$this->enrolsynced) { + // Make sure that all plugin may create instances and enrolments automatically + // before the first instance restore - this is suitable especially for plugins + // that synchronise data automatically using course->idnumber or by course categories. + foreach ($this->plugins as $plugin) { + $plugin->restore_sync_course($courserec); } + $this->enrolsynced = true; } - if ($restoretype === ENROL_RESTORE_NOUSERS) { - // plugin requests to prevent restore of any users - $newitemid = 0; - } + // Map standard fields - plugin has to process custom fields manually. + $data->roleid = $this->get_mappingid('role', $data->roleid); + $data->courseid = $courserec->id; - $this->set_mapping('enrol', $oldid, $newitemid); + if ($this->get_setting_value('enrol_migratetomanual')) { + unset($data->sortorder); // Remove useless sortorder from <2.4 backups. + if (!enrol_is_enabled('manual')) { + $this->set_mapping('enrol', $oldid, 0); + return; + } + if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) { + $instance = reset($instances); + $this->set_mapping('enrol', $oldid, $instance->id); + } else { + if ($data->enrol === 'manual') { + $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data); + } else { + $instanceid = $this->plugins['manual']->add_default_instance($courserec); + } + $this->set_mapping('enrol', $oldid, $instanceid); + } + + } else { + if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) { + debugging("Enrol plugin data can not be restored because it is not enabled, use migration to manual enrolments"); + $this->set_mapping('enrol', $oldid, 0); + return; + } + if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) { + // Let's keep the sortorder in old backups. + } else { + // Prevent problems with colliding sortorders in old backups, + // new 2.4 backups do not need sortorder because xml elements are ordered properly. + unset($data->sortorder); + } + // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type. + $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid); + } } /** - * Create user enrolments + * Create user enrolments. * * This has to be called after creation of enrolment instances * and before adding of role assignments. * + * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards. + * * @param mixed $data * @return void */ public function process_enrolment($data) { global $DB; + if (!isset($this->plugins)) { + $this->plugins = enrol_get_plugins(true); + } + $data = (object)$data; - // Process only if parent instance have been mapped + // Process only if parent instance have been mapped. if ($enrolid = $this->get_new_parentid('enrol')) { + $oldinstancestatus = ENROL_INSTANCE_ENABLED; + $oldenrolid = $this->get_old_parentid('enrol'); + if (isset($this->originalstatus[$oldenrolid])) { + $oldinstancestatus = $this->originalstatus[$oldenrolid]; + } if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) { - // And only if user is a mapped one + // And only if user is a mapped one. if ($userid = $this->get_mappingid('user', $data->userid)) { - $enrol = enrol_get_plugin($instance->enrol); - //TODO: do we need specify modifierid? - $enrol->enrol_user($instance, $userid, null, $data->timestart, $data->timeend, $data->status); - //note: roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing above + if (isset($this->plugins[$instance->enrol])) { + $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus); + } } } } diff --git a/backup/util/plan/restore_structure_step.class.php b/backup/util/plan/restore_structure_step.class.php index 7a1cfa28fdf..da344872f10 100644 --- a/backup/util/plan/restore_structure_step.class.php +++ b/backup/util/plan/restore_structure_step.class.php @@ -278,6 +278,7 @@ abstract class restore_structure_step extends restore_step { /** * As far as restore structure steps are implementing restore_plugin stuff, they need to * have the parent task available for wrapping purposes (get course/context....) + * @return restore_task|null */ public function get_task() { return $this->task; diff --git a/enrol/category/lib.php b/enrol/category/lib.php index e6525e36b56..4fed790f035 100644 --- a/enrol/category/lib.php +++ b/enrol/category/lib.php @@ -92,4 +92,15 @@ class enrol_category_plugin extends enrol_plugin { require_once("$CFG->dirroot/enrol/category/locallib.php"); enrol_category_sync_course($course); } + + /** + * Automatic enrol sync executed during restore. + * Useful for automatic sync by course->idnumber or course category. + * @param stdClass $course course record + */ + public function restore_sync_course($course) { + global $CFG; + require_once("$CFG->dirroot/enrol/category/locallib.php"); + enrol_category_sync_course($course); + } } diff --git a/enrol/guest/lib.php b/enrol/guest/lib.php index 02aec702549..acd2536e390 100644 --- a/enrol/guest/lib.php +++ b/enrol/guest/lib.php @@ -366,18 +366,22 @@ class enrol_guest_plugin extends enrol_plugin { return $this->add_instance($course, $fields); } -} + /** + * Restore instance and map settings. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $course + * @param int $oldid + */ + public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { + global $DB; -/** - * Indicates API features that the enrol plugin supports. - * - * @param string $feature - * @return mixed True if yes (some features may use other values) - */ -function enrol_guest_supports($feature) { - switch($feature) { - case ENROL_RESTORE_TYPE: return ENROL_RESTORE_NOUSERS; + if (!$DB->record_exists('enrol', array('courseid' => $data->courseid, 'enrol' => $this->get_name()))) { + $this->add_instance($course, (array)$data); + } - default: return null; + // No need to set mapping, we do not restore users or roles here. + $step->set_mapping('enrol', $oldid, 0); } } diff --git a/enrol/manual/lib.php b/enrol/manual/lib.php index 2cf94bb7793..fb648e07a6f 100644 --- a/enrol/manual/lib.php +++ b/enrol/manual/lib.php @@ -295,18 +295,89 @@ class enrol_manual_plugin extends enrol_plugin { ); return $bulkoperations; } -} -/** - * Indicates API features that the enrol plugin supports. - * - * @param string $feature - * @return mixed True if yes (some features may use other values) - */ -function enrol_manual_supports($feature) { - switch($feature) { - case ENROL_RESTORE_TYPE: return ENROL_RESTORE_EXACT; + /** + * Restore instance and map settings. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $course + * @param int $oldid + */ + public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { + global $DB; + // There is only I manual enrol instance allowed per course. + if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) { + $instance = reset($instances); + $instanceid = $instance->id; + } else { + $instanceid = $this->add_instance($course, (array)$data); + } + $step->set_mapping('enrol', $oldid, $instanceid); + } - default: return null; + /** + * Restore user enrolment. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $instance + * @param int $oldinstancestatus + * @param int $userid + */ + public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) { + global $DB; + + // Note: this is a bit tricky because other types may be converted to manual enrolments, + // and manual is restricted to one enrolment per user. + + $ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid)); + $enrol = false; + if ($ue and $ue->status == ENROL_USER_ACTIVE) { + // We do not want to restrict current active enrolments, let's kind of merge the times only. + // This prevents some teacher lockouts too. + if ($data->status == ENROL_USER_ACTIVE) { + if ($data->timestart > $ue->timestart) { + $data->timestart = $ue->timestart; + $enrol = true; + } + + if ($data->timeend == 0) { + if ($ue->timeend != 0) { + $enrol = true; + } + } else if ($ue->timeend == 0) { + $data->timeend = 0; + } else if ($data->timeend < $ue->timeend) { + $data->timeend = $ue->timeend; + $enrol = true; + } + } + } else { + if ($instance->status == ENROL_INSTANCE_ENABLED and $oldinstancestatus != ENROL_INSTANCE_ENABLED) { + // Make sure that user enrolments are not activated accidentally, + // we do it only here because it is not expected that enrolments are migrated to other plugins. + $data->status = ENROL_USER_SUSPENDED; + } + $enrol = true; + } + + if ($enrol) { + $this->enrol_user($instance, $userid, null, $data->timestart, $data->timeend, $data->status); + } + } + + /** + * Restore role assignment. + * + * @param stdClass $instance + * @param int $roleid + * @param int $userid + * @param int $contextid + */ + public function restore_role_assignment($instance, $roleid, $userid, $contextid) { + // This is necessary only because we may migrate other types to this instance, + // we do not use component in manual or self enrol. + role_assign($roleid, $userid, $contextid, '', 0); } } diff --git a/enrol/self/lib.php b/enrol/self/lib.php index fda2414301b..8c459f9f5b6 100644 --- a/enrol/self/lib.php +++ b/enrol/self/lib.php @@ -403,18 +403,68 @@ class enrol_self_plugin extends enrol_plugin { } return $actions; } -} -/** - * Indicates API features that the enrol plugin supports. - * - * @param string $feature - * @return mixed true if yes (some features may use other values) - */ -function enrol_self_supports($feature) { - switch($feature) { - case ENROL_RESTORE_TYPE: return ENROL_RESTORE_EXACT; + /** + * Restore instance and map settings. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $course + * @param int $oldid + */ + public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { + global $DB; + if ($step->get_task()->get_target() == backup::TARGET_NEW_COURSE) { + $merge = false; + } else { + $merge = array( + 'courseid' => $data->courseid, + 'enrol' => $this->get_name(), + 'roleid' => $data->roleid, + ); + } + if ($merge and $instances = $DB->get_records('enrol', $merge, 'id')) { + $instance = reset($instances); + $instanceid = $instance->id; + } else { + if (!empty($data->customint5)) { + if ($step->get_task()->is_samesite()) { + // Keep cohort restriction unchanged - we are on the same site. + } else { + // Use some id that can not exist in order to prevent self enrolment, + // because we do not know what cohort it is in this site. + $data->customint5 = -1; + } + } + $instanceid = $this->add_instance($course, (array)$data); + } + $step->set_mapping('enrol', $oldid, $instanceid); + } - default: return null; + /** + * Restore user enrolment. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $instance + * @param int $oldinstancestatus + * @param int $userid + */ + public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) { + $this->enrol_user($instance, $userid, null, $data->timestart, $data->timeend, $data->status); + } + + /** + * Restore role assignment. + * + * @param stdClass $instance + * @param int $roleid + * @param int $userid + * @param int $contextid + */ + public function restore_role_assignment($instance, $roleid, $userid, $contextid) { + // This is necessary only because we may migrate other types to this instance, + // we do not use component in manual or self enrol. + role_assign($roleid, $userid, $contextid, '', 0); } } diff --git a/enrol/upgrade.txt b/enrol/upgrade.txt index 8fcd760ef97..85dbf7808e6 100644 --- a/enrol/upgrade.txt +++ b/enrol/upgrade.txt @@ -7,6 +7,8 @@ information provided here is intended especially for developers. required changes in code: * use role_get_name() or role_fix_names() if you need any role names, using role.name directly from database is not correct any more +* new restore support: ENROL_RESTORE_EXACT, ENROL_RESTORE_NOUSERS + and ENROL_RESTORE_CLASS were removed, implement new restore_* plugin methods instead other changes: * course enrolment manager now works with disabled plugins too diff --git a/lang/en/backup.php b/lang/en/backup.php index e053c96bdee..23216e29e63 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -210,6 +210,7 @@ $string['restoretonewcourse'] = 'Restore as a new course'; $string['restoringcourse'] = 'Course restoration in progress'; $string['restoringcourseshortname'] = 'restoring'; $string['restorerolemappings'] = 'Restore role mappings'; +$string['rootenrolmanual'] = 'Restore as manual enrolments'; $string['rootsettings'] = 'Backup settings'; $string['rootsettingusers'] = 'Include enrolled users'; $string['rootsettinganonymize'] = 'Anonymize user information'; diff --git a/lib/enrollib.php b/lib/enrollib.php index b437a48f1b1..ca626060b60 100644 --- a/lib/enrollib.php +++ b/lib/enrollib.php @@ -51,14 +51,8 @@ define('ENROL_EXT_REMOVED_UNENROL', 0); /** When user disappears from external source, the enrolment is kept as is - one way sync */ define('ENROL_EXT_REMOVED_KEEP', 1); -/** enrol plugin feature describing requested restore type */ +/** @deprecated since 2.4 not used any more, migrate plugin to new restore methods */ define('ENROL_RESTORE_TYPE', 'enrolrestore'); -/** User custom backup/restore class stored in backup/moodle2/ subdirectory */ -define('ENROL_RESTORE_CLASS', 'class'); -/** Restore all custom fields from enrol table without any changes and all user_enrolments records */ -define('ENROL_RESTORE_EXACT', 'exact'); -/** Restore enrol record like ENROL_RESTORE_EXACT, but no user enrolments */ -define('ENROL_RESTORE_NOUSERS', 'nousers'); /** * When user disappears from external source, user enrolment is suspended, roles are kept as is. @@ -1767,4 +1761,51 @@ abstract class enrol_plugin { public function get_bulk_operations(course_enrolment_manager $manager) { return array(); } + + /** + * Automatic enrol sync executed during restore. + * Useful for automatic sync by course->idnumber or course category. + * @param stdClass $course course record + */ + public function restore_sync_course($course) { + // Override if necessary. + } + + /** + * Restore instance and map settings. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $course + * @param int $oldid + */ + public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { + // Do not call this from overridden methods, restore and set new id there. + $step->set_mapping('enrol', $oldid, 0); + } + + /** + * Restore user enrolment. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $instance + * @param int $oldinstancestatus + * @param int $userid + */ + public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) { + // Override as necessary if plugin supports restore of enrolments. + } + + /** + * Restore role assignment. + * + * @param stdClass $instance + * @param int $roleid + * @param int $userid + * @param int $contextid + */ + public function restore_role_assignment($instance, $roleid, $userid, $contextid) { + // No role assignment by default, override if necessary. + } }