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 {
$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),
// 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'));
......
......@@ -715,9 +715,6 @@ class restore_groups_structure_step extends restore_structure_step {
$paths = array(); // Add paths here
$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_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
......@@ -769,33 +766,6 @@ class restore_groups_structure_step extends restore_structure_step {
$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) {
global $DB;
......@@ -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
* by loading them from the scales.xml
......@@ -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 {
} 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);
$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;
}
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 {
}
public function process_calendarevents($data) {
global $DB;
global $DB, $SITE;
$data = (object)$data;
$oldid = $data->id;
......@@ -3204,10 +3272,10 @@ class restore_create_question_files 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();
/** @var array internal cache for {@link choose_repository() */
/** @var array internal cache for {@link choose_repository()} */
private $cachereposbytype = array();
/**
......
......@@ -49,13 +49,18 @@ require_login($course);
require_capability('moodle/course:enrolreview', $context);
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.
$manager = new course_enrolment_manager($PAGE, $course);
$outcome = new stdClass;
$outcome = new stdClass();
$outcome->success = true;
$outcome->response = new stdClass;
$outcome->response = new stdClass();
$outcome->error = '';
switch ($action) {
......@@ -88,18 +93,21 @@ switch ($action) {
enrol_cohort_sync($manager->get_course()->id);
break;
case 'enrolcohortusers':
//TODO: this should be moved to enrol_manual, see MDL-35618.
require_capability('enrol/manual:enrol', $context);
$roleid = required_param('roleid', PARAM_INT);
$cohortid = required_param('cohortid', PARAM_INT);
$result = enrol_cohort_enrol_all_users($manager, $cohortid, $roleid);
$roles = $manager->get_assignable_roles();
if (!enrol_cohort_can_view_cohort($cohortid) || !array_key_exists($roleid, $roles)) {
throw new enrol_ajax_exception('errorenrolcohort');
}
$result = enrol_cohort_enrol_all_users($manager, $cohortid, $roleid);
if ($result === false) {
throw new enrol_ajax_exception('errorenrolcohortusers');
}
$outcome->success = true;
$outcome->response->users = $result;
$outcome->response->title = get_string('success');
......
......@@ -23,22 +23,46 @@
*/
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/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);
require_login($course);
require_capability('moodle/course:enrolconfig', $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');
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.
$courseadmin = $PAGE->settingsnav->get('courseadmin');
......@@ -47,20 +71,29 @@ if ($courseadmin && $courseadmin->get('users') && $courseadmin->get('users')->ge
}
$enrol = enrol_get_plugin('cohort');
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);
$mform = new enrol_cohort_edit_form(null, array($instance, $enrol, $course));
if ($mform->is_cancelled()) {
redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id)));
redirect($returnurl);
} 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);
redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id)));
redirect($returnurl);
}
$PAGE->set_heading($course->fullname);
......
......@@ -26,25 +26,49 @@ defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/formslib.php");
class enrol_cohort_addinstance_form extends moodleform {
protected $course;
class enrol_cohort_edit_form extends moodleform {
function definition() {
global $CFG, $DB;
$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');
$groups = array(0 => get_string('none'));
foreach (groups_get_all_groups($course->id) as $group) {
$groups[$group->id] = format_string($group->name, true, array('context'=>$coursecontext));
}
$mform->addElement('header','general', get_string('pluginname', 'enrol_cohort'));
$mform->addElement('text', 'name', get_string('custominstancename', 'enrol'));
$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'));
}
$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, contextid
$sql = "SELECT id, name, idnumber, contextid
FROM {cohort}
WHERE contextid $sqlparents
ORDER BY name ASC";
ORDER BY name ASC, idnumber ASC";
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $c) {
$context = context::instance_by_id($c->contextid);
......@@ -54,26 +78,38 @@ class enrol_cohort_addinstance_form extends moodleform {
$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');
}
$roles = get_assignable_roles($coursecontext);
$roles[0] = get_string('none');
$roles = array_reverse($roles, true); // Descending default sortorder.
$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->addElement('select', 'roleid', get_string('assignrole', 'enrol_cohort'), $roles);
$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->setType('id', PARAM_INT);
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) {
......@@ -81,8 +117,9 @@ class enrol_cohort_addinstance_form extends moodleform {
$errors = parent::validation($data, $files);
if ($DB->record_exists('enrol', array('roleid'=>$data['roleid'], 'customint1'=>$data['cohortid'], 'courseid'=>$this->course->id, 'enrol'=>'cohort'))) {
$errors['cohortid'] = get_string('instanceexists', 'enrol_cohort');
$params = array('roleid'=>$data['roleid'], 'customint1'=>$data['customint1'], 'courseid'=>$data['courseid'], 'id'=>$data['id']);
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;
......
......@@ -22,10 +22,13 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['addgroup'] = 'Add to group';
$string['ajaxmore'] = 'More...';
$string['assignrole'] = 'Assign role';
$string['cohortsearch'] = 'Search';
$string['cohort:config'] = 'Configure cohort instances';
$string['cohort:unenrol'] = 'Unenrol suspended users';
$string['instanceexists'] = 'Cohort is already synchronised with selected role';
$string['pluginname'] = 'Cohort sync';
$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 {
} else if (empty($instance->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)));
if ($role = $DB->get_record('role', array('id'=>$instance->roleid))) {
$role = role_get_name($role, context_course::instance($instance->courseid, IGNORE_MISSING));
return get_string('pluginname', 'enrol_'.$enrol) . ' (' . format_string($DB->get_field('cohort', 'name', array('id'=>$instance->customint1))) . ' - ' . $role .')';
return get_string('pluginname', 'enrol_'.$enrol) . ' (' . $cohortname . ' - ' . $role .')';
} else {
return get_string('pluginname', 'enrol_'.$enrol) . ' (' . format_string($DB->get_field('cohort', 'name', array('id'=>$instance->customint1))) . ')';
return get_string('pluginname', 'enrol_'.$enrol) . ' (' . $cohortname . ')';
}
} else {
return format_string($instance->name);
return format_string($instance->name, true, array('context'=>context_course::instance($instance->courseid)));
}
}
......@@ -67,7 +69,7 @@ class enrol_cohort_plugin extends enrol_plugin {
return NULL;
}
// Multiple instances supported - multiple parent courses linked.
return new moodle_url('/enrol/cohort/addinstance.php', array('id'=>$courseid));
return new moodle_url('/enrol/cohort/edit.php', array('courseid'=>$courseid));
}
/**
......@@ -99,6 +101,29 @@ class enrol_cohort_plugin extends enrol_plugin {
return false;
}
/**
* Returns edit icons for the page with list of instances.
* @param stdClass $instance
* @return array
*/
public function get_action_icons(stdClass $instance) {
global $OUTPUT;
if ($instance->enrol !== 'cohort') {
throw new coding_exception('invalid enrol instance!');
}
$context = context_course::instance($instance->courseid);
$icons = array();
if (has_capability('enrol/cohort:config', $context)) {
$editlink = new moodle_url("/enrol/cohort/edit.php", array('courseid'=>$instance->courseid, 'id'=>$instance->id));
$icons[] = $OUTPUT->action_icon($editlink, new pix_icon('i/edit', get_string('edit'), 'core', array('class'=>'icon')));
}
return $icons;
}
/**
* Called for all enabled enrol plugins that returned true from is_cron_required().
* @return void
......@@ -190,7 +215,7 @@ class enrol_cohort_plugin extends enrol_plugin {
return false;
}
$cohorturl = new moodle_url('/enrol/cohort/addinstance.php', array('id' => $course->id));
$cohorturl = new moodle_url('/enrol/cohort/edit.php', array('courseid' => $course->id));
$button = new enrol_user_button($cohorturl, get_string('enrolcohort', 'enrol'), 'get');
$button->class .= ' enrol_cohort_plugin';
......@@ -240,6 +265,10 @@ class enrol_cohort_plugin extends enrol_plugin {
return;
}
if (!empty($data->customint2)) {
$data->customint2 = $step->get_mappingid('group', $data->customint2);
}
if ($data->roleid and $DB->record_exists('cohort', array('id'=>$data->customint1))) {
$instance = $DB->get_record('enrol', array('roleid'=>$data->roleid, 'customint1'=>$data->customint1, 'courseid'=>$course->id, 'enrol'=>$this->get_name()));
if ($instance) {
......@@ -296,4 +325,26 @@ class enrol_cohort_plugin extends enrol_plugin {
$this->enrol_user($instance, $userid, null, $data->timestart, $data->timeend, ENROL_USER_SUSPENDED);
}
}
/**
* Restore user group membership.
* @param stdClass $instance
* @param int $groupid
* @param int $userid
*/
public function restore_group_member($instance, $groupid, $userid) {
// Nothing to do here, the group members are added in $this->restore_group_restored()
return;
}
}
/**
* Prevent removal of enrol roles.
* @param int $itemid
* @param int $groupid
* @param int $userid
* @return bool
*/
function enrol_cohort_allow_group_member_remove($itemid, $groupid, $userid) {
return false;
}
......@@ -40,7 +40,8 @@ class enrol_cohort_handler {
* @return bool
*/
public static function member_added($ca) {
global $DB;
global $DB, $CFG;
require_once("$CFG->dirroot/group/lib.php");
if (!enrol_is_enabled('cohort')) {
return true;
......@@ -68,6 +69,15 @@ class enrol_cohort_handler {
unset($instance->roleexists);
// No problem if already enrolled.
$plugin->enrol_user($instance, $ca->userid, $instance->roleid, 0, 0, ENROL_USER_ACTIVE);
// Sync groups.
if ($instance->customint2) {
if (!groups_is_member($instance->customint2, $ca->userid)) {
if ($group = $DB->get_record('groups', array('id'=>$instance->customint2, 'courseid'=>$instance->courseid))) {
groups_add_member($group->id, $ca->userid, 'enrol_cohort', $instance->id);
}
}
}
}
return true;
......@@ -147,6 +157,7 @@ class enrol_cohort_handler {
*/
function enrol_cohort_sync($courseid = NULL, $verbose = false) {
global $CFG, $DB;
require_once("$CFG->dirroot/group/lib.php");
// Purge all roles if cohort sync disabled, those can be recreated later here by cron or CLI.