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 .= ' \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 = '