diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index 6b17bc295f1..850bb2ec2a6 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -1005,7 +1005,7 @@ class backup_groups_structure_step extends backup_structure_step { $members = new backup_nested_element('group_members'); $member = new backup_nested_element('group_member', array('id'), array( - 'userid', 'timeadded')); + 'userid', 'timeadded', 'component', 'itemid')); $groupings = new backup_nested_element('groupings'); diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 6b398521774..8742f0a25b4 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -780,6 +780,17 @@ class restore_groups_structure_step extends restore_structure_step { // 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); } } diff --git a/enrol/imsenterprise/lib.php b/enrol/imsenterprise/lib.php index 81f9e4cf980..0e0092563e3 100644 --- a/enrol/imsenterprise/lib.php +++ b/enrol/imsenterprise/lib.php @@ -702,7 +702,8 @@ function process_membership_tag($tagcontents){ } // Add the user-to-group association if it doesn't already exist if($member->groupid) { - groups_add_member($member->groupid, $memberstoreobj->userid); + groups_add_member($member->groupid, $memberstoreobj->userid, + 'enrol_imsenterprise', $einstance->id); } } // End of group-enrolment (from member.role.extension.cohort tag) @@ -793,6 +794,19 @@ function load_role_mappings() { } } + /** + * Called whenever anybody tries (from the normal interface) to remove a group + * member which is registered as being created by this component. (Not called + * when deleting an entire group or course at once.) + * @param int $itemid Item ID that was stored in the group_members entry + * @param int $groupid Group ID + * @param int $userid User ID being removed from group + * @return bool True if the remove is permitted, false to give an error + */ + function enrol_imsenterprise_allow_group_member_remove($itemid, $groupid, $userid) { + return false; + } + } // end of class diff --git a/group/externallib.php b/group/externallib.php index 0c17c81a57b..f7599d739ba 100644 --- a/group/externallib.php +++ b/group/externallib.php @@ -545,6 +545,9 @@ class core_group_external extends external_api { } require_capability('moodle/course:managegroups', $context); + if (!groups_remove_member_allowed($group, $user)) { + throw new moodle_exception('errorremovenotpermitted', 'group', '', fullname($user)); + } groups_remove_member($group, $user); } diff --git a/group/lib.php b/group/lib.php index 68d3385c7dd..e16bc01e46d 100644 --- a/group/lib.php +++ b/group/lib.php @@ -33,10 +33,12 @@ * * @param mixed $grouporid The group id or group object * @param mixed $userorid The user id or user object + * @param string $component Optional component name e.g. 'enrol_imsenterprise' + * @param int $itemid Optional itemid associated with component * @return bool True if user added successfully or the user is already a * member of the group, false otherwise. */ -function groups_add_member($grouporid, $userorid) { +function groups_add_member($grouporid, $userorid, $component=null, $itemid=0) { global $DB; if (is_object($userorid)) { @@ -68,6 +70,25 @@ function groups_add_member($grouporid, $userorid) { $member->groupid = $groupid; $member->userid = $userid; $member->timeadded = time(); + $member->component = ''; + $member->itemid = 0; + + // Check the component exists if specified + if (!empty($component)) { + $dir = get_component_directory($component); + if ($dir && is_dir($dir)) { + // Component exists and can be used + $member->component = $component; + $member->itemid = $itemid; + } else { + throw new coding_exception('Invalid call to groups_add_member(). An invalid component was specified'); + } + } + + if ($itemid !== 0 && empty($member->component)) { + // An itemid can only be specified if a valid component was found + throw new coding_exception('Invalid call to groups_add_member(). A component must be specified if an itemid is given'); + } $DB->insert_record('groups_members', $member); @@ -78,11 +99,62 @@ function groups_add_member($grouporid, $userorid) { $eventdata = new stdClass(); $eventdata->groupid = $groupid; $eventdata->userid = $userid; + $eventdata->component = $member->component; + $eventdata->itemid = $member->itemid; events_trigger('groups_member_added', $eventdata); return true; } +/** + * Checks whether the current user is permitted (using the normal UI) to + * remove a specific group member, assuming that they have access to remove + * group members in general. + * + * For automatically-created group member entries, this checks with the + * relevant plugin to see whether it is permitted. The default, if the plugin + * doesn't provide a function, is true. + * + * For other entries (and any which have already been deleted/don't exist) it + * just returns true. + * + * @param mixed $grouporid The group id or group object + * @param mixed $userorid The user id or user object + * @return bool True if permitted, false otherwise + */ +function groups_remove_member_allowed($grouporid, $userorid) { + global $DB; + + if (is_object($userorid)) { + $userid = $userorid->id; + } else { + $userid = $userorid; + } + if (is_object($grouporid)) { + $groupid = $grouporid->id; + } else { + $groupid = $grouporid; + } + + // Get entry + if (!($entry = $DB->get_record('groups_members', + array('groupid' => $groupid, 'userid' => $userid), '*', IGNORE_MISSING))) { + // If the entry does not exist, they are allowed to remove it (this + // is consistent with groups_remove_member below). + return true; + } + + // If the entry does not have a component value, they can remove it + if (empty($entry->component)) { + return true; + } + + // It has a component value, so we need to call a plugin function (if it + // exists); the default is to allow removal + return component_callback($entry->component, 'allow_group_member_remove', + array($entry->itemid, $entry->groupid, $entry->userid), true); +} + /** * Deletes the link between the specified user and group. * diff --git a/group/members.php b/group/members.php index 421d1d34e16..a8b74ef2514 100644 --- a/group/members.php +++ b/group/members.php @@ -67,6 +67,10 @@ if (optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) { $userstoremove = $groupmembersselector->get_selected_users(); if (!empty($userstoremove)) { foreach ($userstoremove as $user) { + if (!groups_remove_member_allowed($groupid, $user->id)) { + print_error('errorremovenotpermitted', 'group', $returnurl, + $user->fullname); + } if (!groups_remove_member($groupid, $user->id)) { print_error('erroraddremoveuser', 'group', $returnurl); } diff --git a/lang/en/group.php b/lang/en/group.php index 15059865142..807a806eb4d 100644 --- a/lang/en/group.php +++ b/lang/en/group.php @@ -24,6 +24,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['addedby'] = 'Added by {$a}'; $string['addgroup'] = 'Add user into group'; $string['addgroupstogrouping'] = 'Add group to grouping'; $string['addgroupstogroupings'] = 'Add/remove groups'; @@ -62,6 +63,7 @@ $string['erroraddremoveuser'] = 'Error adding/removing user {$a} to group'; $string['erroreditgroup'] = 'Error creating/updating group {$a}'; $string['erroreditgrouping'] = 'Error creating/updating grouping {$a}'; $string['errorinvalidgroup'] = 'Error, invalid group {$a}'; +$string['errorremovenotpermitted'] = 'You do not have permission to remove automatically-added group member {$a}'; $string['errorselectone'] = 'Please select a single group before choosing this option'; $string['errorselectsome'] = 'Please select one or more groups before choosing this option'; $string['evenallocation'] = 'Note: To keep group allocation even, the actual number of members per group differs from the number you specified.'; diff --git a/lib/db/install.xml b/lib/db/install.xml index 8c01fd2bff4..35c7bfc943e 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -2097,7 +2097,9 @@ - + + + @@ -2864,4 +2866,4 @@ - \ No newline at end of file + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 58bad7dbaa9..c243d8b00e6 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1162,6 +1162,27 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2012082300.01); } + if ($oldversion < 2012082300.02) { + // Define field component to be added to groups_members + $table = new xmldb_table('groups_members'); + $field = new xmldb_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'timeadded'); + + // Conditionally launch add field component + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field itemid to be added to groups_members + $field = new xmldb_field('itemid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'component'); + + // Conditionally launch add field itemid + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached + upgrade_main_savepoint(true, 2012082300.02); + } return true; } diff --git a/mod/page/lib.php b/mod/page/lib.php index 0f03e2abcfb..ea2133f2e08 100644 --- a/mod/page/lib.php +++ b/mod/page/lib.php @@ -515,3 +515,7 @@ function page_dndupload_handle($uploadinfo) { return page_add_instance($data, null); } + +function mod_page_allow_group_member_remove($itemid, $groupid, $userid) { + return true; +} diff --git a/theme/base/style/user.css b/theme/base/style/user.css index 64f88e0a0d2..32102d32e5a 100644 --- a/theme/base/style/user.css +++ b/theme/base/style/user.css @@ -40,6 +40,9 @@ .userselector select {width: 100%;} .userselector div {margin-top: 0.2em;} .userselector div label {margin-right: 0.3em;} +/* Next style does not work in all browsers but looks nicer when it does */ +.userselector .userselector-infobelow {font-size: 0.8em;} + #userselector_options {padding:0.3em 0;} #userselector_options .collapsibleregioncaption {font-weight: bold;} #userselector_options p {margin:0.2em 0;text-align:left;} diff --git a/user/selector/lib.php b/user/selector/lib.php index daecbf1e1a0..c1bdb2a7794 100644 --- a/user/selector/lib.php +++ b/user/selector/lib.php @@ -578,6 +578,12 @@ abstract class user_selector_base { unset($this->selected[$user->id]); $output .= ' ' . $this->output_user($user) . "\n"; + if (!empty($user->infobelow)) { + // 'Poor man's indent' here is because CSS styles do not work + // in select options, except in Firefox. + $output .= ' '; + } } } else { $output = ' ' . "\n"; @@ -712,6 +718,10 @@ abstract class groups_user_selector_base extends user_selector_base { foreach ($groupedusers[$groupname] as &$user) { unset($user->roles); $user->fullname = fullname($user); + if (!empty($user->component)) { + $user->infobelow = get_string('addedby', 'group', + get_string('pluginname', $user->component)); + } } } return $groupedusers; @@ -726,8 +736,8 @@ class group_members_selector extends groups_user_selector_base { public function find_users($search) { list($wherecondition, $params) = $this->search_sql($search, 'u'); $roles = groups_get_members_by_role($this->groupid, $this->courseid, - $this->required_fields_sql('u'), 'u.lastname, u.firstname', - $wherecondition, $params); + $this->required_fields_sql('u') . ', gm.component', + 'u.lastname, u.firstname', $wherecondition, $params); return $this->convert_array_format($roles, $search); } } diff --git a/user/selector/module.js b/user/selector/module.js index e6818c88f7c..80e37521c75 100644 --- a/user/selector/module.js +++ b/user/selector/module.js @@ -240,6 +240,11 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc option.set('selected', false); } optgroup.append(option); + if (user.infobelow) { + extraoption = Y.Node.create('