Commit 7a7b8a1f authored by Petr Škoda's avatar Petr Škoda
Browse files

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.
parent 935c3d5e
......@@ -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) {
......
......@@ -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'));
......
......@@ -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;
......
......@@ -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 (!isset($this->plugins)) {
$this->plugins = enrol_get_plugins(true);
}
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);
}
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;
$this->enrolsynced = true;
}
// map standard fields - plugin has to process custom fields from own restore class
// Map standard fields - plugin has to process custom fields manually.
$data->roleid = $this->get_mappingid('role', $data->roleid);
//TODO: should we move the enrol start and end date here?
$data->courseid = $courserec->id;
// 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
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 ($instances = $DB->get_records('enrol', array('courseid'=>$courserec->id, 'enrol'=>$data->enrol))) {
// most probably plugin that supports only one instance
$newitemid = key($instances);
if ($data->enrol === 'manual') {
$instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
} else {
debugging('Can not create new enrol instance or reuse existing');
$newitemid = 0;
$instanceid = $this->plugins['manual']->add_default_instance($courserec);
}
$this->set_mapping('enrol', $oldid, $instanceid);
}
if ($restoretype === ENROL_RESTORE_NOUSERS) {
// plugin requests to prevent restore of any users
$newitemid = 0;
} 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);
}
$this->set_mapping('enrol', $oldid, $newitemid);
}
/**
* 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);
}
}
}
}
......
......@@ -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;
......
......@@ -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);
}
}
......@@ -366,18 +366,22 @@ class enrol_guest_plugin extends enrol_plugin {
return $this->add_instance($course, $fields);
}
}
/**
* Indicates API features that the enrol plugin supports.
/**
* Restore instance and map settings.
*
* @param string $feature
* @return mixed True if yes (some features may use other values)
* @param restore_enrolments_structure_step $step
* @param stdClass $data
* @param stdClass $course
* @param int $oldid
*/
function enrol_guest_supports($feature) {
switch($feature) {
case ENROL_RESTORE_TYPE: return ENROL_RESTORE_NOUSERS;
public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
global $DB;
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);
}
}
......@@ -295,18 +295,89 @@ class enrol_manual_plugin extends enrol_plugin {
);
return $bulkoperations;
}
}
/**
* Indicates API features that the enrol plugin supports.
/**
* Restore instance and map settings.
*
* @param string $feature
* @return mixed True if yes (some features may use other values)
* @param restore_enrolments_structure_step $step
* @param stdClass $data
* @param stdClass $course
* @param int $oldid
*/
function enrol_manual_supports($feature) {
switch($feature) {
case ENROL_RESTORE_TYPE: return ENROL_RESTORE_EXACT;
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);
}
}
......@@ -403,18 +403,68 @@ class enrol_self_plugin extends enrol_plugin {
}
return $actions;
}
}
/**
* Indicates API features that the enrol plugin supports.
/**
* 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);
}
/**
* Restore user enrolment.
*
* @param string $feature
* @return mixed true if yes (some features may use other values)
* @param restore_enrolments_structure_step $step
* @param stdClass $data
* @param stdClass $instance
* @param int $oldinstancestatus
* @param int $userid
*/
function enrol_self_supports($feature) {
switch($feature) {
case ENROL_RESTORE_TYPE: return ENROL_RESTORE_EXACT;
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);
}
default: return null;
/**
* 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);
}
}
......@@ -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
......
......@@ -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';
......
......@@ -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.
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment