diff --git a/group/classes/output/user_groups_editable.php b/group/classes/output/user_groups_editable.php
new file mode 100644
index 00000000000..c6534ba6dd6
--- /dev/null
+++ b/group/classes/output/user_groups_editable.php
@@ -0,0 +1,164 @@
+.
+
+/**
+ * Contains class core_group\output\user_groups_editable
+ *
+ * @package core_group
+ * @copyright 2017 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_group\output;
+
+use context_system;
+use context_course;
+use core_user;
+use core_external;
+
+/**
+ * Class to display list of user groups.
+ *
+ * @package core_group
+ * @copyright 2017 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_groups_editable extends \core\output\inplace_editable {
+
+ /** @var $coursegroups */
+ private $coursegroups = null;
+ /** @var $context */
+ private $context = null;
+
+ /**
+ * Constructor.
+ *
+ * @param \stdClass|core_tag_tag $tag
+ */
+ public function __construct($course, $context, $user, $coursegroups, $value) {
+ // Check capabilities to get editable value.
+ $editable = has_capability('moodle/course:managegroups', $context);
+
+ // Invent an itemid.
+ $itemid = $course->id . ':' . $user->id;
+
+ $value = json_encode($value);
+
+ // Remember these for the display value.
+ $this->coursegroups = $coursegroups;
+ $this->context = $context;
+
+ parent::__construct('core_group', 'user_groups', $itemid, $editable, $value, $value);
+
+ // Assignable groups.
+ $options = [];
+
+ foreach ($coursegroups as $group) {
+ $options[$group->id] = $group->name;
+ }
+
+ $attributes = ['multiple' => true];
+ $this->set_type_autocomplete($options, $attributes);
+ }
+
+ /**
+ * Export this data so it can be used as the context for a mustache template.
+ *
+ * @param \renderer_base $output
+ * @return \stdClass
+ */
+ public function export_for_template(\renderer_base $output) {
+ // Set edithint and display value.
+ $this->edithint = get_string('editusersgroups', 'group');
+
+ $listofgroups = [];
+ $groupids = json_decode($this->value);
+ foreach ($groupids as $id) {
+ $listofgroups[] = format_string($this->coursegroups[$id]->name, true, ['context' => $this->context]);
+ }
+
+ $this->displayvalue = implode($listofgroups, ', ');
+ return parent::export_for_template($output);
+ }
+
+ /**
+ * Updates the value in database and returns itself, called from inplace_editable callback
+ *
+ * @param int $itemid
+ * @param mixed $newvalue
+ * @return \self
+ */
+ public static function update($itemid, $newvalue) {
+ // Check caps.
+ // Do the thing.
+ // Return one of me.
+ // Validate the inputs.
+ list($courseid, $userid) = explode(':', $itemid, 2);
+
+ $courseid = clean_param($courseid, PARAM_INT);
+ $userid = clean_param($userid, PARAM_INT);
+ $groupids = json_decode($newvalue);
+ foreach ($groupids as $index => $groupid) {
+ $groupids[$index] = clean_param($groupid, PARAM_INT);
+ }
+
+ // Check user is enrolled in the course.
+ $context = context_course::instance($courseid);
+ core_external::validate_context($context);
+
+ if (!is_enrolled($context, $userid)) {
+ throw new coding_exception('User does not belong to the course');
+ }
+
+ // Check that all the groups belong to the course.
+ $coursegroups = groups_get_all_groups($courseid, 0, 0, 'g.*', true);
+
+ $byid = [];
+ foreach ($groupids as $groupid) {
+ if (!isset($coursegroups[$groupid])) {
+ throw new coding_exception('Group does not belong to the course');
+ }
+ $byid[$groupid] = $groupid;
+ }
+ $groupids = $byid;
+ // Check permissions.
+ require_capability('moodle/course:managegroups', $context);
+
+ // Process adds.
+ foreach ($groupids as $groupid) {
+ if (!isset($coursegroups[$groupid]->members[$userid])) {
+ // Add them.
+ groups_add_member($groupid, $userid);
+ // Keep this variable in sync.
+ $coursegroups[$groupid]->members[$userid] = $userid;
+ }
+ }
+
+ // Process removals.
+ foreach ($coursegroups as $groupid => $group) {
+ if (isset($group->members[$userid]) && !isset($groupids[$groupid])) {
+ if (groups_remove_member_allowed($groupid, $userid)) {
+ groups_remove_member($groupid, $userid);
+ unset($coursegroups[$groupid]->members[$userid]);
+ }
+ }
+ }
+
+ $course = get_course($courseid);
+ $user = core_user::get_user($userid);
+ return new self($course, $context, $user, $coursegroups, $groupids);
+ }
+}
diff --git a/group/lib.php b/group/lib.php
index 3f8de4e6804..67d04ddfbf9 100644
--- a/group/lib.php
+++ b/group/lib.php
@@ -1096,3 +1096,9 @@ function groups_sync_with_enrolment($enrolname, $courseid = 0, $gidfield = 'cust
return $affectedusers;
}
+
+function core_group_inplace_editable($itemtype, $itemid, $newvalue) {
+ if ($itemtype === 'user_groups') {
+ return \core_group\output\user_groups_editable::update($itemid, $newvalue);
+ }
+}
diff --git a/lang/en/group.php b/lang/en/group.php
index bd2a6058492..3132a6436ff 100644
--- a/lang/en/group.php
+++ b/lang/en/group.php
@@ -56,6 +56,7 @@ $string['deletegroupsconfirm'] = 'Are you sure you want to delete the following
$string['deleteselectedgroup'] = 'Delete selected group';
$string['editgroupingsettings'] = 'Edit grouping settings';
$string['editgroupsettings'] = 'Edit group settings';
+$string['editusersgroups'] = 'Edit users groups';
$string['enrolmentkey'] = 'Enrolment key';
$string['enrolmentkey_help'] = 'An enrolment key enables access to the course to be restricted to only those who know the key. If a group enrolment key is specified, then not only will entering that key let the user into the course, but it will also automatically make them a member of this group.
diff --git a/lib/amd/src/inplace_editable.js b/lib/amd/src/inplace_editable.js
index bc79fda29d3..43a89098b79 100644
--- a/lib/amd/src/inplace_editable.js
+++ b/lib/amd/src/inplace_editable.js
@@ -28,8 +28,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.1
*/
-define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/config', 'core/url'],
- function($, ajax, templates, notification, str, cfg, url) {
+define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/config', 'core/url', 'core/form-autocomplete'],
+ function($, ajax, templates, notification, str, cfg, url, autocomplete) {
$('body').on('click keypress', '[data-inplaceeditable] [data-inplaceeditablelink]', function(e) {
if (e.type === 'keypress' && e.keyCode !== 13) {
@@ -176,6 +176,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
.html(options[i].value));
}
inputelement.val(el.attr('data-value'));
+
el.html('')
.append(lbl)
.append(inputelement);
@@ -199,6 +200,63 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
});
};
+ var turnEditingOnAutocomplete = function(el, args) {
+ var i,
+ inputelement = $('').
+ attr('id', uniqueId('id_inplacevalue_', 20)).
+ addClass('custom-select'),
+ lbl = $('')
+ .attr('for', inputelement.attr('id')),
+ options = args.options,
+ attributes = args.attributes,
+ saveelement = $('');
+
+ for (i in options) {
+ inputelement
+ .append($('