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 { ...@@ -520,7 +520,7 @@ class backup_enrolments_structure_step extends backup_structure_step {
$enrols = new backup_nested_element('enrols'); $enrols = new backup_nested_element('enrols');
$enrol = new backup_nested_element('enrol', array('id'), array( $enrol = new backup_nested_element('enrol', array('id'), array(
'enrol', 'status', 'sortorder', 'name', 'enrolperiod', 'enrolstartdate', 'enrol', 'status', 'name', 'enrolperiod', 'enrolstartdate',
'enrolenddate', 'expirynotify', 'expirytreshold', 'notifyall', 'enrolenddate', 'expirynotify', 'expirytreshold', 'notifyall',
'password', 'cost', 'currency', 'roleid', 'password', 'cost', 'currency', 'roleid',
'customint1', 'customint2', 'customint3', 'customint4', 'customint5', 'customint6', 'customint7', 'customint8', 'customint1', 'customint2', 'customint3', 'customint4', 'customint5', 'customint6', 'customint7', 'customint8',
...@@ -541,9 +541,8 @@ class backup_enrolments_structure_step extends backup_structure_step { ...@@ -541,9 +541,8 @@ class backup_enrolments_structure_step extends backup_structure_step {
$enrol->add_child($userenrolments); $enrol->add_child($userenrolments);
$userenrolments->add_child($enrolment); $userenrolments->add_child($enrolment);
// Define sources // 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));
$enrol->set_source_table('enrol', array('courseid' => backup::VAR_COURSEID));
// User enrolments only added only if users included // User enrolments only added only if users included
if ($users) { if ($users) {
......
...@@ -71,14 +71,15 @@ class restore_course_task extends restore_task { ...@@ -71,14 +71,15 @@ class restore_course_task extends restore_task {
$this->add_step(new restore_course_structure_step('course_info', 'course.xml')); $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 // 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) { 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')); $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) // Restore course filters (conditionally)
if ($this->get_setting_value('filters')) { if ($this->get_setting_value('filters')) {
$this->add_step(new restore_filters_structure_step('course_filters', 'filters.xml')); $this->add_step(new restore_filters_structure_step('course_filters', 'filters.xml'));
......
...@@ -112,6 +112,12 @@ class restore_root_task extends restore_task { ...@@ -112,6 +112,12 @@ class restore_root_task extends restore_task {
$users->get_ui()->set_changeable($changeable); $users->get_ui()->set_changeable($changeable);
$this->add_setting($users); $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) // Define role_assignments (dependent of users)
$defaultvalue = false; // Safer default $defaultvalue = false; // Safer default
$changeable = false; $changeable = false;
......
...@@ -1407,13 +1407,14 @@ class restore_course_structure_step extends restore_structure_step { ...@@ -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) * 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 * one mapped role, they will be applied to target context. Will observe the role_assignments
* setting to decide if ras are restored. * 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 { class restore_ras_and_caps_structure_step extends restore_structure_step {
protected $plugins = null;
protected function define_structure() { protected function define_structure() {
...@@ -1462,15 +1463,15 @@ class restore_ras_and_caps_structure_step extends restore_structure_step { ...@@ -1462,15 +1463,15 @@ class restore_ras_and_caps_structure_step extends restore_structure_step {
role_assign($newroleid, $newuserid, $contextid); role_assign($newroleid, $newuserid, $contextid);
} else if ((strpos($data->component, 'enrol_') === 0)) { } 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 ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
if ($component = $DB->get_field('enrol', 'component', array('id'=>$enrolid))) { if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
//note: we have to verify component because it might have changed if (isset($this->plguins[$instance->enrol])) {
if ($component === 'enrol_manual') { $this->plguins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
// 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?
} }
} }
} }
...@@ -1496,6 +1497,9 @@ class restore_ras_and_caps_structure_step extends restore_structure_step { ...@@ -1496,6 +1497,9 @@ class restore_ras_and_caps_structure_step extends restore_structure_step {
* enrolments, performing all the mappings and/or movements required * enrolments, performing all the mappings and/or movements required
*/ */
class restore_enrolments_structure_step extends restore_structure_step { 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. * Conditionally decide if this step should be executed.
...@@ -1542,82 +1546,103 @@ class restore_enrolments_structure_step extends restore_structure_step { ...@@ -1542,82 +1546,103 @@ class restore_enrolments_structure_step extends restore_structure_step {
global $DB; global $DB;
$data = (object)$data; $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) { if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
// TODO: add complex restore support via custom class
debugging("Skipping '{$data->enrol}' enrolment plugin. Will be implemented before 2.0 release", DEBUG_DEVELOPER);
$this->set_mapping('enrol', $oldid, 0); $this->set_mapping('enrol', $oldid, 0);
return; return;
} }
// Perform various checks to decide what to do with the enrol plugin if (!isset($this->plugins)) {
if (!array_key_exists($data->enrol, enrol_get_plugins(false))) { $this->plugins = enrol_get_plugins(true);
// 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 if (!$this->enrolsynced) {
debugging("Enrol plugin data can not be restored because it is not enabled"); // Make sure that all plugin may create instances and enrolments automatically
$this->set_mapping('enrol', $oldid, 0); // before the first instance restore - this is suitable especially for plugins
return; // that synchronise data automatically using course->idnumber or by course categories.
foreach ($this->plugins as $plugin) {
$plugin->restore_sync_course($courserec);
}
$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); $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 if ($this->get_setting_value('enrol_migratetomanual')) {
$enrol = enrol_get_plugin($data->enrol); unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
$courserec = $DB->get_record('course', array('id' => $this->get_courseid())); // Requires object, uses only id!! if (!enrol_is_enabled('manual')) {
if ($newitemid = $enrol->add_instance($courserec, (array)$data)) { $this->set_mapping('enrol', $oldid, 0);
// ok return;
} else { }
if ($instances = $DB->get_records('enrol', array('courseid'=>$courserec->id, 'enrol'=>$data->enrol))) { if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
// most probably plugin that supports only one instance $instance = reset($instances);
$newitemid = key($instances); $this->set_mapping('enrol', $oldid, $instance->id);
} else { } else {
debugging('Can not create new enrol instance or reuse existing'); if ($data->enrol === 'manual') {
$newitemid = 0; $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
} else {
$instanceid = $this->plugins['manual']->add_default_instance($courserec);
}
$this->set_mapping('enrol', $oldid, $instanceid);
} }
}
if ($restoretype === ENROL_RESTORE_NOUSERS) { } else {
// plugin requests to prevent restore of any users if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
$newitemid = 0; 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 * This has to be called after creation of enrolment instances
* and before adding of role assignments. * and before adding of role assignments.
* *
* Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
*
* @param mixed $data * @param mixed $data
* @return void * @return void
*/ */
public function process_enrolment($data) { public function process_enrolment($data) {
global $DB; global $DB;
if (!isset($this->plugins)) {
$this->plugins = enrol_get_plugins(true);
}
$data = (object)$data; $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')) { 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))) { 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)) { if ($userid = $this->get_mappingid('user', $data->userid)) {
$enrol = enrol_get_plugin($instance->enrol); if (isset($this->plugins[$instance->enrol])) {
//TODO: do we need specify modifierid? $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
$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
} }
} }
} }
......
...@@ -278,6 +278,7 @@ abstract class restore_structure_step extends restore_step { ...@@ -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 * 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....) * have the parent task available for wrapping purposes (get course/context....)
* @return restore_task|null
*/ */
public function get_task() { public function get_task() {
return $this->task; return $this->task;
......
...@@ -92,4 +92,15 @@ class enrol_category_plugin extends enrol_plugin { ...@@ -92,4 +92,15 @@ class enrol_category_plugin extends enrol_plugin {
require_once("$CFG->dirroot/enrol/category/locallib.php"); require_once("$CFG->dirroot/enrol/category/locallib.php");
enrol_category_sync_course($course); 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 { ...@@ -366,18 +366,22 @@ class enrol_guest_plugin extends enrol_plugin {
return $this->add_instance($course, $fields); 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;
/** if (!$DB->record_exists('enrol', array('courseid' => $data->courseid, 'enrol' => $this->get_name()))) {
* Indicates API features that the enrol plugin supports. $this->add_instance($course, (array)$data);
* }
* @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;
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 { ...@@ -295,18 +295,89 @@ class enrol_manual_plugin extends enrol_plugin {
); );
return $bulkoperations; return $bulkoperations;
} }
}
/** /**
* Indicates API features that the enrol plugin supports. * Restore instance and map settings.
* *
* @param string $feature * @param restore_enrolments_structure_step $step
* @return mixed True if yes (some features may use other values) * @param stdClass $data
*/ * @param stdClass $course
function enrol_manual_supports($feature) { * @param int $oldid
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 { ...@@ -403,18 +403,68 @@ class enrol_self_plugin extends enrol_plugin {
} }
return $actions; return $actions;
} }
}
/** /**
* Indicates API features that the enrol plugin supports. * Restore instance and map settings.
* *
* @param string $feature * @param restore_enrolments_structure_step $step
* @return mixed true if yes (some features may use other values) * @param stdClass $data
*/ * @param stdClass $course
function enrol_self_supports($feature) { * @param int $oldid
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;
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);
} }
} }
...@@ -7,6 +7,8 @@ information provided here is intended especially for developers. ...@@ -7,6 +7,8 @@ information provided here is intended especially for developers.
required changes in code: required changes in code:
* use role_get_name() or role_fix_names() if you need any role names, using role.name * 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 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: other changes:
* course enrolment manager now works with disabled plugins too * course enrolment manager now works with disabled plugins too
......
...@@ -210,6 +210,7 @@ $string['restoretonewcourse'] = 'Restore as a new course'; ...@@ -210,6 +210,7 @@ $string['restoretonewcourse'] = 'Restore as a new course';
$string['restoringcourse'] = 'Course restoration in progress'; $string['restoringcourse'] = 'Course restoration in progress';
$string['restoringcourseshortname'] = 'restoring'; $string['restoringcourseshortname'] = 'restoring';
$string['restorerolemappings'] = 'Restore role mappings'; $string['restorerolemappings'] = 'Restore role mappings';
$string['rootenrolmanual'] = 'Restore as manual enrolments';