Commit 7881024e authored by Petr Škoda's avatar Petr Škoda
Browse files

MDL-31437 add group sync option to enrol_cohort

This patch includes following changes and new features:
* Group sync in enrol_cohort plugin.
* Option for editing of role in existing cohort sync instance.
* Group memberships are now restored after enrolments.
* New enrol method for restore of protected group membership.
* New component callback 'restore_group_member' for restore of general plugin group membership.
* New component callback 'restore_role_assignment' for restore of general plugin role assignments.
* Implemented group membership protection in enrolment UI.
* Other minor fixes and cleanup.

Notes:
* The YUI base enrolment page is not reimplemented yet - see MDL-35618.
parent 08403f9e
...@@ -76,6 +76,9 @@ class restore_course_task extends restore_task { ...@@ -76,6 +76,9 @@ class restore_course_task extends restore_task {
$this->add_step(new restore_enrolments_structure_step('course_enrolments', 'enrolments.xml')); $this->add_step(new restore_enrolments_structure_step('course_enrolments', 'enrolments.xml'));
} }
// Populate groups, this must be done after enrolments because only enrolled users may be in groups.
$this->add_step(new restore_groups_members_structure_step('create_groups_members', '../groups.xml'));
// Restore course role assignments and overrides (internally will observe the role_assignments setting), // Restore course role assignments and overrides (internally will observe the role_assignments setting),
// this must be done after all users are enrolled. // 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')); $this->add_step(new restore_ras_and_caps_structure_step('course_ras_and_caps', 'roles.xml'));
......
...@@ -715,9 +715,6 @@ class restore_groups_structure_step extends restore_structure_step { ...@@ -715,9 +715,6 @@ class restore_groups_structure_step extends restore_structure_step {
$paths = array(); // Add paths here $paths = array(); // Add paths here
$paths[] = new restore_path_element('group', '/groups/group'); $paths[] = new restore_path_element('group', '/groups/group');
if ($this->get_setting_value('users')) {
$paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
}
$paths[] = new restore_path_element('grouping', '/groups/groupings/grouping'); $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
$paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group'); $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
...@@ -769,33 +766,6 @@ class restore_groups_structure_step extends restore_structure_step { ...@@ -769,33 +766,6 @@ class restore_groups_structure_step extends restore_structure_step {
$this->set_mapping('group', $oldid, $newitemid, $restorefiles); $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
} }
public function process_member($data) {
global $DB;
$data = (object)$data; // handy
// get parent group->id
$data->groupid = $this->get_new_parentid('group');
// map user newitemid and insert if not member already
if ($data->userid = $this->get_mappingid('user', $data->userid)) {
if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
// Check the componment, if any, exists
if (!empty($data->component)) {
$dir = get_component_directory($data->component);
if (!$dir || !is_dir($dir)) {
// Component does not exist on restored system; clear
// component and itemid
unset($data->component);
unset($data->itemid);
}
}
$DB->insert_record('groups_members', $data);
}
}
}
public function process_grouping($data) { public function process_grouping($data) {
global $DB; global $DB;
...@@ -866,6 +836,86 @@ class restore_groups_structure_step extends restore_structure_step { ...@@ -866,6 +836,86 @@ class restore_groups_structure_step extends restore_structure_step {
} }
/**
* Structure step that will create all the needed group memberships
* by loading them from the groups.xml file performing the required matches.
*/
class restore_groups_members_structure_step extends restore_structure_step {
protected $plugins = null;
protected function define_structure() {
$paths = array(); // Add paths here
if ($this->get_setting_value('users')) {
$paths[] = new restore_path_element('group', '/groups/group');
$paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
}
return $paths;
}
public function process_group($data) {
$data = (object)$data; // handy
// HACK ALERT!
// Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
// Let's fake internal state to make $this->get_new_parentid('group') work.
$this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
}
public function process_member($data) {
global $DB, $CFG;
require_once("$CFG->dirroot/group/lib.php");
// NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
$data = (object)$data; // handy
// get parent group->id
$data->groupid = $this->get_new_parentid('group');
// map user newitemid and insert if not member already
if ($data->userid = $this->get_mappingid('user', $data->userid)) {
if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
// Check the component, if any, exists.
if (empty($data->component)) {
groups_add_member($data->groupid, $data->userid);
} else if ((strpos($data->component, 'enrol_') === 0)) {
// Deal with enrolment groups - 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->plugins = enrol_get_plugins(true);
}
if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
if (isset($this->plugins[$instance->enrol])) {
$this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
}
}
}
} else {
$dir = get_component_directory($data->component);
if ($dir and is_dir($dir)) {
if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
return;
}
}
// Bad luck, plugin could not restore the data, let's add normal membership.
groups_add_member($data->groupid, $data->userid);
$message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
debugging($message);
$this->log($message, backup::LOG_WARNING);
}
}
}
}
}
/** /**
* Structure step that will create all the needed scales * Structure step that will create all the needed scales
* by loading them from the scales.xml * by loading them from the scales.xml
...@@ -1475,6 +1525,22 @@ class restore_ras_and_caps_structure_step extends restore_structure_step { ...@@ -1475,6 +1525,22 @@ class restore_ras_and_caps_structure_step extends restore_structure_step {
} }
} }
} }
} else {
$data->roleid = $newroleid;
$data->userid = $newuserid;
$data->contextid = $contextid;
$dir = get_component_directory($data->component);
if ($dir and is_dir($dir)) {
if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
return;
}
}
// Bad luck, plugin could not restore the data, let's add normal membership.
role_assign($data->roleid, $data->userid, $data->contextid);
$message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
debugging($message);
$this->log($message, backup::LOG_WARNING);
} }
} }
...@@ -1594,8 +1660,10 @@ class restore_enrolments_structure_step extends restore_structure_step { ...@@ -1594,8 +1660,10 @@ class restore_enrolments_structure_step extends restore_structure_step {
} else { } else {
if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) { 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); $this->set_mapping('enrol', $oldid, 0);
$message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, use migration to manual enrolments";
debugging($message);
$this->log($message, backup::LOG_WARNING);
return; return;
} }
if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) { if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
...@@ -1796,7 +1864,7 @@ class restore_calendarevents_structure_step extends restore_structure_step { ...@@ -1796,7 +1864,7 @@ class restore_calendarevents_structure_step extends restore_structure_step {
} }
public function process_calendarevents($data) { public function process_calendarevents($data) {
global $DB; global $DB, $SITE;
$data = (object)$data; $data = (object)$data;
$oldid = $data->id; $oldid = $data->id;
...@@ -3204,10 +3272,10 @@ class restore_create_question_files extends restore_execution_step { ...@@ -3204,10 +3272,10 @@ class restore_create_question_files extends restore_execution_step {
*/ */
class restore_process_file_aliases_queue extends restore_execution_step { class restore_process_file_aliases_queue extends restore_execution_step {
/** @var array internal cache for {@link choose_repository() */ /** @var array internal cache for {@link choose_repository()} */
private $cachereposbyid = array(); private $cachereposbyid = array();
/** @var array internal cache for {@link choose_repository() */ /** @var array internal cache for {@link choose_repository()} */
private $cachereposbytype = array(); private $cachereposbytype = array();
/** /**
......
...@@ -49,13 +49,18 @@ require_login($course); ...@@ -49,13 +49,18 @@ require_login($course);
require_capability('moodle/course:enrolreview', $context); require_capability('moodle/course:enrolreview', $context);
require_sesskey(); require_sesskey();
if (!enrol_is_enabled('cohort')) {
// This should never happen, no need to invent new error strings.
throw new enrol_ajax_exception('errorenrolcohort');
}
echo $OUTPUT->header(); // Send headers. echo $OUTPUT->header(); // Send headers.
$manager = new course_enrolment_manager($PAGE, $course); $manager = new course_enrolment_manager($PAGE, $course);
$outcome = new stdClass; $outcome = new stdClass();
$outcome->success = true; $outcome->success = true;
$outcome->response = new stdClass; $outcome->response = new stdClass();
$outcome->error = ''; $outcome->error = '';
switch ($action) { switch ($action) {
...@@ -88,18 +93,21 @@ switch ($action) { ...@@ -88,18 +93,21 @@ switch ($action) {
enrol_cohort_sync($manager->get_course()->id); enrol_cohort_sync($manager->get_course()->id);
break; break;
case 'enrolcohortusers': case 'enrolcohortusers':
//TODO: this should be moved to enrol_manual, see MDL-35618.
require_capability('enrol/manual:enrol', $context); require_capability('enrol/manual:enrol', $context);
$roleid = required_param('roleid', PARAM_INT); $roleid = required_param('roleid', PARAM_INT);
$cohortid = required_param('cohortid', PARAM_INT); $cohortid = required_param('cohortid', PARAM_INT);
$result = enrol_cohort_enrol_all_users($manager, $cohortid, $roleid);
$roles = $manager->get_assignable_roles(); $roles = $manager->get_assignable_roles();
if (!enrol_cohort_can_view_cohort($cohortid) || !array_key_exists($roleid, $roles)) { if (!enrol_cohort_can_view_cohort($cohortid) || !array_key_exists($roleid, $roles)) {
throw new enrol_ajax_exception('errorenrolcohort'); throw new enrol_ajax_exception('errorenrolcohort');
} }
$result = enrol_cohort_enrol_all_users($manager, $cohortid, $roleid);
if ($result === false) { if ($result === false) {
throw new enrol_ajax_exception('errorenrolcohortusers'); throw new enrol_ajax_exception('errorenrolcohortusers');
} }
$outcome->success = true; $outcome->success = true;
$outcome->response->users = $result; $outcome->response->users = $result;
$outcome->response->title = get_string('success'); $outcome->response->title = get_string('success');
......
...@@ -23,22 +23,46 @@ ...@@ -23,22 +23,46 @@
*/ */
require('../../config.php'); require('../../config.php');
require_once("$CFG->dirroot/enrol/cohort/addinstance_form.php"); require_once("$CFG->dirroot/enrol/cohort/edit_form.php");
require_once("$CFG->dirroot/enrol/cohort/locallib.php"); require_once("$CFG->dirroot/enrol/cohort/locallib.php");
require_once("$CFG->dirroot/group/lib.php");
$id = required_param('id', PARAM_INT); // Course id. $courseid = required_param('courseid', PARAM_INT);
$instanceid = optional_param('id', 0, PARAM_INT);
$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
$context = context_course::instance($course->id, MUST_EXIST); $context = context_course::instance($course->id, MUST_EXIST);
require_login($course); require_login($course);
require_capability('moodle/course:enrolconfig', $context); require_capability('moodle/course:enrolconfig', $context);
require_capability('enrol/cohort:config', $context); require_capability('enrol/cohort:config', $context);
$PAGE->set_url('/enrol/cohort/addinstance.php', array('id'=>$course->id)); $PAGE->set_url('/enrol/cohort/edit.php', array('courseid'=>$course->id, 'id'=>$instanceid));
$PAGE->set_pagelayout('admin'); $PAGE->set_pagelayout('admin');
navigation_node::override_active_url(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); $returnurl = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
if (!enrol_is_enabled('cohort')) {
redirect($returnurl);
}
$enrol = enrol_get_plugin('cohort');
if ($instanceid) {
$instance = $DB->get_record('enrol', array('courseid'=>$course->id, 'enrol'=>'cohort', 'id'=>$instanceid), '*', MUST_EXIST);
} else {
// No instance yet, we have to add new instance.
if (!$enrol->get_newinstance_link($course->id)) {
redirect($returnurl);
}
navigation_node::override_active_url(new moodle_url('/enrol/instances.php', array('id'=>$course->id)));
$instance = new stdClass();
$instance->id = null;
$instance->courseid = $course->id;
$instance->enrol = 'cohort';
$instance->customint1 = ''; // Cohort id.
$instance->customint2 = 0; // Optional group id.
}
// Try and make the manage instances node on the navigation active. // Try and make the manage instances node on the navigation active.
$courseadmin = $PAGE->settingsnav->get('courseadmin'); $courseadmin = $PAGE->settingsnav->get('courseadmin');
...@@ -47,20 +71,29 @@ if ($courseadmin && $courseadmin->get('users') && $courseadmin->get('users')->ge ...@@ -47,20 +71,29 @@ if ($courseadmin && $courseadmin->get('users') && $courseadmin->get('users')->ge
} }
$enrol = enrol_get_plugin('cohort'); $mform = new enrol_cohort_edit_form(null, array($instance, $enrol, $course));
if (!$enrol->get_newinstance_link($course->id)) {
redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id)));
}
$mform = new enrol_cohort_addinstance_form(NULL, $course);
if ($mform->is_cancelled()) { if ($mform->is_cancelled()) {
redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); redirect($returnurl);
} else if ($data = $mform->get_data()) { } else if ($data = $mform->get_data()) {
$enrol->add_instance($course, array('customint1'=>$data->cohortid, 'roleid'=>$data->roleid)); if ($data->id) {
// NOTE: no cohort changes here!!!
if ($data->roleid != $instance->roleid) {
// The sync script can only add roles, for perf reasons it does not modify them.
role_unassign_all(array('contextid'=>$context->id, 'roleid'=>$instance->roleid, 'component'=>'enrol_cohort', 'itemid'=>$instance->id));
}
$instance->name = $data->name;
$instance->status = $data->status;
$instance->roleid = $data->roleid;
$instance->customint2 = $data->customint2;
$instance->timemodified = time();
$DB->update_record('enrol', $instance);
} else {
$enrol->add_instance($course, array('name'=>$data->name, 'status'=>$data->status, 'customint1'=>$data->customint1, 'roleid'=>$data->roleid, 'customint2'=>$data->customint2));
}
enrol_cohort_sync($course->id); enrol_cohort_sync($course->id);
redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); redirect($returnurl);
} }
$PAGE->set_heading($course->fullname); $PAGE->set_heading($course->fullname);
......
...@@ -26,54 +26,90 @@ defined('MOODLE_INTERNAL') || die(); ...@@ -26,54 +26,90 @@ defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/formslib.php"); require_once("$CFG->libdir/formslib.php");
class enrol_cohort_addinstance_form extends moodleform { class enrol_cohort_edit_form extends moodleform {
protected $course;
function definition() { function definition() {
global $CFG, $DB; global $CFG, $DB;
$mform = $this->_form; $mform = $this->_form;
$this->course = $this->_customdata;
$coursecontext = context_course::instance($this->course->id); list($instance, $plugin, $course) = $this->_customdata;
$coursecontext = context_course::instance($course->id);
$enrol = enrol_get_plugin('cohort'); $enrol = enrol_get_plugin('cohort');
$cohorts = array('' => get_string('choosedots'));
list($sqlparents, $params) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids()); $groups = array(0 => get_string('none'));
$sql = "SELECT id, name, contextid foreach (groups_get_all_groups($course->id) as $group) {
FROM {cohort} $groups[$group->id] = format_string($group->name, true, array('context'=>$coursecontext));
WHERE contextid $sqlparents }
ORDER BY name ASC";
$rs = $DB->get_recordset_sql($sql, $params); $mform->addElement('header','general', get_string('pluginname', 'enrol_cohort'));
foreach ($rs as $c) {
$context = context::instance_by_id($c->contextid); $mform->addElement('text', 'name', get_string('custominstancename', 'enrol'));
if (!has_capability('moodle/cohort:view', $context)) {
continue; $options = array(ENROL_INSTANCE_ENABLED => get_string('yes'),
ENROL_INSTANCE_DISABLED => get_string('no'));
$mform->addElement('select', 'status', get_string('status', 'enrol_cohort'), $options);
if ($instance->id) {
if ($cohort = $DB->get_record('cohort', array('id'=>$instance->customint1))) {
$cohorts = array($instance->customint1=>format_string($cohort->name, true, array('context'=>context::instance_by_id($cohort->contextid))));
} else {
$cohorts = array($instance->customint1=>get_string('error'));
} }
$cohorts[$c->id] = format_string($c->name); $mform->addElement('select', 'customint1', get_string('cohort', 'cohort'), $cohorts);
$mform->setConstant('customint1', $instance->customint1);
$mform->hardFreeze('customint1', $instance->customint1);
} else {
$cohorts = array('' => get_string('choosedots'));
list($sqlparents, $params) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids());
$sql = "SELECT id, name, idnumber, contextid
FROM {cohort}
WHERE contextid $sqlparents
ORDER BY name ASC, idnumber ASC";
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $c) {
$context = context::instance_by_id($c->contextid);
if (!has_capability('moodle/cohort:view', $context)) {
continue;
}
$cohorts[$c->id] = format_string($c->name);
}
$rs->close();
$mform->addElement('select', 'customint1', get_string('cohort', 'cohort'), $cohorts);
$mform->addRule('customint1', get_string('required'), 'required', null, 'client');
} }
$rs->close();
$roles = get_assignable_roles($coursecontext); $roles = get_assignable_roles($coursecontext);
$roles[0] = get_string('none'); $roles[0] = get_string('none');
$roles = array_reverse($roles, true); // Descending default sortorder. $roles = array_reverse($roles, true); // Descending default sortorder.
$mform->addElement('select', 'roleid', get_string('assignrole', 'enrol_cohort'), $roles);
$mform->addElement('header','general', get_string('pluginname', 'enrol_cohort'));
$mform->addElement('select', 'cohortid', get_string('cohort', 'cohort'), $cohorts);
$mform->addRule('cohortid', get_string('required'), 'required', null, 'client');
$mform->addElement('select', 'roleid', get_string('role'), $roles);
$mform->addRule('roleid', get_string('required'), 'required', null, 'client');
$mform->setDefault('roleid', $enrol->get_config('roleid')); $mform->setDefault('roleid', $enrol->get_config('roleid'));
if ($instance->id and !isset($roles[$instance->roleid])) {
if ($role = $DB->get_record('role', array('id'=>$instance->roleid))) {
$roles = role_fix_names($roles, $coursecontext, ROLENAME_ALIAS, true);
$roles[$instance->roleid] = role_get_name($role, $coursecontext);
} else {
$roles[$instance->roleid] = get_string('error');
}
}
$mform->addElement('select', 'customint2', get_string('addgroup', 'enrol_cohort'), $groups);
$mform->addElement('hidden', 'courseid', null);
$mform->setType('courseid', PARAM_INT);
$mform->addElement('hidden', 'id', null); $mform->addElement('hidden', 'id', null);
$mform->setType('id', PARAM_INT); $mform->setType('id', PARAM_INT);
$this->add_action_buttons(true, get_string('addinstance', 'enrol')); if ($instance->id) {
$this->add_action_buttons(true);
} else {
$this->add_action_buttons(true, get_string('addinstance', 'enrol'));
}
$this->set_data(array('id'=>$this->course->id)); $this->set_data($instance);
} }
function validation($data, $files) { function validation($data, $files) {
...@@ -81,8 +117,9 @@ class enrol_cohort_addinstance_form extends moodleform { ...@@ -81,8 +117,9 @@ class enrol_cohort_addinstance_form extends moodleform {
$errors = parent::validation($data, $files); $errors = parent::validation($data, $files);
if ($DB->record_exists('enrol', array('roleid'=>$data['roleid'], 'customint1'=>$data['cohortid'], 'courseid'=>$this->course->id, 'enrol'=>'cohort'))) { $params = array('roleid'=>$data['roleid'], 'customint1'=>$data['customint1'], 'courseid'=>$data['courseid'], 'id'=>$data['id']);
$errors['cohortid'] = get_string('instanceexists', 'enrol_cohort'); if ($DB->record_exists_select('enrol', "roleid = :roleid AND customint1 = :customint1 AND courseid = :courseid AND enrol = 'cohort' AND id <> :id", $params)) {
$errors['roleid'] = get_string('instanceexists', 'enrol_cohort');
} }
return $errors; return $errors;
......
...@@ -22,10 +22,13 @@ ...@@ -22,10 +22,13 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/ */
$string['addgroup'] = 'Add to group';
$string['ajaxmore'] = 'More...'; $string['ajaxmore'] = 'More...';
$string['assignrole'] = 'Assign role';
$string['cohortsearch'] = 'Search'; $string['cohortsearch'] = 'Search';
$string['cohort:config'] = 'Configure cohort instances'; $string['cohort:config'] = 'Configure cohort instances';
$string['cohort:unenrol'] = 'Unenrol suspended users'; $string['cohort:unenrol'] = 'Unenrol suspended users';
$string['instanceexists'] = 'Cohort is already synchronised with selected role'; $string['instanceexists'] = 'Cohort is already synchronised with selected role';
$string['pluginname'] = 'Cohort sync'; $string['pluginname'] = 'Cohort sync';
$string['pluginname_desc'] = 'Cohort enrolment plugin synchronises cohort members with course participants.'; $string['pluginname_desc'] = 'Cohort enrolment plugin synchronises cohort members with course participants.';
$string['status'] = 'Active';
...@@ -45,15 +45,17 @@ class enrol_cohort_plugin extends enrol_plugin { ...@@ -45,15 +45,17 @@ class enrol_cohort_plugin extends enrol_plugin {
} else if (empty($instance->name)) { } else if (empty($instance->name)) {
$enrol = $this->get_name(); $enrol = $this->get_name();
$cohort = $DB->get_record('cohort', array('id'=>$instance->customint1));
$cohortname = format_string($cohort->name, true, array('context'=>context::instance_by_id($cohort->contextid)));<