File manager - Edit - /home/u466501803/domains/qurdis.my.id/public_html/group.tar
Back
lib.php 0000644 00000135507 15215711721 0006040 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Extra library for groups and groupings. * * @copyright 2006 The Open University, J.White AT open.ac.uk, Petr Skoda (skodak) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ /* * INTERNAL FUNCTIONS - to be used by moodle core only * require_once $CFG->dirroot.'/group/lib.php' must be used */ /** * Adds a specified user to a group * * @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, $component=null, $itemid=0) { global $DB; if (is_object($userorid)) { $userid = $userorid->id; $user = $userorid; if (!isset($user->deleted)) { $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); } } else { $userid = $userorid; $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); } if ($user->deleted) { return false; } if (is_object($grouporid)) { $groupid = $grouporid->id; $group = $grouporid; } else { $groupid = $grouporid; $group = $DB->get_record('groups', array('id'=>$groupid), '*', MUST_EXIST); } // Check if the user a participant of the group course. $context = context_course::instance($group->courseid); if (!is_enrolled($context, $userid)) { return false; } if (groups_is_member($groupid, $userid)) { return true; } $member = new stdClass(); $member->groupid = $groupid; $member->userid = $userid; $member->timeadded = time(); $member->component = ''; $member->itemid = 0; // Check the component exists if specified if (!empty($component)) { $dir = core_component::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); // Update group info, and group object. $DB->set_field('groups', 'timemodified', $member->timeadded, array('id'=>$groupid)); $group->timemodified = $member->timeadded; // Invalidate the group and grouping cache for users. cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), array($userid)); // Group conversation messaging. if ($conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $groupid, $context->id)) { \core_message\api::add_members_to_conversation([$userid], $conversation->id); } // Trigger group event. $params = array( 'context' => $context, 'objectid' => $groupid, 'relateduserid' => $userid, 'other' => array( 'component' => $member->component, 'itemid' => $member->itemid ) ); $event = \core\event\group_member_added::create($params); $event->add_record_snapshot('groups', $group); $event->trigger(); // Dispatch the hook for a user added to the group. $hook = new \core_group\hook\after_group_membership_added( groupinstance: $group, userids: [$userid], ); \core\di::get(\core\hook\manager::class)->dispatch($hook); 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. * * @param mixed $grouporid The group id or group object * @param mixed $userorid The user id or user object * @return bool True if deletion was successful, false otherwise */ function groups_remove_member($grouporid, $userorid) { global $DB; if (is_object($userorid)) { $userid = $userorid->id; } else { $userid = $userorid; } if (is_object($grouporid)) { $groupid = $grouporid->id; $group = $grouporid; } else { $groupid = $grouporid; $group = $DB->get_record('groups', array('id'=>$groupid), '*', MUST_EXIST); } if (!groups_is_member($groupid, $userid)) { return true; } $DB->delete_records('groups_members', array('groupid'=>$groupid, 'userid'=>$userid)); // Update group info. $time = time(); $DB->set_field('groups', 'timemodified', $time, array('id' => $groupid)); $group->timemodified = $time; // Invalidate the group and grouping cache for users. cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), array($userid)); // Group conversation messaging. $context = context_course::instance($group->courseid); if ($conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $groupid, $context->id)) { \core_message\api::remove_members_from_conversation([$userid], $conversation->id); } // Trigger group event. $params = array( 'context' => context_course::instance($group->courseid), 'objectid' => $groupid, 'relateduserid' => $userid ); $event = \core\event\group_member_removed::create($params); $event->add_record_snapshot('groups', $group); $event->trigger(); // Dispatch the hook for a user removed from the group. $hook = new \core_group\hook\after_group_membership_removed( groupinstance: $group, userids: [$userid], ); \core\di::get(\core\hook\manager::class)->dispatch($hook); return true; } /** * Add a new group * * @param stdClass $data group properties * @param stdClass $editform * @param array $editoroptions * @return int id of group or throws an exception on error * @throws moodle_exception */ function groups_create_group($data, $editform = false, $editoroptions = false) { global $CFG, $DB, $USER; //check that courseid exists $course = $DB->get_record('course', array('id' => $data->courseid), '*', MUST_EXIST); $context = context_course::instance($course->id); $data->timecreated = time(); $data->timemodified = $data->timecreated; $data->name = trim($data->name); if (isset($data->idnumber)) { $data->idnumber = trim($data->idnumber); if (groups_get_group_by_idnumber($course->id, $data->idnumber)) { throw new moodle_exception('idnumbertaken'); } } $data->visibility ??= GROUPS_VISIBILITY_ALL; if (!in_array($data->visibility, [GROUPS_VISIBILITY_ALL, GROUPS_VISIBILITY_MEMBERS])) { $data->participation = false; $data->enablemessaging = false; } if ($editform and $editoroptions) { $data->description = $data->description_editor['text']; $data->descriptionformat = $data->description_editor['format']; } $data->id = $DB->insert_record('groups', $data); $handler = \core_group\customfield\group_handler::create(); $handler->instance_form_save($data, true); if ($editform and $editoroptions) { // Update description from editor with fixed files $data = file_postupdate_standard_editor($data, 'description', $editoroptions, $context, 'group', 'description', $data->id); $upd = new stdClass(); $upd->id = $data->id; $upd->description = $data->description; $upd->descriptionformat = $data->descriptionformat; $DB->update_record('groups', $upd); } $group = $DB->get_record('groups', array('id'=>$data->id)); if ($editform) { groups_update_group_icon($group, $data, $editform); } // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($course->id)); // Rebuild the coursehiddengroups cache for the course. \core_group\visibility::update_hiddengroups_cache($course->id); // Group conversation messaging. if (\core_message\api::can_create_group_conversation($USER->id, $context)) { if (!empty($data->enablemessaging)) { \core_message\api::create_conversation( \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, [], $group->name, \core_message\api::MESSAGE_CONVERSATION_ENABLED, 'core_group', 'groups', $group->id, $context->id); } } // Trigger group event. $params = array( 'context' => $context, 'objectid' => $group->id ); $event = \core\event\group_created::create($params); $event->add_record_snapshot('groups', $group); $event->trigger(); // Dispatch the hook for post group creation actions. $hook = new \core_group\hook\after_group_created( groupinstance: $group, ); \core\di::get(\core\hook\manager::class)->dispatch($hook); return $group->id; } /** * Add a new grouping * * @param stdClass $data grouping properties * @param array $editoroptions * @return int id of grouping or throws an exception on error * @throws moodle_exception */ function groups_create_grouping($data, $editoroptions=null) { global $DB; $data->timecreated = time(); $data->timemodified = $data->timecreated; $data->name = trim($data->name); if (isset($data->idnumber)) { $data->idnumber = trim($data->idnumber); if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) { throw new moodle_exception('idnumbertaken'); } } if ($editoroptions !== null) { $data->description = $data->description_editor['text']; $data->descriptionformat = $data->description_editor['format']; } $id = $DB->insert_record('groupings', $data); $data->id = $id; $handler = \core_group\customfield\grouping_handler::create(); $handler->instance_form_save($data, true); if ($editoroptions !== null) { $description = new stdClass; $description->id = $data->id; $description->description_editor = $data->description_editor; $description = file_postupdate_standard_editor($description, 'description', $editoroptions, $editoroptions['context'], 'grouping', 'description', $description->id); $DB->update_record('groupings', $description); } // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid)); // Trigger group event. $params = array( 'context' => context_course::instance($data->courseid), 'objectid' => $id ); $event = \core\event\grouping_created::create($params); $event->trigger(); return $id; } /** * Update the group icon from form data * * @param stdClass $group group information * @param stdClass $data * @param stdClass $editform */ function groups_update_group_icon($group, $data, $editform) { global $CFG, $DB; require_once("$CFG->libdir/gdlib.php"); $fs = get_file_storage(); $context = context_course::instance($group->courseid, MUST_EXIST); $newpicture = $group->picture; if (!empty($data->deletepicture)) { $fs->delete_area_files($context->id, 'group', 'icon', $group->id); $newpicture = 0; } else if ($iconfile = $editform->save_temp_file('imagefile')) { if ($rev = process_new_icon($context, 'group', 'icon', $group->id, $iconfile)) { $newpicture = $rev; } else { $fs->delete_area_files($context->id, 'group', 'icon', $group->id); $newpicture = 0; } @unlink($iconfile); } if ($newpicture != $group->picture) { $DB->set_field('groups', 'picture', $newpicture, array('id' => $group->id)); $group->picture = $newpicture; // Invalidate the group data as we've updated the group record. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($group->courseid)); } } /** * Update group * * @param stdClass $data group properties (with magic quotes) * @param stdClass $editform * @param array $editoroptions * @return bool true or exception */ function groups_update_group($data, $editform = false, $editoroptions = false) { global $CFG, $DB, $USER; $context = context_course::instance($data->courseid); $data->timemodified = time(); if (isset($data->name)) { $data->name = trim($data->name); } if (isset($data->idnumber)) { $data->idnumber = trim($data->idnumber); if (($existing = groups_get_group_by_idnumber($data->courseid, $data->idnumber)) && $existing->id != $data->id) { throw new moodle_exception('idnumbertaken'); } } if (isset($data->visibility) && !in_array($data->visibility, [GROUPS_VISIBILITY_ALL, GROUPS_VISIBILITY_MEMBERS])) { $data->participation = false; $data->enablemessaging = false; } if ($editform and $editoroptions) { $data = file_postupdate_standard_editor($data, 'description', $editoroptions, $context, 'group', 'description', $data->id); } $DB->update_record('groups', $data); $handler = \core_group\customfield\group_handler::create(); $handler->instance_form_save($data); // Invalidate the group data. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid)); // Rebuild the coursehiddengroups cache for the course. \core_group\visibility::update_hiddengroups_cache($data->courseid); $group = $DB->get_record('groups', array('id'=>$data->id)); if ($editform) { groups_update_group_icon($group, $data, $editform); } // Group conversation messaging. if (\core_message\api::can_create_group_conversation($USER->id, $context)) { if ($conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $group->id, $context->id)) { if ($data->enablemessaging && $data->enablemessaging != $conversation->enabled) { \core_message\api::enable_conversation($conversation->id); } if (!$data->enablemessaging && $data->enablemessaging != $conversation->enabled) { \core_message\api::disable_conversation($conversation->id); } \core_message\api::update_conversation_name($conversation->id, $group->name); } else { if (!empty($data->enablemessaging)) { $conversation = \core_message\api::create_conversation( \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, [], $group->name, \core_message\api::MESSAGE_CONVERSATION_ENABLED, 'core_group', 'groups', $group->id, $context->id ); // Add members to conversation if they exists in the group. if ($groupmemberroles = groups_get_members_by_role($group->id, $group->courseid, 'u.id')) { $users = []; foreach ($groupmemberroles as $roleid => $roledata) { foreach ($roledata->users as $member) { $users[] = $member->id; } } \core_message\api::add_members_to_conversation($users, $conversation->id); } } } } // Trigger group event. $params = array( 'context' => $context, 'objectid' => $group->id ); $event = \core\event\group_updated::create($params); $event->add_record_snapshot('groups', $group); $event->trigger(); // Dispatch the hook for post group update actions. $hook = new \core_group\hook\after_group_updated( groupinstance: $group, ); \core\di::get(\core\hook\manager::class)->dispatch($hook); return true; } /** * Update grouping * * @param stdClass $data grouping properties (with magic quotes) * @param array $editoroptions * @return bool true or exception */ function groups_update_grouping($data, $editoroptions=null) { global $DB; $data->timemodified = time(); if (isset($data->name)) { $data->name = trim($data->name); } if (isset($data->idnumber)) { $data->idnumber = trim($data->idnumber); if (($existing = groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) && $existing->id != $data->id) { throw new moodle_exception('idnumbertaken'); } } if ($editoroptions !== null) { $data = file_postupdate_standard_editor($data, 'description', $editoroptions, $editoroptions['context'], 'grouping', 'description', $data->id); } $DB->update_record('groupings', $data); $handler = \core_group\customfield\grouping_handler::create(); $handler->instance_form_save($data); // Invalidate the group data. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid)); // Trigger group event. $params = array( 'context' => context_course::instance($data->courseid), 'objectid' => $data->id ); $event = \core\event\grouping_updated::create($params); $event->trigger(); return true; } /** * Delete a group best effort, first removing members and links with courses and groupings. * Removes group avatar too. * * @param mixed $grouporid The id of group to delete or full group object * @return bool True if deletion was successful, false otherwise */ function groups_delete_group($grouporid) { global $CFG, $DB; require_once("$CFG->libdir/gdlib.php"); if (is_object($grouporid)) { $groupid = $grouporid->id; $group = $grouporid; } else { $groupid = $grouporid; if (!$group = $DB->get_record('groups', array('id'=>$groupid))) { //silently ignore attempts to delete missing already deleted groups ;-) return true; } } $context = context_course::instance($group->courseid); // delete group calendar events $DB->delete_records('event', array('groupid'=>$groupid)); //first delete usage in groupings_groups $DB->delete_records('groupings_groups', array('groupid'=>$groupid)); //delete members $DB->delete_records('groups_members', array('groupid'=>$groupid)); // Delete any members in a conversation related to this group. if ($conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $groupid, $context->id)) { \core_message\api::delete_all_conversation_data($conversation->id); } //group itself last $DB->delete_records('groups', array('id'=>$groupid)); // Delete all files associated with this group $context = context_course::instance($group->courseid); $fs = get_file_storage(); $fs->delete_area_files($context->id, 'group', 'description', $groupid); $fs->delete_area_files($context->id, 'group', 'icon', $groupid); // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($group->courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); // Rebuild the coursehiddengroups cache for the course. \core_group\visibility::update_hiddengroups_cache($group->courseid); // Trigger group event. $params = array( 'context' => $context, 'objectid' => $groupid ); $event = \core\event\group_deleted::create($params); $event->add_record_snapshot('groups', $group); $event->trigger(); // Dispatch the hook for post group delete actions. $hook = new \core_group\hook\after_group_deleted( groupinstance: $group, ); \core\di::get(\core\hook\manager::class)->dispatch($hook); return true; } /** * Delete grouping * * @param int $groupingorid * @return bool success */ function groups_delete_grouping($groupingorid) { global $DB; if (is_object($groupingorid)) { $groupingid = $groupingorid->id; $grouping = $groupingorid; } else { $groupingid = $groupingorid; if (!$grouping = $DB->get_record('groupings', array('id'=>$groupingorid))) { //silently ignore attempts to delete missing already deleted groupings ;-) return true; } } //first delete usage in groupings_groups $DB->delete_records('groupings_groups', array('groupingid'=>$groupingid)); // remove the default groupingid from course $DB->set_field('course', 'defaultgroupingid', 0, array('defaultgroupingid'=>$groupingid)); // remove the groupingid from all course modules $DB->set_field('course_modules', 'groupingid', 0, array('groupingid'=>$groupingid)); //group itself last $DB->delete_records('groupings', array('id'=>$groupingid)); $context = context_course::instance($grouping->courseid); $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'grouping', 'description', $groupingid); foreach ($files as $file) { $file->delete(); } // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($grouping->courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); // Trigger group event. $params = array( 'context' => $context, 'objectid' => $groupingid ); $event = \core\event\grouping_deleted::create($params); $event->add_record_snapshot('groupings', $grouping); $event->trigger(); return true; } /** * Remove all users (or one user) from all groups in course * * @param int $courseid * @param int $userid 0 means all users * @param bool $unused - formerly $showfeedback, is no longer used. * @return bool success */ function groups_delete_group_members($courseid, $userid=0, $unused=false) { global $DB, $OUTPUT; // Get the users in the course which are in a group. $sql = "SELECT gm.id as gmid, gm.userid, g.* FROM {groups_members} gm INNER JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = :courseid"; $params = array(); $params['courseid'] = $courseid; // Check if we want to delete a specific user. if ($userid) { $sql .= " AND gm.userid = :userid"; $params['userid'] = $userid; } $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $usergroup) { groups_remove_member($usergroup, $usergroup->userid); } $rs->close(); return true; } /** * Remove all groups from all groupings in course * * @param int $courseid * @param bool $showfeedback * @return bool success */ function groups_delete_groupings_groups($courseid, $showfeedback=false) { global $DB, $OUTPUT; $groupssql = "SELECT id FROM {groups} g WHERE g.courseid = ?"; $results = $DB->get_recordset_select('groupings_groups', "groupid IN ($groupssql)", array($courseid), '', 'groupid, groupingid'); foreach ($results as $result) { groups_unassign_grouping($result->groupingid, $result->groupid, false); } $results->close(); // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); // no need to show any feedback here - we delete usually first groupings and then groups return true; } /** * Delete all groups from course * * @param int $courseid * @param bool $showfeedback * @return bool success */ function groups_delete_groups($courseid, $showfeedback=false) { global $CFG, $DB, $OUTPUT; $groups = $DB->get_recordset('groups', array('courseid' => $courseid)); foreach ($groups as $group) { groups_delete_group($group); } $groups->close(); // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); // Rebuild the coursehiddengroups cache for the course. \core_group\visibility::update_hiddengroups_cache($courseid); if ($showfeedback) { echo $OUTPUT->notification(get_string('deleted').' - '.get_string('groups', 'group'), 'notifysuccess'); } return true; } /** * Delete all groupings from course * * @param int $courseid * @param bool $showfeedback * @return bool success */ function groups_delete_groupings($courseid, $showfeedback=false) { global $DB, $OUTPUT; $groupings = $DB->get_recordset_select('groupings', 'courseid = ?', array($courseid)); foreach ($groupings as $grouping) { groups_delete_grouping($grouping); } $groupings->close(); // Invalidate the grouping cache for the course. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); if ($showfeedback) { echo $OUTPUT->notification(get_string('deleted').' - '.get_string('groupings', 'group'), 'notifysuccess'); } return true; } /* =================================== */ /* various functions used by groups UI */ /* =================================== */ /** * Obtains a list of the possible roles that group members might come from, * on a course. Generally this includes only profile roles. * * @param context $context Context of course * @return Array of role ID integers, or false if error/none. */ function groups_get_possible_roles($context) { $roles = get_profile_roles($context); return array_keys($roles); } /** * Gets potential group members for grouping * * @param int $courseid The id of the course * @param int $roleid The role to select users from * @param mixed $source restrict to cohort, grouping or group id * @param string $orderby The column to sort users by * @param int $notingroup restrict to users not in existing groups * @param bool $onlyactiveenrolments restrict to users who have an active enrolment in the course * @param array $extrafields Extra user fields to return * @return array An array of the users */ function groups_get_potential_members($courseid, $roleid = null, $source = null, $orderby = 'lastname ASC, firstname ASC', $notingroup = null, $onlyactiveenrolments = false, $extrafields = []) { global $DB; $context = context_course::instance($courseid); list($esql, $params) = get_enrolled_sql($context, '', 0, $onlyactiveenrolments); $notingroupsql = ""; if ($notingroup) { // We want to eliminate users that are already associated with a course group. $notingroupsql = "u.id NOT IN (SELECT userid FROM {groups_members} WHERE groupid IN (SELECT id FROM {groups} WHERE courseid = :courseid))"; $params['courseid'] = $courseid; } if ($roleid) { // We are looking for all users with this role assigned in this context or higher. list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); $params = array_merge($params, $relatedctxparams, array('roleid' => $roleid)); $where = "WHERE u.id IN (SELECT userid FROM {role_assignments} WHERE roleid = :roleid AND contextid $relatedctxsql)"; $where .= $notingroup ? "AND $notingroupsql" : ""; } else if ($notingroup) { $where = "WHERE $notingroupsql"; } else { $where = ""; } $sourcejoin = ""; if (is_int($source)) { $sourcejoin .= "JOIN {cohort_members} cm ON (cm.userid = u.id AND cm.cohortid = :cohortid) "; $params['cohortid'] = $source; } else { // Auto-create groups from an existing cohort membership. if (isset($source['cohortid'])) { $sourcejoin .= "JOIN {cohort_members} cm ON (cm.userid = u.id AND cm.cohortid = :cohortid) "; $params['cohortid'] = $source['cohortid']; } // Auto-create groups from an existing group membership. if (isset($source['groupid'])) { $sourcejoin .= "JOIN {groups_members} gp ON (gp.userid = u.id AND gp.groupid = :groupid) "; $params['groupid'] = $source['groupid']; } // Auto-create groups from an existing grouping membership. if (isset($source['groupingid'])) { $sourcejoin .= "JOIN {groupings_groups} gg ON gg.groupingid = :groupingid "; $sourcejoin .= "JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = gg.groupid) "; $params['groupingid'] = $source['groupingid']; } } $userfieldsapi = \core_user\fields::for_userpic()->including(...$extrafields); $allusernamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $sql = "SELECT DISTINCT u.id, u.username, $allusernamefields, u.idnumber FROM {user} u JOIN ($esql) e ON e.id = u.id $sourcejoin $where ORDER BY $orderby"; return $DB->get_records_sql($sql, $params); } /** * Parse a group name for characters to replace * * @param string $format The format a group name will follow * @param int $groupnumber The number of the group to be used in the parsed format string * @return string the parsed format string */ function groups_parse_name($format, $groupnumber) { if (strstr($format, '@') !== false) { // Convert $groupnumber to a character series $letter = 'A'; for($i=0; $i<$groupnumber; $i++) { $letter++; } $str = str_replace('@', $letter, $format); } else { $str = str_replace('#', $groupnumber+1, $format); } return($str); } /** * Assigns group into grouping * * @param int groupingid * @param int groupid * @param int $timeadded The time the group was added to the grouping. * @param bool $invalidatecache If set to true the course group cache and the user group cache will be invalidated as well. * @return bool true or exception */ function groups_assign_grouping($groupingid, $groupid, $timeadded = null, $invalidatecache = true) { global $DB; if ($DB->record_exists('groupings_groups', array('groupingid'=>$groupingid, 'groupid'=>$groupid))) { return true; } $assign = new stdClass(); $assign->groupingid = $groupingid; $assign->groupid = $groupid; if ($timeadded != null) { $assign->timeadded = (integer)$timeadded; } else { $assign->timeadded = time(); } $DB->insert_record('groupings_groups', $assign); $courseid = $DB->get_field('groupings', 'courseid', array('id' => $groupingid)); if ($invalidatecache) { // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); } // Trigger event. $params = array( 'context' => context_course::instance($courseid), 'objectid' => $groupingid, 'other' => array('groupid' => $groupid) ); $event = \core\event\grouping_group_assigned::create($params); $event->trigger(); return true; } /** * Unassigns group from grouping * * @param int groupingid * @param int groupid * @param bool $invalidatecache If set to true the course group cache and the user group cache will be invalidated as well. * @return bool success */ function groups_unassign_grouping($groupingid, $groupid, $invalidatecache = true) { global $DB; $DB->delete_records('groupings_groups', array('groupingid'=>$groupingid, 'groupid'=>$groupid)); $courseid = $DB->get_field('groupings', 'courseid', array('id' => $groupingid)); if ($invalidatecache) { // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); } // Trigger event. $params = array( 'context' => context_course::instance($courseid), 'objectid' => $groupingid, 'other' => array('groupid' => $groupid) ); $event = \core\event\grouping_group_unassigned::create($params); $event->trigger(); return true; } /** * Lists users in a group based on their role on the course. * Returns false if there's an error or there are no users in the group. * Otherwise returns an array of role ID => role data, where role data includes: * (role) $id, $shortname, $name * $users: array of objects for each user which include the specified fields * Users who do not have a role are stored in the returned array with key '-' * and pseudo-role details (including a name, 'No role'). Users with multiple * roles, same deal with key '*' and name 'Multiple roles'. You can find out * which roles each has by looking in the $roles array of the user object. * * @param int $groupid * @param int $courseid Course ID (should match the group's course) * @param string $fields List of fields from user table (prefixed with u) and joined tables, default 'u.*' * @param string|null $sort SQL ORDER BY clause, default (when null passed) is what comes from users_order_by_sql. * @param string $extrawheretest extra SQL conditions ANDed with the existing where clause. * @param array $whereorsortparams any parameters required by $extrawheretest or $joins (named parameters). * @param string $joins any joins required to get the specified fields. * @return array Complex array as described above */ function groups_get_members_by_role(int $groupid, int $courseid, string $fields = 'u.*', ?string $sort = null, string $extrawheretest = '', array $whereorsortparams = [], string $joins = '') { global $DB; // Retrieve information about all users and their roles on the course or // parent ('related') contexts $context = context_course::instance($courseid); // We are looking for all users with this role assigned in this context or higher. list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); if ($extrawheretest) { $extrawheretest = ' AND ' . $extrawheretest; } if (is_null($sort)) { list($sort, $sortparams) = users_order_by_sql('u'); $whereorsortparams = array_merge($whereorsortparams, $sortparams); } $sql = "SELECT r.id AS roleid, u.id AS userid, $fields FROM {groups_members} gm JOIN {user} u ON u.id = gm.userid LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid $relatedctxsql) LEFT JOIN {role} r ON r.id = ra.roleid $joins WHERE gm.groupid=:mgroupid ".$extrawheretest." ORDER BY r.sortorder, $sort"; $whereorsortparams = array_merge($whereorsortparams, $relatedctxparams, array('mgroupid' => $groupid)); $rs = $DB->get_recordset_sql($sql, $whereorsortparams); return groups_calculate_role_people($rs, $context); } /** * Internal function used by groups_get_members_by_role to handle the * results of a database query that includes a list of users and possible * roles on a course. * * @param moodle_recordset $rs The record set (may be false) * @param int $context ID of course context * @return array As described in groups_get_members_by_role */ function groups_calculate_role_people($rs, $context) { global $CFG, $DB; if (!$rs) { return array(); } $allroles = role_fix_names(get_all_roles($context), $context); $visibleroles = get_viewable_roles($context); // Array of all involved roles $roles = array(); // Array of all retrieved users $users = array(); // Fill arrays foreach ($rs as $rec) { // Create information about user if this is a new one if (!array_key_exists($rec->userid, $users)) { // User data includes all the optional fields, but not any of the // stuff we added to get the role details $userdata = clone($rec); unset($userdata->roleid); unset($userdata->roleshortname); unset($userdata->rolename); unset($userdata->userid); $userdata->id = $rec->userid; // Make an array to hold the list of roles for this user $userdata->roles = array(); $users[$rec->userid] = $userdata; } // If user has a role... if (!is_null($rec->roleid)) { // Create information about role if this is a new one if (!array_key_exists($rec->roleid, $roles)) { $role = $allroles[$rec->roleid]; $roledata = new stdClass(); $roledata->id = $role->id; $roledata->shortname = $role->shortname; $roledata->name = $role->localname; $roledata->users = array(); $roles[$roledata->id] = $roledata; } // Record that user has role $users[$rec->userid]->roles[$rec->roleid] = $roles[$rec->roleid]; } } $rs->close(); // Return false if there weren't any users if (count($users) == 0) { return false; } // Add pseudo-role for multiple roles $roledata = new stdClass(); $roledata->name = get_string('multipleroles','role'); $roledata->users = array(); $roles['*'] = $roledata; $roledata = new stdClass(); $roledata->name = get_string('noroles','role'); $roledata->users = array(); $roles[0] = $roledata; // Now we rearrange the data to store users by role foreach ($users as $userid=>$userdata) { $visibleuserroles = array_intersect_key($userdata->roles, $visibleroles); $rolecount = count($visibleuserroles); if ($rolecount == 0) { // does not have any roles $roleid = 0; } else if($rolecount > 1) { $roleid = '*'; } else { $userrole = reset($visibleuserroles); $roleid = $userrole->id; } $roles[$roleid]->users[$userid] = $userdata; } // Delete roles not used foreach ($roles as $key=>$roledata) { if (count($roledata->users)===0) { unset($roles[$key]); } } // Return list of roles containing their users return $roles; } /** * Synchronises enrolments with the group membership * * Designed for enrolment methods provide automatic synchronisation between enrolled users * and group membership, such as enrol_cohort and enrol_meta . * * @param string $enrolname name of enrolment method without prefix * @param int $courseid course id where sync needs to be performed (0 for all courses) * @param string $gidfield name of the field in 'enrol' table that stores group id * @return array Returns the list of removed and added users. Each record contains fields: * userid, enrolid, courseid, groupid, groupname */ function groups_sync_with_enrolment($enrolname, $courseid = 0, $gidfield = 'customint2') { global $DB; $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; $params = array( 'enrolname' => $enrolname, 'component' => 'enrol_'.$enrolname, 'courseid' => $courseid ); $affectedusers = array( 'removed' => array(), 'added' => array() ); // Remove invalid. $sql = "SELECT ue.userid, ue.enrolid, e.courseid, g.id AS groupid, g.name AS groupname FROM {groups_members} gm JOIN {groups} g ON (g.id = gm.groupid) JOIN {enrol} e ON (e.enrol = :enrolname AND e.courseid = g.courseid $onecourse) JOIN {user_enrolments} ue ON (ue.userid = gm.userid AND ue.enrolid = e.id) WHERE gm.component=:component AND gm.itemid = e.id AND g.id <> e.{$gidfield}"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $gm) { groups_remove_member($gm->groupid, $gm->userid); $affectedusers['removed'][] = $gm; } $rs->close(); // Add missing. $sql = "SELECT ue.userid, ue.enrolid, e.courseid, g.id AS groupid, g.name AS groupname FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrolname $onecourse) JOIN {groups} g ON (g.courseid = e.courseid AND g.id = e.{$gidfield}) JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0) LEFT JOIN {groups_members} gm ON (gm.groupid = g.id AND gm.userid = ue.userid) WHERE gm.id IS NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $ue) { groups_add_member($ue->groupid, $ue->userid, 'enrol_'.$enrolname, $ue->enrolid); $affectedusers['added'][] = $ue; } $rs->close(); return $affectedusers; } /** * Callback for inplace editable API. * * @param string $itemtype - Only user_groups is supported. * @param string $itemid - Userid and groupid separated by a : * @param string $newvalue - json encoded list of groupids. * @return \core\output\inplace_editable */ function core_group_inplace_editable($itemtype, $itemid, $newvalue) { if ($itemtype === 'user_groups') { return \core_group\output\user_groups_editable::update($itemid, $newvalue); } } /** * Updates group messaging to enable/disable in bulk. * * @param array $groupids array of group id numbers. * @param bool $enabled if true, enables messaging else disables messaging */ function set_groups_messaging(array $groupids, bool $enabled): void { foreach ($groupids as $groupid) { $data = groups_get_group($groupid, '*', MUST_EXIST); $data->enablemessaging = $enabled; groups_update_group($data); } } /** * Returns custom fields data for provided groups. * * @param array $groupids a list of group IDs to provide data for. * @return \core_customfield\data_controller[] */ function get_group_custom_fields_data(array $groupids): array { $result = []; if (!empty($groupids)) { $handler = \core_group\customfield\group_handler::create(); $customfieldsdata = $handler->get_instances_data($groupids, true); foreach ($customfieldsdata as $groupid => $fieldcontrollers) { foreach ($fieldcontrollers as $fieldcontroller) { $result[$groupid][] = [ 'type' => $fieldcontroller->get_field()->get('type'), 'value' => $fieldcontroller->export_value(), 'valueraw' => $fieldcontroller->get_value(), 'name' => $fieldcontroller->get_field()->get('name'), 'shortname' => $fieldcontroller->get_field()->get('shortname'), ]; } } } return $result; } /** * Returns custom fields data for provided groupings. * * @param array $groupingids a list of group IDs to provide data for. * @return \core_customfield\data_controller[] */ function get_grouping_custom_fields_data(array $groupingids): array { $result = []; if (!empty($groupingids)) { $handler = \core_group\customfield\grouping_handler::create(); $customfieldsdata = $handler->get_instances_data($groupingids, true); foreach ($customfieldsdata as $groupingid => $fieldcontrollers) { foreach ($fieldcontrollers as $fieldcontroller) { $result[$groupingid][] = [ 'type' => $fieldcontroller->get_field()->get('type'), 'value' => $fieldcontroller->export_value(), 'valueraw' => $fieldcontroller->get_value(), 'name' => $fieldcontroller->get_field()->get('name'), 'shortname' => $fieldcontroller->get_field()->get('shortname'), ]; } } } return $result; } classes/external/get_groups_for_selector.php 0000644 00000015523 15215711721 0015470 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group\external; use context_course; use context_module; use core_external\external_api; use core_external\external_description; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_grades\external\coding_exception; use core_grades\external\invalid_parameter_exception; use core_grades\external\moodle_exception; use core_grades\external\restricted_context_exception; use moodle_url; /** * External group name and image API implementation * * @package core_group * @copyright 2022 Mathew May <mathew.solutions> * @category external * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_groups_for_selector extends external_api { /** * Returns description of method parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters ( [ 'courseid' => new external_value(PARAM_INT, 'Course Id', VALUE_REQUIRED), 'cmid' => new external_value(PARAM_INT, 'Course module Id', VALUE_DEFAULT, 0), ] ); } /** * Given a course ID find the existing user groups and map some fields to the returned array of group objects. * * If a course module ID is provided, this function will return only the available groups within the given course * module, adhering to the set group mode for that context. All validation checks will be performed within this * specific context. * * @param int $courseid * @param int|null $cmid The course module ID (optional). * @return array Groups and warnings to pass back to the calling widget. */ public static function execute(int $courseid, ?int $cmid = null): array { global $DB, $USER, $OUTPUT; $params = self::validate_parameters( self::execute_parameters(), [ 'courseid' => $courseid, 'cmid' => $cmid, ] ); $warnings = []; $course = $DB->get_record('course', ['id' => $params['courseid']]); if ($params['cmid']) { $context = context_module::instance($params['cmid']); $cm = get_coursemodule_from_id('', $params['cmid']); $groupmode = groups_get_activity_groupmode($cm, $course); $groupingid = $cm->groupingid; $participationonly = true; } else { $context = context_course::instance($params['courseid']); $groupmode = $course->groupmode; $groupingid = $course->defaultgroupingid; $participationonly = false; } parent::validate_context($context); $mappedgroups = []; // Initialise the grade tracking object. if ($groupmode) { $aag = has_capability('moodle/site:accessallgroups', $context); $usergroups = []; if ($groupmode == VISIBLEGROUPS || $aag) { $groupuserid = 0; // Get user's own groups and put to the top. $usergroups = groups_get_all_groups( courseid: $course->id, userid: $USER->id, groupingid: $groupingid, participationonly: $participationonly ); } else { $groupuserid = $USER->id; } $allowedgroups = groups_get_all_groups( courseid: $course->id, userid: $groupuserid, groupingid: $groupingid, participationonly: $participationonly ); $allgroups = array_merge($allowedgroups, $usergroups); // Filter out any duplicate groups. $groupsmenu = array_intersect_key($allgroups, array_unique(array_column($allgroups, 'name'))); if (!$allowedgroups || $groupmode == VISIBLEGROUPS || $aag) { array_unshift($groupsmenu, (object) [ 'id' => 0, 'name' => get_string('allparticipants'), ]); } $mappedgroups = array_map(function($group) use ($context, $OUTPUT) { if ($group->id) { // Particular group. Get the group picture if it exists, otherwise return a generic image. $picture = get_group_picture_url($group, $group->courseid, true) ?? moodle_url::make_pluginfile_url($context->get_course_context()->id, 'group', 'generated', $group->id, '/', 'group.svg'); } else { // All participants. $picture = $OUTPUT->image_url('g/g1'); } return (object) [ 'id' => $group->id, 'name' => format_string($group->name, true, ['context' => $context]), 'groupimageurl' => $picture->out(false), ]; }, $groupsmenu); } return [ 'groups' => $mappedgroups, 'warnings' => $warnings, ]; } /** * Returns description of what the group search for the widget should return. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'groups' => new external_multiple_structure(self::group_description()), 'warnings' => new external_warnings(), ]); } /** * Create group return value description. * * @return external_description */ public static function group_description(): external_description { $groupfields = [ 'id' => new external_value(PARAM_ALPHANUM, 'An ID for the group', VALUE_REQUIRED), 'name' => new external_value(PARAM_TEXT, 'The full name of the group', VALUE_REQUIRED), 'groupimageurl' => new external_value(PARAM_URL, 'Group image URL', VALUE_OPTIONAL), ]; return new external_single_structure($groupfields); } } classes/hook/after_group_deleted.php 0000644 00000002510 15215711721 0013655 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group\hook; use stdClass; /** * Hook after group deletion. * * @package core_group * @copyright 2024 Safat Shahin <safat.shahin@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after a group is deleted.')] #[\core\attribute\tags('group')] class after_group_deleted { /** * Constructor for the hook. * * @param stdClass $groupinstance The group instance. */ public function __construct( /** @var stdClass The group instance */ public readonly stdClass $groupinstance, ) { } } classes/hook/after_group_membership_removed.php 0000644 00000002762 15215711721 0016134 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group\hook; use stdClass; /** * Hook after a member removed from the group. * * @package core_group * @copyright 2024 Safat Shahin <safat.shahin@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after members removed from the group.')] #[\core\attribute\tags('group', 'user')] class after_group_membership_removed { /** * Constructor for the hook. * * @param stdClass $groupinstance The group instance. * @param array $userids The user ids. */ public function __construct( /** @var stdClass The group instance */ public readonly stdClass $groupinstance, /** @var array The user ids */ public readonly array $userids, ) { } } classes/hook/after_group_updated.php 0000644 00000002507 15215711721 0013703 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group\hook; use stdClass; /** * Hook after group updates. * * @package core_group * @copyright 2023 Safat Shahin <safat.shahin@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after a group is updated.')] #[\core\attribute\tags('group')] class after_group_updated { /** * Constructor for the hook. * * @param stdClass $groupinstance The group instance. */ public function __construct( /** @var stdClass The group instance */ public readonly stdClass $groupinstance, ) { } } classes/hook/after_group_created.php 0000644 00000002510 15215711721 0013656 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group\hook; use stdClass; /** * Hook after group creation. * * @package core_group * @copyright 2024 Safat Shahin <safat.shahin@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after a group is created.')] #[\core\attribute\tags('group')] class after_group_created { /** * Constructor for the hook. * * @param stdClass $groupinstance The group instance. */ public function __construct( /** @var stdClass The group instance */ public readonly stdClass $groupinstance, ) { } } classes/hook/after_group_membership_added.php 0000644 00000002750 15215711721 0015531 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group\hook; use stdClass; /** * Hook after a member added to the group. * * @package core_group * @copyright 2023 Safat Shahin <safat.shahin@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after members added to the group.')] #[\core\attribute\tags('group', 'user')] class after_group_membership_added { /** * Constructor for the hook. * * @param stdClass $groupinstance The group instance. * @param array $userids The user ids. */ public function __construct( /** @var stdClass The group instance */ public readonly stdClass $groupinstance, /** @var array The user ids */ public readonly array $userids, ) { } } classes/output/renderer.php 0000644 00000003412 15215711721 0012062 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Renderers. * * @package core_group * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_group\output; defined('MOODLE_INTERNAL') || die(); use plugin_renderer_base; /** * Renderer class. * * @package core_group * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends plugin_renderer_base { /** * Defer to template. * * @param index_page $page * @return string */ public function render_index_page(index_page $page) { $data = $page->export_for_template($this); return parent::render_from_template('core_group/index', $data); } /** * Defer to template. * * @param group_details $page Group details page object. * @return string HTML to render the group details. */ public function group_details(group_details $page) { $data = $page->export_for_template($this); return parent::render_from_template('core_group/group_details', $data); } } classes/output/group_details.php 0000644 00000006274 15215711721 0013126 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Group details page. * * @package core_group * @copyright 2017 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_group\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use context_course; use moodle_url; /** * Group details page class. * * @package core_group * @copyright 2017 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_details implements renderable, templatable { /** @var stdClass $group An object with the group information. */ protected $group; /** * group_details constructor. * * @param int $groupid Group ID to show details of. */ public function __construct($groupid) { $this->group = groups_get_group($groupid, '*', MUST_EXIST); } /** * Export the data. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { if (!empty($this->group->description) || (!empty($this->group->picture))) { $context = context_course::instance($this->group->courseid); $description = file_rewrite_pluginfile_urls($this->group->description, 'pluginfile.php', $context->id, 'group', 'description', $this->group->id); $descriptionformat = $this->group->descriptionformat ?? FORMAT_MOODLE; $options = [ 'overflowdiv' => true, 'context' => $context ]; $data = new stdClass(); $data->name = format_string($this->group->name, true, ['context' => $context]); $data->pictureurl = get_group_picture_url($this->group, $this->group->courseid, true); $data->description = format_text($description, $descriptionformat, $options); if (has_capability('moodle/course:managegroups', $context)) { $url = new moodle_url('/group/group.php', ['id' => $this->group->id, 'courseid' => $this->group->courseid]); $data->editurl = $url->out(false); } return $data; } else { return; } } } classes/output/index_page.php 0000644 00000010364 15215711721 0012363 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Group index page. * * @package core_group * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_group\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; /** * Group index page class. * * @package core_group * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class index_page implements renderable, templatable { /** @var int $courseid The course ID. */ public $courseid; /** @var array The array of groups to be rendered. */ public $groups; /** @var string The name of the currently selected group. */ public $selectedgroupname; /** @var array The array of group members to be rendered, if a group is selected. */ public $selectedgroupmembers; /** @var bool Whether to disable the add members/edit group buttons. */ public $disableaddedit; /** @var bool Whether to disable the delete group button. */ public $disabledelete; /** @var array Groups that can't be deleted by the user. */ public $undeletablegroups; /** @var bool Whether to show/hide the messaging setting buttons. */ public $messagingsettingsvisible; /** * index_page constructor. * * @param int $courseid The course ID. * @param array $groups The array of groups to be rendered. * @param string $selectedgroupname The name of the currently selected group. * @param array $selectedgroupmembers The array of group members to be rendered, if a group is selected. * @param bool $disableaddedit Whether to disable the add members/edit group buttons. * @param bool $disabledelete Whether to disable the delete group button. * @param array $undeletablegroups Groups that can't be deleted by the user. * @param bool $messagingsettingsvisible If the messaging settings buttons should be visible. */ public function __construct($courseid, $groups, $selectedgroupname, $selectedgroupmembers, $disableaddedit, $disabledelete, $undeletablegroups, $messagingsettingsvisible) { $this->courseid = $courseid; $this->groups = $groups; $this->selectedgroupname = $selectedgroupname; $this->selectedgroupmembers = $selectedgroupmembers; $this->disableaddedit = $disableaddedit; $this->disabledelete = $disabledelete; $this->undeletablegroups = $undeletablegroups; $this->messagingsettingsvisible = $messagingsettingsvisible; } /** * Export the data. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $CFG; $data = new stdClass(); // Variables that will be passed to the JS helper. $data->courseid = $this->courseid; $data->wwwroot = $CFG->wwwroot; // To be passed to the JS init script in the template. Encode as a JSON string. $data->undeletablegroups = json_encode($this->undeletablegroups); // Some buttons are enabled if single group selected. $data->addmembersdisabled = $this->disableaddedit; $data->editgroupsettingsdisabled = $this->disableaddedit; $data->deletegroupdisabled = $this->disabledelete; $data->groups = $this->groups; $data->members = $this->selectedgroupmembers; $data->selectedgroup = $this->selectedgroupname; $data->messagingsettingsvisible = $this->messagingsettingsvisible; return $data; } } classes/output/user_groups_editable.php 0000644 00000014223 15215711721 0014464 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * 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_course; use core_user; use core_external; use coding_exception; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/group/lib.php'); /** * 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 $course The current course * @param \context $context The course context * @param \stdClass $user The current user * @param \stdClass[] $coursegroups The list of course groups from groups_get_all_groups with membership. * @param array $value Array of groupids. */ public function __construct($course, $context, $user, $coursegroups, $value) { // Check capabilities to get editable value. $editable = has_capability('moodle/course:managegroups', $context) && !empty($coursegroups); // 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] = format_string($group->name, true, ['context' => $this->context]); } $fullname = fullname($user, has_capability('moodle/site:viewfullnames', $this->context)); $fullname = htmlspecialchars($fullname, ENT_QUOTES, 'utf-8'); $this->edithint = get_string('editusersgroupsa', 'group', $fullname); $this->editlabel = get_string('editusersgroupsa', 'group', $fullname); $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 array */ public function export_for_template(\renderer_base $output) { $listofgroups = []; $groupids = json_decode($this->value); foreach ($groupids as $id) { $listofgroups[] = format_string($this->coursegroups[$id]->name, true, ['context' => $this->context]); } if (!empty($listofgroups)) { $this->displayvalue = implode(', ', $listofgroups); } else { $this->displayvalue = get_string('groupsnone'); } 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]); } else { $groupids[$groupid] = $groupid; } } } $course = get_course($courseid); $user = core_user::get_user($userid); return new self($course, $context, $user, $coursegroups, array_values($groupids)); } } classes/privacy/provider.php 0000644 00000036306 15215711721 0012233 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for core_group. * * @package core_group * @category privacy * @copyright 2018 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_group\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\userlist; /** * Privacy Subsystem implementation for core_group. * * @copyright 2018 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements // Groups store user data. \core_privacy\local\metadata\provider, // The group subsystem contains user's group memberships. \core_privacy\local\request\subsystem\provider, // The group subsystem can provide information to other plugins. \core_privacy\local\request\subsystem\plugin_provider, // This plugin is capable of determining which users have data within it. \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\shared_userlist_provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_database_table('groups_members', [ 'groupid' => 'privacy:metadata:groups:groupid', 'userid' => 'privacy:metadata:groups:userid', 'timeadded' => 'privacy:metadata:groups:timeadded', ], 'privacy:metadata:groups'); $collection->link_subsystem('core_message', 'privacy:metadata:core_message'); return $collection; } /** * Writes user data to the writer for the user to download. * * @param \context $context The context to export data for. * @param string $component The component that is calling this function. Empty string means no component. * @param array $subcontext The sub-context in which to export this data. * @param int $itemid Optional itemid associated with component. */ public static function export_groups(\context $context, string $component, array $subcontext = [], int $itemid = 0) { global $DB, $USER; if (!$context instanceof \context_course) { return; } $subcontext[] = get_string('groups', 'core_group'); $sql = "SELECT gm.id, gm.timeadded, gm.userid, g.name, gm.groupid FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = :courseid AND gm.component = :component AND gm.userid = :userid"; $params = [ 'courseid' => $context->instanceid, 'component' => $component, 'userid' => $USER->id ]; if ($itemid) { $sql .= ' AND gm.itemid = :itemid'; $params['itemid'] = $itemid; } $groups = $DB->get_records_sql($sql, $params); $groupstoexport = array_map(function($group) { return (object) [ 'name' => format_string($group->name), 'timeadded' => transform::datetime($group->timeadded), ]; }, $groups); if (!empty($groups)) { \core_privacy\local\request\writer::with_context($context) ->export_data($subcontext, (object) [ 'groups' => $groupstoexport, ]); foreach ($groups as $group) { // Export associated conversations to this group. \core_message\privacy\provider::export_conversations($USER->id, 'core_group', 'groups', $context, [], $group->groupid); } } } /** * Deletes all group memberships for a specified context and component. * * @param \context $context Details about which context to delete group memberships for. * @param string $component Component to delete. Empty string means no component (manual group memberships). * @param int $itemid Optional itemid associated with component. */ public static function delete_groups_for_all_users(\context $context, string $component, int $itemid = 0) { global $DB; if (!$context instanceof \context_course) { return; } if (!$DB->record_exists('groups', ['courseid' => $context->instanceid])) { return; } $select = "component = :component AND groupid IN (SELECT g.id FROM {groups} g WHERE courseid = :courseid)"; $params = ['component' => $component, 'courseid' => $context->instanceid]; if ($itemid) { $select .= ' AND itemid = :itemid'; $params['itemid'] = $itemid; } // Delete the group conversations. $groups = $DB->get_records_select('groups_members', $select, $params); foreach ($groups as $group) { \core_message\privacy\provider::delete_conversations_for_all_users($context, 'core_group', 'groups', $group->groupid); } // Remove members from the group. $DB->delete_records_select('groups_members', $select, $params); // Purge the group and grouping cache for users. \cache_helper::purge_by_definition('core', 'user_group_groupings'); } /** * Deletes all records for a user from a list of approved contexts. * * @param approved_contextlist $contextlist Contains the user ID and a list of contexts to be deleted from. * @param string $component Component to delete from. Empty string means no component (manual memberships). * @param int $itemid Optional itemid associated with component. */ public static function delete_groups_for_user(approved_contextlist $contextlist, string $component, int $itemid = 0) { global $DB; $userid = $contextlist->get_user()->id; $contextids = $contextlist->get_contextids(); if (!$contextids) { return; } list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); $contextparams += ['contextcourse' => CONTEXT_COURSE]; $groupselect = "SELECT g.id FROM {groups} g JOIN {context} ctx ON g.courseid = ctx.instanceid AND ctx.contextlevel = :contextcourse WHERE ctx.id $contextsql"; if (!$DB->record_exists_sql($groupselect, $contextparams)) { return; } $select = "userid = :userid AND component = :component AND groupid IN ({$groupselect})"; $params = ['userid' => $userid, 'component' => $component] + $contextparams; if ($itemid) { $select .= ' AND itemid = :itemid'; $params['itemid'] = $itemid; } // Delete the group conversations. $groups = $DB->get_records_select('groups_members', $select, $params); foreach ($groups as $group) { \core_message\privacy\provider::delete_conversations_for_user($contextlist, 'core_group', 'groups', $group->groupid); } // Remove members from the group. $DB->delete_records_select('groups_members', $select, $params); // Invalidate the group and grouping cache for the user. \cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), array($userid)); } /** * Add the list of users who are members of some groups in the specified constraints. * * @param userlist $userlist The userlist to add the users to. * @param string $component The component to check. * @param int $itemid Optional itemid associated with component. */ public static function get_group_members_in_context(userlist $userlist, string $component, int $itemid = 0) { $context = $userlist->get_context(); if (!$context instanceof \context_course) { return; } // Group members in the given context. $sql = "SELECT gm.userid FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = :courseid AND gm.component = :component"; $params = [ 'courseid' => $context->instanceid, 'component' => $component ]; if ($itemid) { $sql .= ' AND gm.itemid = :itemid'; $params['itemid'] = $itemid; } $userlist->add_from_sql('userid', $sql, $params); // Get the users with some group conversation in this context. \core_message\privacy\provider::add_conversations_in_context($userlist, 'core_group', 'groups', $itemid); } /** * Deletes all records for multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. * @param string $component Component to delete from. Empty string means no component (manual memberships). * @param int $itemid Optional itemid associated with component. */ public static function delete_groups_for_users(approved_userlist $userlist, string $component, int $itemid = 0) { global $DB; $context = $userlist->get_context(); $userids = $userlist->get_userids(); if (!$context instanceof \context_course) { return; } list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $groupselect = "SELECT id FROM {groups} WHERE courseid = :courseid"; $groupparams = ['courseid' => $context->instanceid]; $select = "component = :component AND userid {$usersql} AND groupid IN ({$groupselect})"; $params = ['component' => $component] + $groupparams + $userparams; if ($itemid) { $select .= ' AND itemid = :itemid'; $params['itemid'] = $itemid; } // Delete the group conversations for these users. $groups = $DB->get_records_select('groups_members', $select, $params); foreach ($groups as $group) { \core_message\privacy\provider::delete_conversations_for_users($userlist, 'core_group', 'groups', $group->groupid); } $DB->delete_records_select('groups_members', $select, $params); // Invalidate the group and grouping cache for the user. \cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), $userids); } /** * Get the list of contexts that contain group membership for the specified user. * * @param int $userid The user to search. * @param string $component The component to check. * @param int $itemid Optional itemid associated with component. * @return contextlist The contextlist containing the list of contexts. */ public static function get_contexts_for_group_member(int $userid, string $component, int $itemid = 0) { $contextlist = new contextlist(); $sql = "SELECT ctx.id FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id JOIN {context} ctx ON g.courseid = ctx.instanceid AND ctx.contextlevel = :contextcourse WHERE gm.userid = :userid AND gm.component = :component"; $params = [ 'contextcourse' => CONTEXT_COURSE, 'userid' => $userid, 'component' => $component ]; if ($itemid) { $sql .= ' AND gm.itemid = :itemid'; $params['itemid'] = $itemid; } $contextlist->add_from_sql($sql, $params); // Get the contexts where the userid has group conversations. \core_message\privacy\provider::add_contexts_for_conversations($contextlist, $userid, 'core_group', 'groups', $itemid); return $contextlist; } /** * Get the list of users who have data within a context. * * @param int $userid The user to search. * @return contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { return static::get_contexts_for_group_member($userid, ''); } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_course) { return; } static::get_group_members_in_context($userlist, ''); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { $contexts = $contextlist->get_contexts(); foreach ($contexts as $context) { static::export_groups($context, ''); } } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { static::delete_groups_for_all_users($context, ''); } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { static::delete_groups_for_user($contextlist, ''); } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { static::delete_groups_for_users($userlist, ''); } } classes/customfield/grouping_handler.php 0000644 00000015156 15215711721 0014571 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group\customfield; use context; use context_course; use context_system; use core_customfield\api; use core_customfield\handler; use core_customfield\field_controller; use moodle_url; use restore_task; /** * Grouping handler for custom fields. * * @package core_group * @author Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> * @copyright 2023 Catalyst IT Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grouping_handler extends handler { /** * @var grouping_handler */ static protected $singleton; /** * Returns a singleton. * * @param int $itemid * @return \core_customfield\handler */ public static function create(int $itemid = 0): handler { if (static::$singleton === null) { self::$singleton = new static(0); } return self::$singleton; } /** * Run reset code after unit tests to reset the singleton usage. */ public static function reset_caches(): void { if (!PHPUNIT_TEST) { throw new \coding_exception('This feature is only intended for use in unit tests'); } static::$singleton = null; } /** * The current user can configure custom fields on this component. * * @return bool true if the current can configure custom fields, false otherwise */ public function can_configure(): bool { return has_capability('moodle/group:configurecustomfields', $this->get_configuration_context()); } /** * The current user can edit custom fields on the given group. * * @param field_controller $field * @param int $instanceid id of the group to test edit permission * @return bool true if the current can edit custom field, false otherwise */ public function can_edit(field_controller $field, int $instanceid = 0): bool { return has_capability('moodle/course:managegroups', $this->get_instance_context($instanceid)); } /** * The current user can view custom fields on the given group. * * @param field_controller $field * @param int $instanceid id of the group to test edit permission * @return bool true if the current can view custom field, false otherwise */ public function can_view(field_controller $field, int $instanceid): bool { return has_any_capability(['moodle/course:managegroups', 'moodle/course:view'], $this->get_instance_context($instanceid)); } /** * Context that should be used for new categories created by this handler. * * @return context the context for configuration */ public function get_configuration_context(): context { return context_system::instance(); } /** * URL for configuration of the fields on this handler. * * @return moodle_url The URL to configure custom fields for this component */ public function get_configuration_url(): moodle_url { return new moodle_url('/group/grouping_customfield.php'); } /** * Returns the context for the data associated with the given instanceid. * * @param int $instanceid id of the record to get the context for * @return context the context for the given record */ public function get_instance_context(int $instanceid = 0): context { global $COURSE, $DB; if ($instanceid > 0) { $grouping = $DB->get_record('groupings', ['id' => $instanceid], '*', MUST_EXIST); return context_course::instance($grouping->courseid); } else if (!empty($COURSE->id)) { return context_course::instance($COURSE->id); } else { return context_system::instance(); } } /** * Get raw data associated with all fields current user can view or edit * * @param int $instanceid * @return array */ public function get_instance_data_for_backup(int $instanceid): array { $finalfields = []; $instancedata = $this->get_instance_data($instanceid, true); foreach ($instancedata as $data) { if ($data->get('id') && $this->can_backup($data->get_field(), $instanceid)) { $finalfields[] = [ 'id' => $data->get('id'), 'shortname' => $data->get_field()->get('shortname'), 'type' => $data->get_field()->get('type'), 'value' => $data->get_value(), 'valueformat' => $data->get('valueformat'), 'valuetrust' => $data->get('valuetrust'), 'groupingid' => $data->get('instanceid'), ]; } } return $finalfields; } /** * Creates or updates custom field data for a instanceid from backup data. * * The handlers have to override it if they support backup * * @param restore_task $task * @param array $data * * @return int|void Conditionally returns the ID of the created or updated record. */ public function restore_instance_data_from_backup(restore_task $task, array $data) { $instanceid = $data['groupingid']; $context = $this->get_instance_context($instanceid); $editablefields = $this->get_editable_fields($instanceid); $records = api::get_instance_fields_data($editablefields, $instanceid); foreach ($records as $d) { $field = $d->get_field(); if ($field->get('shortname') === $data['shortname'] && $field->get('type') === $data['type']) { if (!$d->get('id')) { $d->set($d->datafield(), $data['value']); $d->set('value', $data['value']); $d->set('valueformat', $data['valueformat']); $d->set('valuetrust', !empty($data['valuetrust'])); $d->set('contextid', $context->id); $d->save(); } return $d->get('id'); } } } } classes/customfield/group_handler.php 0000644 00000014747 15215711721 0014100 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group\customfield; use context; use context_course; use context_system; use core_customfield\api; use core_customfield\handler; use core_customfield\field_controller; use moodle_url; use restore_task; /** * Group handler for custom fields. * * @package core_group * @author Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> * @copyright 2023 Catalyst IT Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_handler extends handler { /** * @var group_handler */ static protected $singleton; /** * Returns a singleton. * * @param int $itemid * @return \core_customfield\handler */ public static function create(int $itemid = 0): handler { if (static::$singleton === null) { self::$singleton = new static(0); } return self::$singleton; } /** * Run reset code after unit tests to reset the singleton usage. */ public static function reset_caches(): void { if (!PHPUNIT_TEST) { throw new \coding_exception('This feature is only intended for use in unit tests'); } static::$singleton = null; } /** * The current user can configure custom fields on this component. * * @return bool true if the current can configure custom fields, false otherwise */ public function can_configure(): bool { return has_capability('moodle/group:configurecustomfields', $this->get_configuration_context()); } /** * The current user can edit custom fields on the given group. * * @param field_controller $field * @param int $instanceid id of the group to test edit permission * @return bool true if the current can edit custom field, false otherwise */ public function can_edit(field_controller $field, int $instanceid = 0): bool { return has_capability('moodle/course:managegroups', $this->get_instance_context($instanceid)); } /** * The current user can view custom fields on the given group. * * @param field_controller $field * @param int $instanceid id of the group to test edit permission * @return bool true if the current can view custom field, false otherwise */ public function can_view(field_controller $field, int $instanceid): bool { return has_any_capability(['moodle/course:managegroups', 'moodle/course:view'], $this->get_instance_context($instanceid)); } /** * Context that should be used for new categories created by this handler. * * @return context the context for configuration */ public function get_configuration_context(): context { return context_system::instance(); } /** * URL for configuration of the fields on this handler. * * @return moodle_url The URL to configure custom fields for this component */ public function get_configuration_url(): moodle_url { return new moodle_url('/group/customfield.php'); } /** * Returns the context for the data associated with the given instanceid. * * @param int $instanceid id of the record to get the context for * @return context the context for the given record */ public function get_instance_context(int $instanceid = 0): \context { global $COURSE, $DB; if ($instanceid > 0) { $group = $DB->get_record('groups', ['id' => $instanceid], '*', MUST_EXIST); return context_course::instance($group->courseid); } else if (!empty($COURSE->id)) { return context_course::instance($COURSE->id); } else { return context_system::instance(); } } /** * Get raw data associated with all fields current user can view or edit * * @param int $instanceid * @return array */ public function get_instance_data_for_backup(int $instanceid): array { $finalfields = []; $instancedata = $this->get_instance_data($instanceid, true); foreach ($instancedata as $data) { if ($data->get('id') && $this->can_backup($data->get_field(), $instanceid)) { $finalfields[] = [ 'id' => $data->get('id'), 'shortname' => $data->get_field()->get('shortname'), 'type' => $data->get_field()->get('type'), 'value' => $data->get_value(), 'valueformat' => $data->get('valueformat'), 'valuetrust' => $data->get('valuetrust'), 'groupid' => $data->get('instanceid'), ]; } } return $finalfields; } /** * Creates or updates custom field data. * * @param restore_task $task * @param array $data * * @return int|void Conditionally returns the ID of the created or updated record. */ public function restore_instance_data_from_backup(restore_task $task, array $data) { $instanceid = $data['groupid']; $context = $this->get_instance_context($instanceid); $editablefields = $this->get_editable_fields($instanceid); $records = api::get_instance_fields_data($editablefields, $instanceid); foreach ($records as $d) { $field = $d->get_field(); if ($field->get('shortname') === $data['shortname'] && $field->get('type') === $data['type']) { if (!$d->get('id')) { $d->set($d->datafield(), $data['value']); $d->set('value', $data['value']); $d->set('valueformat', $data['valueformat']); $d->set('valuetrust', !empty($data['valuetrust'])); $d->set('contextid', $context->id); $d->save(); } return $d->get('id'); } } } } classes/visibility.php 0000644 00000016222 15215711721 0011106 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Group visibility methods * * @package core_group * @copyright 2022 onwards Catalyst IT EU {@link https://catalyst-eu.net} * @author Mark Johnson <mark.johnson@catalyst-eu.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_group; /** * Group visibility methods. */ class visibility { /** * Store the number groups with visibility other than ALL on the course. * * @param int $courseid Course ID to update the cache for. * @param \cache|null $cache Existing cache instance. If null, once will be created. * @return void * @throws \dml_exception */ public static function update_hiddengroups_cache(int $courseid, ?\cache $cache = null): void { global $DB; if (!$cache) { $cache = \cache::make('core', 'coursehiddengroups'); } $hiddengroups = $DB->count_records_select('groups', 'courseid = ? AND visibility != ?', [$courseid, GROUPS_VISIBILITY_ALL]); $cache->set($courseid, $hiddengroups); } /** * Return whether a course currently had hidden groups. * * This can be used as a shortcut to decide whether visibility restrictions need to be applied. If this returns false, * we may be able to use cached data, or do a much simpler query. * * @param int $courseid * @return bool * @throws \coding_exception * @throws \dml_exception */ public static function course_has_hidden_groups(int $courseid): bool { $cache = \cache::make('core', 'coursehiddengroups'); $hiddengroups = $cache->get($courseid); if ($hiddengroups === false) { self::update_hiddengroups_cache($courseid, $cache); $cache->get($courseid); } return $hiddengroups > 0; } /** * Can the current user view all the groups on the course? * * Returns true if there are no groups on the course with visibility != ALL, * or if the user has viewhiddengroups. * * This is useful for deciding whether we need to perform additional visibility checkes * such as the sql_* methods of this class. * * @param int $courseid * @return bool */ public static function can_view_all_groups(int $courseid): bool { $viewhidden = has_capability('moodle/course:viewhiddengroups', \context_course::instance($courseid)); $hashidden = self::course_has_hidden_groups($courseid); return $viewhidden || !$hashidden; } /** * Return SQL conditions for determining whether a user can see a group and its memberships. * * @param int $userid * @param string $groupsalias The SQL alias being used for the groups table. * @param string $groupsmembersalias The SQL alias being used for the groups_members table. * @return array [$where, $params] */ public static function sql_group_visibility_where(int $userid, string $groupsalias = 'g', string $groupsmembersalias = 'gm'): array { global $USER; // Apply visibility restrictions. // Everyone can see who is in groups with ALL visibility. $where = "({$groupsalias}.visibility = :all"; $params['all'] = GROUPS_VISIBILITY_ALL; if ($userid == $USER->id) { // If the user is looking at their own groups, they can see those with MEMBERS or OWN visibility. $where .= " OR {$groupsalias}.visibility IN (:members, :own)"; $params['members'] = GROUPS_VISIBILITY_MEMBERS; $params['own'] = GROUPS_VISIBILITY_OWN; } else { list($memberssql, $membersparams) = self::sql_members_visibility_condition($groupsalias, $groupsmembersalias); // If someone else's groups, they can see those with MEMBERS visibilty, only if they are a member too. $where .= " OR ($memberssql)"; $params = array_merge($params, $membersparams); } $where .= ")"; return [$where, $params]; } /** * Return SQL conditions for determining whether a user can see a group's members. * * @param string $groupsalias The SQL alias being used for the groups table. * @param string $groupsmembersalias The SQL alias being used for the groups_members table. * @param string $useralias The SQL alias being used for the user table. * @param string $paramprefix Prefix for the parameter names. * @return array [$where, $params] */ public static function sql_member_visibility_where( string $groupsalias = 'g', string $groupsmembersalias = 'gm', string $useralias = 'u', string $paramprefix = '', ): array { global $USER; list($memberssql, $membersparams) = self::sql_members_visibility_condition($groupsalias, $groupsmembersalias, $paramprefix); $where = "( {$groupsalias}.visibility = :{$paramprefix}all OR ($memberssql) OR ({$groupsalias}.visibility = :{$paramprefix}own AND {$useralias}.id = :{$paramprefix}currentuser2) )"; $params = [ "{$paramprefix}all" => GROUPS_VISIBILITY_ALL, "{$paramprefix}own" => GROUPS_VISIBILITY_OWN, "{$paramprefix}currentuser2" => $USER->id, ]; $params = array_merge($params, $membersparams); return [$where, $params]; } /** * Return a condition to check if a user can view a group because it has MEMBERS visibility and they are a member. * * @param string $groupsalias The SQL alias being used for the groups table. * @param string $groupsmembersalias The SQL alias being used for the groups_members table. * @param string $paramprefix Prefix for the parameter names. * @return array [$sql, $params] */ protected static function sql_members_visibility_condition( string $groupsalias = 'g', string $groupsmembersalias = 'gm', string $paramprefix = '', ): array { global $USER; $sql = "{$groupsalias}.visibility = :{$paramprefix}members AND ( SELECT gm2.id FROM {groups_members} gm2 WHERE gm2.groupid = {$groupsmembersalias}.groupid AND gm2.userid = :{$paramprefix}currentuser ) IS NOT NULL"; $params = [ "{$paramprefix}members" => GROUPS_VISIBILITY_MEMBERS, "{$paramprefix}currentuser" => $USER->id ]; return [$sql, $params]; } } classes/reportbuilder/datasource/groups.php 0000644 00000012075 15215711721 0015254 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\datasource; use core_group\reportbuilder\local\entities\{grouping, group, group_member}; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\{course, user}; use core_reportbuilder\local\helpers\database; /** * Groups datasource * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class groups extends datasource { /** * Return user friendly name of the datasource * * @return string */ public static function get_name(): string { return get_string('groups', 'core_group'); } /** * Initialise report */ protected function initialise(): void { $courseentity = new course(); $coursealias = $courseentity->get_table_alias('course'); $this->set_main_table('course', $coursealias); $this->add_entity($courseentity); $paramsiteid = database::generate_param_name(); $this->add_base_condition_sql("{$coursealias}.id != :{$paramsiteid}", [$paramsiteid => SITEID]); // Re-use the context table alias/join from the course entity in subsequent entities. $contextalias = $courseentity->get_table_alias('context'); $this->add_join($courseentity->get_context_join()); // Group entity. $groupentity = (new group()) ->set_table_alias('context', $contextalias); $groupsalias = $groupentity->get_table_alias('groups'); $this->add_entity($groupentity ->add_join("LEFT JOIN {groups} {$groupsalias} ON {$groupsalias}.courseid = {$coursealias}.id")); // Grouping entity. $groupingentity = (new grouping()) ->set_table_alias('context', $contextalias); $groupingsalias = $groupingentity->get_table_alias('groupings'); // Sub-select for all groupings groups. $groupinginnerselect = " SELECT gr.*, grg.groupid FROM {groupings} gr JOIN {groupings_groups} grg ON grg.groupingid = gr.id"; $this->add_entity($groupingentity ->add_joins($groupentity->get_joins()) ->add_join("LEFT JOIN ({$groupinginnerselect}) {$groupingsalias} ON {$groupingsalias}.courseid = {$coursealias}.id AND {$groupingsalias}.groupid = {$groupsalias}.id")); // Group member entity. $groupmemberentity = new group_member(); $groupsmembersalias = $groupmemberentity->get_table_alias('groups_members'); $this->add_entity($groupmemberentity ->add_joins($groupentity->get_joins()) ->add_join("LEFT JOIN {groups_members} {$groupsmembersalias} ON {$groupsmembersalias}.groupid = {$groupsalias}.id")); // User entity. $userentity = new user(); $useralias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_joins($groupmemberentity->get_joins()) ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$groupsmembersalias}.userid")); // Add all elements from entities to be available in custom reports. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report as part of default setup * * @return string[] */ public function get_default_columns(): array { return [ 'course:coursefullnamewithlink', 'group:name', 'user:fullname', ]; } /** * Return the column sorting that will be added to the report upon creation * * @return int[] */ public function get_default_column_sorting(): array { return [ 'course:coursefullnamewithlink' => SORT_ASC, 'group:name' => SORT_ASC, 'user:fullname' => SORT_ASC, ]; } /** * Return the filters that will be added to the report as part of default setup * * @return string[] */ public function get_default_filters(): array { return [ 'course:fullname', 'group:name', ]; } /** * Return the conditions that will be added to the report as part of default setup * * @return string[] */ public function get_default_conditions(): array { return [ 'course:fullname', 'group:name', ]; } } classes/reportbuilder/local/entities/group_member.php 0000644 00000007336 15215711721 0017210 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use core_reportbuilder\local\filters\date; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Group member entity * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_member extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'groups_members', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('groupmember', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $groupsmembersalias = $this->get_table_alias('groups_members'); // Time added column. $columns[] = (new column( 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsmembersalias}.timeadded") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Component column. $columns[] = (new column( 'component', new lang_string('plugin', 'core'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsmembersalias}.component") ->set_is_sortable(true); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupsmembersalias = $this->get_table_alias('groups_members'); // Time added filter. $filters[] = (new filter( date::class, 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name(), "{$groupsmembersalias}.timeadded" )) ->add_joins($this->get_joins()); return $filters; } } classes/reportbuilder/local/entities/grouping.php 0000644 00000017021 15215711721 0016347 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use context_course; use context_helper; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, text}; use core_reportbuilder\local\helpers\{custom_fields, format}; use core_reportbuilder\local\report\{column, filter}; /** * Grouping entity * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grouping extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'context', 'groupings', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('grouping', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $groupingsalias = $this->get_table_alias('groupings'); $customfields = (new custom_fields( "{$groupingsalias}.id", $this->get_entity_name(), 'core_group', 'grouping', )) ->add_joins($this->get_joins()); $columns = array_merge($this->get_all_columns(), $customfields->get_columns()); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = array_merge($this->get_all_filters(), $customfields->get_filters()); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $contextalias = $this->get_table_alias('context'); $groupingsalias = $this->get_table_alias('groupings'); // Name column. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupingsalias}.name, {$groupingsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function($name, stdClass $grouping): string { if ($name === null) { return ''; } context_helper::preload_from_record($grouping); $context = context_course::instance($grouping->courseid); return format_string($grouping->name, true, ['context' => $context]); }); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumber'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupingsalias}.idnumber") ->set_is_sortable(true); // Description column. $descriptionfieldsql = "{$groupingsalias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description') ->add_fields("{$groupingsalias}.descriptionformat, {$groupingsalias}.id, {$groupingsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(false) ->set_callback(static function(?string $description, stdClass $grouping): string { global $CFG; if ($description === null) { return ''; } require_once("{$CFG->libdir}/filelib.php"); context_helper::preload_from_record($grouping); $context = context_course::instance($grouping->courseid); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'grouping', 'description', $grouping->id); return format_text($description, $grouping->descriptionformat, ['context' => $context]); }); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupingsalias}.timecreated") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupingsalias}.timemodified") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupingsalias = $this->get_table_alias('groupings'); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$groupingsalias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumber'), $this->get_entity_name(), "{$groupingsalias}.idnumber" )) ->add_joins($this->get_joins()); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$groupingsalias}.timecreated" )) ->add_joins($this->get_joins()); return $filters; } } classes/reportbuilder/local/entities/group.php 0000644 00000026304 15215711721 0015655 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use context_course; use context_helper; use html_writer; use lang_string; use moodle_url; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{boolean_select, date, select, text}; use core_reportbuilder\local\helpers\{custom_fields, format}; use core_reportbuilder\local\report\{column, filter}; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->libdir}/grouplib.php"); /** * Group entity * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'context', 'groups', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('group', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $groupsalias = $this->get_table_alias('groups'); $customfields = (new custom_fields( "{$groupsalias}.id", $this->get_entity_name(), 'core_group', 'group', )) ->add_joins($this->get_joins()); $columns = array_merge($this->get_all_columns(), $customfields->get_columns()); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = array_merge($this->get_all_filters(), $customfields->get_filters()); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $contextalias = $this->get_table_alias('context'); $groupsalias = $this->get_table_alias('groups'); // Name column. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.name, {$groupsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function($name, stdClass $group): string { if ($name === null) { return ''; } context_helper::preload_from_record($group); $context = context_course::instance($group->courseid); return format_string($group->name, true, ['context' => $context]); }); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumber'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.idnumber") ->set_is_sortable(true); // Description column. $descriptionfieldsql = "{$groupsalias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description') ->add_fields("{$groupsalias}.descriptionformat, {$groupsalias}.id, {$groupsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(false) ->set_callback(static function(?string $description, stdClass $group): string { global $CFG; if ($description === null) { return ''; } require_once("{$CFG->libdir}/filelib.php"); context_helper::preload_from_record($group); $context = context_course::instance($group->courseid); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'group', 'description', $group->id); return format_text($description, $group->descriptionformat, ['context' => $context]); }); // Enrolment key column. $columns[] = (new column( 'enrolmentkey', new lang_string('enrolmentkey', 'core_group'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.enrolmentkey") ->set_is_sortable(true); // Visibility column. $columns[] = (new column( 'visibility', new lang_string('visibilityshort', 'core_group'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_fields("{$groupsalias}.visibility") ->set_is_sortable(true) ->set_callback(static function(?string $visibility): string { if ($visibility === null) { return ''; } $options = [ GROUPS_VISIBILITY_ALL => new lang_string('visibilityall', 'core_group'), GROUPS_VISIBILITY_MEMBERS => new lang_string('visibilitymembers', 'core_group'), GROUPS_VISIBILITY_OWN => new lang_string('visibilityown', 'core_group'), GROUPS_VISIBILITY_NONE => new lang_string('visibilitynone', 'core_group'), ]; return (string) ($options[(int) $visibility] ?? $visibility); }); // Participation column. $columns[] = (new column( 'participation', new lang_string('participationshort', 'core_group'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_fields("{$groupsalias}.participation") ->set_is_sortable(true) ->set_callback([format::class, 'boolean_as_text']); // Picture column. $columns[] = (new column( 'picture', new lang_string('picture'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_fields("{$groupsalias}.picture, {$groupsalias}.id, {$contextalias}.id AS contextid") ->set_is_sortable(false) ->set_callback(static function($value, stdClass $group): string { if (empty($group->picture)) { return ''; } $pictureurl = moodle_url::make_pluginfile_url($group->contextid, 'group', 'icon', $group->id, '/', 'f2'); $pictureurl->param('rev', $group->picture); return html_writer::img($pictureurl, ''); }); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsalias}.timecreated") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsalias}.timemodified") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupsalias = $this->get_table_alias('groups'); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$groupsalias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumber'), $this->get_entity_name(), "{$groupsalias}.idnumber" )) ->add_joins($this->get_joins()); // Visibility filter. $filters[] = (new filter( select::class, 'visibility', new lang_string('visibilityshort', 'core_group'), $this->get_entity_name(), "{$groupsalias}.visibility" )) ->add_joins($this->get_joins()) ->set_options([ GROUPS_VISIBILITY_ALL => new lang_string('visibilityall', 'core_group'), GROUPS_VISIBILITY_MEMBERS => new lang_string('visibilitymembers', 'core_group'), GROUPS_VISIBILITY_OWN => new lang_string('visibilityown', 'core_group'), GROUPS_VISIBILITY_NONE => new lang_string('visibilitynone', 'core_group'), ]); // Participation filter. $filters[] = (new filter( boolean_select::class, 'participation', new lang_string('participationshort', 'core_group'), $this->get_entity_name(), "{$groupsalias}.participation" )) ->add_joins($this->get_joins()); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$groupsalias}.timecreated" )) ->add_joins($this->get_joins()); return $filters; } } externallib.php 0000644 00000210237 15215711721 0007575 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. use core_external\external_api; use core_external\external_format_value; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\util; use core_group\visibility; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/group/lib.php'); /** * Group external functions * * @package core_group * @category external * @copyright 2011 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.2 */ class core_group_external extends external_api { /** * Validate visibility. * * @param int $visibility Visibility string, must one of the visibility class constants. * @throws invalid_parameter_exception if visibility is not an allowed value. */ protected static function validate_visibility(int $visibility): void { $allowed = [ GROUPS_VISIBILITY_ALL, GROUPS_VISIBILITY_MEMBERS, GROUPS_VISIBILITY_OWN, GROUPS_VISIBILITY_NONE, ]; if (!array_key_exists($visibility, $allowed)) { throw new invalid_parameter_exception('Invalid group visibility provided. Must be one of ' . join(',', $allowed)); } } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function create_groups_parameters() { return new external_function_parameters( array( 'groups' => new external_multiple_structure( new external_single_structure( array( 'courseid' => new external_value(PARAM_INT, 'id of course'), 'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'), 'description' => new external_value(PARAM_RAW, 'group description text'), 'descriptionformat' => new external_format_value('description', VALUE_DEFAULT), 'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase', VALUE_OPTIONAL), 'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL), 'visibility' => new external_value(PARAM_INT, 'group visibility mode. 0 = Visible to all. 1 = Visible to members. ' . '2 = See own membership. 3 = Membership is hidden. default: 0', VALUE_DEFAULT, 0), 'participation' => new external_value(PARAM_BOOL, 'activity participation enabled? Only for "all" and "members" visibility. Default true.', VALUE_DEFAULT, true), 'customfields' => self::build_custom_fields_parameters_structure(), ) ), 'List of group object. A group has a courseid, a name, a description and an enrolment key.' ) ) ); } /** * Create groups * * @param array $groups array of group description arrays (with keys groupname and courseid) * @return array of newly created groups * @since Moodle 2.2 */ public static function create_groups($groups) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); $params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups)); $transaction = $DB->start_delegated_transaction(); $groups = array(); foreach ($params['groups'] as $group) { $group = (object)$group; if (trim($group->name) == '') { throw new invalid_parameter_exception('Invalid group name'); } if ($DB->get_record('groups', array('courseid'=>$group->courseid, 'name'=>$group->name))) { throw new invalid_parameter_exception('Group with the same name already exists in the course'); } // now security checks $context = context_course::instance($group->courseid, IGNORE_MISSING); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $group->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); // Validate format. $group->descriptionformat = util::validate_format($group->descriptionformat); // Validate visibility. self::validate_visibility($group->visibility); // Custom fields. if (!empty($group->customfields)) { foreach ($group->customfields as $field) { $fieldname = self::build_custom_field_name($field['shortname']); $group->{$fieldname} = $field['value']; } } // finally create the group $group->id = groups_create_group($group, false); if (!isset($group->enrolmentkey)) { $group->enrolmentkey = ''; } if (!isset($group->idnumber)) { $group->idnumber = ''; } $groups[] = (array)$group; } $transaction->allow_commit(); return $groups; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.2 */ public static function create_groups_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'group record id'), 'courseid' => new external_value(PARAM_INT, 'id of course'), 'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'), 'description' => new external_value(PARAM_RAW, 'group description text'), 'descriptionformat' => new external_format_value('description'), 'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'), 'idnumber' => new external_value(PARAM_RAW, 'id number'), 'visibility' => new external_value(PARAM_INT, 'group visibility mode. 0 = Visible to all. 1 = Visible to members. 2 = See own membership. ' . '3 = Membership is hidden.'), 'participation' => new external_value(PARAM_BOOL, 'participation mode'), 'customfields' => self::build_custom_fields_parameters_structure(), ) ), 'List of group object. A group has an id, a courseid, a name, a description and an enrolment key.' ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function get_groups_parameters() { return new external_function_parameters( array( 'groupids' => new external_multiple_structure(new external_value(PARAM_INT, 'Group ID') ,'List of group id. A group id is an integer.'), ) ); } /** * Get groups definition specified by ids * * @param array $groupids arrays of group ids * @return array of group objects (id, courseid, name, enrolmentkey) * @since Moodle 2.2 */ public static function get_groups($groupids) { $params = self::validate_parameters(self::get_groups_parameters(), array('groupids'=>$groupids)); $groups = array(); $customfieldsdata = get_group_custom_fields_data($groupids); foreach ($params['groupids'] as $groupid) { // validate params $group = groups_get_group($groupid, 'id, courseid, name, idnumber, description, descriptionformat, enrolmentkey, ' . 'visibility, participation', MUST_EXIST); // now security checks $context = context_course::instance($group->courseid, IGNORE_MISSING); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $group->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); $group->name = \core_external\util::format_string($group->name, $context); [$group->description, $group->descriptionformat] = \core_external\util::format_text($group->description, $group->descriptionformat, $context, 'group', 'description', $group->id); $group->customfields = $customfieldsdata[$group->id] ?? []; $groups[] = (array)$group; } return $groups; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.2 */ public static function get_groups_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'group record id'), 'courseid' => new external_value(PARAM_INT, 'id of course'), 'name' => new external_value(PARAM_TEXT, 'group name'), 'description' => new external_value(PARAM_RAW, 'group description text'), 'descriptionformat' => new external_format_value('description'), 'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'), 'idnumber' => new external_value(PARAM_RAW, 'id number'), 'visibility' => new external_value(PARAM_INT, 'group visibility mode. 0 = Visible to all. 1 = Visible to members. 2 = See own membership. ' . '3 = Membership is hidden.'), 'participation' => new external_value(PARAM_BOOL, 'participation mode'), 'customfields' => self::build_custom_fields_returns_structure(), ) ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function get_course_groups_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'id of course'), ) ); } /** * Get all groups in the specified course * * @param int $courseid id of course * @return array of group objects (id, courseid, name, enrolmentkey) * @since Moodle 2.2 */ public static function get_course_groups($courseid) { $params = self::validate_parameters(self::get_course_groups_parameters(), array('courseid'=>$courseid)); // now security checks $context = context_course::instance($params['courseid'], IGNORE_MISSING); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $params['courseid']; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); $gs = groups_get_all_groups($params['courseid'], 0, 0, 'g.id, g.courseid, g.name, g.idnumber, g.description, g.descriptionformat, g.enrolmentkey, ' . 'g.visibility, g.participation'); $groups = array(); foreach ($gs as $group) { $group->name = \core_external\util::format_string($group->name, $context); [$group->description, $group->descriptionformat] = \core_external\util::format_text($group->description, $group->descriptionformat, $context, 'group', 'description', $group->id); $groups[] = (array)$group; } return $groups; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.2 */ public static function get_course_groups_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'group record id'), 'courseid' => new external_value(PARAM_INT, 'id of course'), 'name' => new external_value(PARAM_TEXT, 'group name'), 'description' => new external_value(PARAM_RAW, 'group description text'), 'descriptionformat' => new external_format_value('description'), 'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'), 'idnumber' => new external_value(PARAM_RAW, 'id number'), 'visibility' => new external_value(PARAM_INT, 'group visibility mode. 0 = Visible to all. 1 = Visible to members. 2 = See own membership. ' . '3 = Membership is hidden.'), 'participation' => new external_value(PARAM_BOOL, 'participation mode'), ) ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function delete_groups_parameters() { return new external_function_parameters( array( 'groupids' => new external_multiple_structure(new external_value(PARAM_INT, 'Group ID')), ) ); } /** * Delete groups * * @param array $groupids array of group ids * @since Moodle 2.2 */ public static function delete_groups($groupids) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); $params = self::validate_parameters(self::delete_groups_parameters(), array('groupids'=>$groupids)); $transaction = $DB->start_delegated_transaction(); foreach ($params['groupids'] as $groupid) { // validate params $groupid = validate_param($groupid, PARAM_INT); if (!$group = groups_get_group($groupid, '*', IGNORE_MISSING)) { // silently ignore attempts to delete nonexisting groups continue; } // now security checks $context = context_course::instance($group->courseid, IGNORE_MISSING); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $group->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); groups_delete_group($group); } $transaction->allow_commit(); } /** * Returns description of method result value * * @return null * @since Moodle 2.2 */ public static function delete_groups_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function get_group_members_parameters() { return new external_function_parameters( array( 'groupids' => new external_multiple_structure(new external_value(PARAM_INT, 'Group ID')), ) ); } /** * Return all members for a group * * @param array $groupids array of group ids * @return array with group id keys containing arrays of user ids * @since Moodle 2.2 */ public static function get_group_members($groupids) { $members = array(); $params = self::validate_parameters(self::get_group_members_parameters(), array('groupids'=>$groupids)); foreach ($params['groupids'] as $groupid) { // validate params $group = groups_get_group($groupid, 'id, courseid, name, enrolmentkey', MUST_EXIST); // now security checks $context = context_course::instance($group->courseid, IGNORE_MISSING); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $group->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); $groupmembers = groups_get_members($group->id, 'u.id', 'lastname ASC, firstname ASC'); $members[] = array('groupid'=>$groupid, 'userids'=>array_keys($groupmembers)); } return $members; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.2 */ public static function get_group_members_returns() { return new external_multiple_structure( new external_single_structure( array( 'groupid' => new external_value(PARAM_INT, 'group record id'), 'userids' => new external_multiple_structure(new external_value(PARAM_INT, 'user id')), ) ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function add_group_members_parameters() { return new external_function_parameters( array( 'members'=> new external_multiple_structure( new external_single_structure( array( 'groupid' => new external_value(PARAM_INT, 'group record id'), 'userid' => new external_value(PARAM_INT, 'user id'), ) ) ) ) ); } /** * Add group members * * @param array $members of arrays with keys userid, groupid * @since Moodle 2.2 */ public static function add_group_members($members) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); $params = self::validate_parameters(self::add_group_members_parameters(), array('members'=>$members)); $transaction = $DB->start_delegated_transaction(); foreach ($params['members'] as $member) { // validate params $groupid = $member['groupid']; $userid = $member['userid']; $group = groups_get_group($groupid, '*', MUST_EXIST); $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST); // now security checks $context = context_course::instance($group->courseid, IGNORE_MISSING); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $group->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); // now make sure user is enrolled in course - this is mandatory requirement, // unfortunately this is slow if (!is_enrolled($context, $userid)) { throw new invalid_parameter_exception('Only enrolled users may be members of groups'); } groups_add_member($group, $user); } $transaction->allow_commit(); } /** * Returns description of method result value * * @return null * @since Moodle 2.2 */ public static function add_group_members_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function delete_group_members_parameters() { return new external_function_parameters( array( 'members'=> new external_multiple_structure( new external_single_structure( array( 'groupid' => new external_value(PARAM_INT, 'group record id'), 'userid' => new external_value(PARAM_INT, 'user id'), ) ) ) ) ); } /** * Delete group members * * @param array $members of arrays with keys userid, groupid * @since Moodle 2.2 */ public static function delete_group_members($members) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); $params = self::validate_parameters(self::delete_group_members_parameters(), array('members'=>$members)); $transaction = $DB->start_delegated_transaction(); foreach ($params['members'] as $member) { // validate params $groupid = $member['groupid']; $userid = $member['userid']; $group = groups_get_group($groupid, '*', MUST_EXIST); $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST); // now security checks $context = context_course::instance($group->courseid, IGNORE_MISSING); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $group->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); if (!groups_remove_member_allowed($group, $user)) { $fullname = fullname($user, has_capability('moodle/site:viewfullnames', $context)); throw new moodle_exception('errorremovenotpermitted', 'group', '', $fullname); } groups_remove_member($group, $user); } $transaction->allow_commit(); } /** * Returns description of method result value * * @return null * @since Moodle 2.2 */ public static function delete_group_members_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function create_groupings_parameters() { return new external_function_parameters( array( 'groupings' => new external_multiple_structure( new external_single_structure( array( 'courseid' => new external_value(PARAM_INT, 'id of course'), 'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'), 'description' => new external_value(PARAM_RAW, 'grouping description text'), 'descriptionformat' => new external_format_value('description', VALUE_DEFAULT), 'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL), 'customfields' => self::build_custom_fields_parameters_structure(), ) ), 'List of grouping object. A grouping has a courseid, a name and a description.' ) ) ); } /** * Create groupings * * @param array $groupings array of grouping description arrays (with keys groupname and courseid) * @return array of newly created groupings * @since Moodle 2.3 */ public static function create_groupings($groupings) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); $params = self::validate_parameters(self::create_groupings_parameters(), array('groupings'=>$groupings)); $transaction = $DB->start_delegated_transaction(); $groupings = array(); foreach ($params['groupings'] as $grouping) { $grouping = (object)$grouping; if (trim($grouping->name) == '') { throw new invalid_parameter_exception('Invalid grouping name'); } if ($DB->count_records('groupings', array('courseid'=>$grouping->courseid, 'name'=>$grouping->name))) { throw new invalid_parameter_exception('Grouping with the same name already exists in the course'); } // Now security checks . $context = context_course::instance($grouping->courseid); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $grouping->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); $grouping->descriptionformat = util::validate_format($grouping->descriptionformat); // Custom fields. if (!empty($grouping->customfields)) { foreach ($grouping->customfields as $field) { $fieldname = self::build_custom_field_name($field['shortname']); $grouping->{$fieldname} = $field['value']; } } // Finally create the grouping. $grouping->id = groups_create_grouping($grouping); $groupings[] = (array)$grouping; } $transaction->allow_commit(); return $groupings; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.3 */ public static function create_groupings_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'grouping record id'), 'courseid' => new external_value(PARAM_INT, 'id of course'), 'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'), 'description' => new external_value(PARAM_RAW, 'grouping description text'), 'descriptionformat' => new external_format_value('description'), 'idnumber' => new external_value(PARAM_RAW, 'id number'), 'customfields' => self::build_custom_fields_parameters_structure(), ) ), 'List of grouping object. A grouping has an id, a courseid, a name and a description.' ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function update_groupings_parameters() { return new external_function_parameters( array( 'groupings' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'id of grouping'), 'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'), 'description' => new external_value(PARAM_RAW, 'grouping description text'), 'descriptionformat' => new external_format_value('description', VALUE_DEFAULT), 'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL), 'customfields' => self::build_custom_fields_parameters_structure(), ) ), 'List of grouping object. A grouping has a courseid, a name and a description.' ) ) ); } /** * Update groupings * * @param array $groupings array of grouping description arrays (with keys groupname and courseid) * @return array of newly updated groupings * @since Moodle 2.3 */ public static function update_groupings($groupings) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); $params = self::validate_parameters(self::update_groupings_parameters(), array('groupings'=>$groupings)); $transaction = $DB->start_delegated_transaction(); foreach ($params['groupings'] as $grouping) { $grouping = (object)$grouping; if (trim($grouping->name) == '') { throw new invalid_parameter_exception('Invalid grouping name'); } if (! $currentgrouping = $DB->get_record('groupings', array('id'=>$grouping->id))) { throw new invalid_parameter_exception("Grouping $grouping->id does not exist in the course"); } // Check if the new modified grouping name already exists in the course. if ($grouping->name != $currentgrouping->name and $DB->count_records('groupings', array('courseid'=>$currentgrouping->courseid, 'name'=>$grouping->name))) { throw new invalid_parameter_exception('A different grouping with the same name already exists in the course'); } $grouping->courseid = $currentgrouping->courseid; // Now security checks. $context = context_course::instance($grouping->courseid); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $grouping->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); // We must force allways FORMAT_HTML. $grouping->descriptionformat = util::validate_format($grouping->descriptionformat); // Custom fields. if (!empty($grouping->customfields)) { foreach ($grouping->customfields as $field) { $fieldname = self::build_custom_field_name($field['shortname']); $grouping->{$fieldname} = $field['value']; } } // Finally update the grouping. groups_update_grouping($grouping); } $transaction->allow_commit(); return null; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.3 */ public static function update_groupings_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function get_groupings_parameters() { return new external_function_parameters( array( 'groupingids' => new external_multiple_structure(new external_value(PARAM_INT, 'grouping ID') , 'List of grouping id. A grouping id is an integer.'), 'returngroups' => new external_value(PARAM_BOOL, 'return associated groups', VALUE_DEFAULT, 0) ) ); } /** * Get groupings definition specified by ids * * @param array $groupingids arrays of grouping ids * @param boolean $returngroups return the associated groups if true. The default is false. * @return array of grouping objects (id, courseid, name) * @since Moodle 2.3 */ public static function get_groupings($groupingids, $returngroups = false) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); require_once("$CFG->libdir/filelib.php"); $params = self::validate_parameters(self::get_groupings_parameters(), array('groupingids' => $groupingids, 'returngroups' => $returngroups)); $groupings = array(); $groupingcustomfieldsdata = get_grouping_custom_fields_data($groupingids); foreach ($params['groupingids'] as $groupingid) { // Validate params. $grouping = groups_get_grouping($groupingid, '*', MUST_EXIST); // Now security checks. $context = context_course::instance($grouping->courseid); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $grouping->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); list($grouping->description, $grouping->descriptionformat) = \core_external\util::format_text($grouping->description, $grouping->descriptionformat, $context, 'grouping', 'description', $grouping->id); $grouping->customfields = $groupingcustomfieldsdata[$grouping->id] ?? []; $groupingarray = (array)$grouping; if ($params['returngroups']) { $grouprecords = $DB->get_records_sql("SELECT * FROM {groups} g INNER JOIN {groupings_groups} gg ". "ON g.id = gg.groupid WHERE gg.groupingid = ? ". "ORDER BY groupid", array($groupingid)); if ($grouprecords) { $groups = array(); $groupids = []; foreach ($grouprecords as $grouprecord) { list($grouprecord->description, $grouprecord->descriptionformat) = \core_external\util::format_text($grouprecord->description, $grouprecord->descriptionformat, $context, 'group', 'description', $grouprecord->groupid); $groups[] = array('id' => $grouprecord->groupid, 'name' => $grouprecord->name, 'idnumber' => $grouprecord->idnumber, 'description' => $grouprecord->description, 'descriptionformat' => $grouprecord->descriptionformat, 'enrolmentkey' => $grouprecord->enrolmentkey, 'courseid' => $grouprecord->courseid ); $groupids[] = $grouprecord->groupid; } $groupcustomfieldsdata = get_group_custom_fields_data($groupids); foreach ($groups as $i => $group) { $groups[$i]['customfields'] = $groupcustomfieldsdata[$group['id']] ?? []; } $groupingarray['groups'] = $groups; } } $groupings[] = $groupingarray; } return $groupings; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.3 */ public static function get_groupings_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'grouping record id'), 'courseid' => new external_value(PARAM_INT, 'id of course'), 'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'), 'description' => new external_value(PARAM_RAW, 'grouping description text'), 'descriptionformat' => new external_format_value('description'), 'idnumber' => new external_value(PARAM_RAW, 'id number'), 'customfields' => self::build_custom_fields_returns_structure(), 'groups' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'group record id'), 'courseid' => new external_value(PARAM_INT, 'id of course'), 'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'), 'description' => new external_value(PARAM_RAW, 'group description text'), 'descriptionformat' => new external_format_value('description'), 'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'), 'idnumber' => new external_value(PARAM_RAW, 'id number'), 'customfields' => self::build_custom_fields_returns_structure(), ) ), 'optional groups', VALUE_OPTIONAL) ) ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function get_course_groupings_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'id of course'), ) ); } /** * Get all groupings in the specified course * * @param int $courseid id of course * @return array of grouping objects (id, courseid, name, enrolmentkey) * @since Moodle 2.3 */ public static function get_course_groupings($courseid) { global $CFG; require_once("$CFG->dirroot/group/lib.php"); require_once("$CFG->libdir/filelib.php"); $params = self::validate_parameters(self::get_course_groupings_parameters(), array('courseid'=>$courseid)); // Now security checks. $context = context_course::instance($params['courseid']); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $params['courseid']; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); $gs = groups_get_all_groupings($params['courseid']); $groupings = array(); foreach ($gs as $grouping) { list($grouping->description, $grouping->descriptionformat) = \core_external\util::format_text($grouping->description, $grouping->descriptionformat, $context, 'grouping', 'description', $grouping->id); $groupings[] = (array)$grouping; } return $groupings; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.3 */ public static function get_course_groupings_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'grouping record id'), 'courseid' => new external_value(PARAM_INT, 'id of course'), 'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'), 'description' => new external_value(PARAM_RAW, 'grouping description text'), 'descriptionformat' => new external_format_value('description'), 'idnumber' => new external_value(PARAM_RAW, 'id number') ) ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function delete_groupings_parameters() { return new external_function_parameters( array( 'groupingids' => new external_multiple_structure(new external_value(PARAM_INT, 'grouping ID')), ) ); } /** * Delete groupings * * @param array $groupingids array of grouping ids * @return void * @since Moodle 2.3 */ public static function delete_groupings($groupingids) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); $params = self::validate_parameters(self::delete_groupings_parameters(), array('groupingids'=>$groupingids)); $transaction = $DB->start_delegated_transaction(); foreach ($params['groupingids'] as $groupingid) { if (!$grouping = groups_get_grouping($groupingid)) { // Silently ignore attempts to delete nonexisting groupings. continue; } // Now security checks. $context = context_course::instance($grouping->courseid); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $grouping->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); groups_delete_grouping($grouping); } $transaction->allow_commit(); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.3 */ public static function delete_groupings_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function assign_grouping_parameters() { return new external_function_parameters( array( 'assignments'=> new external_multiple_structure( new external_single_structure( array( 'groupingid' => new external_value(PARAM_INT, 'grouping record id'), 'groupid' => new external_value(PARAM_INT, 'group record id'), ) ) ) ) ); } /** * Assign a group to a grouping * * @param array $assignments of arrays with keys groupid, groupingid * @return void * @since Moodle 2.3 */ public static function assign_grouping($assignments) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); $params = self::validate_parameters(self::assign_grouping_parameters(), array('assignments'=>$assignments)); $transaction = $DB->start_delegated_transaction(); foreach ($params['assignments'] as $assignment) { // Validate params. $groupingid = $assignment['groupingid']; $groupid = $assignment['groupid']; $grouping = groups_get_grouping($groupingid, 'id, courseid', MUST_EXIST); $group = groups_get_group($groupid, 'id, courseid', MUST_EXIST); if ($DB->record_exists('groupings_groups', array('groupingid'=>$groupingid, 'groupid'=>$groupid))) { // Continue silently if the group is yet assigned to the grouping. continue; } // Now security checks. $context = context_course::instance($grouping->courseid); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $group->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); groups_assign_grouping($groupingid, $groupid); } $transaction->allow_commit(); } /** * Returns description of method result value * * @return null * @since Moodle 2.3 */ public static function assign_grouping_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function unassign_grouping_parameters() { return new external_function_parameters( array( 'unassignments'=> new external_multiple_structure( new external_single_structure( array( 'groupingid' => new external_value(PARAM_INT, 'grouping record id'), 'groupid' => new external_value(PARAM_INT, 'group record id'), ) ) ) ) ); } /** * Unassign a group from a grouping * * @param array $unassignments of arrays with keys groupid, groupingid * @return void * @since Moodle 2.3 */ public static function unassign_grouping($unassignments) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); $params = self::validate_parameters(self::unassign_grouping_parameters(), array('unassignments'=>$unassignments)); $transaction = $DB->start_delegated_transaction(); foreach ($params['unassignments'] as $unassignment) { // Validate params. $groupingid = $unassignment['groupingid']; $groupid = $unassignment['groupid']; $grouping = groups_get_grouping($groupingid, 'id, courseid', MUST_EXIST); $group = groups_get_group($groupid, 'id, courseid', MUST_EXIST); if (!$DB->record_exists('groupings_groups', array('groupingid'=>$groupingid, 'groupid'=>$groupid))) { // Continue silently if the group is not assigned to the grouping. continue; } // Now security checks. $context = context_course::instance($grouping->courseid); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $group->courseid; throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); groups_unassign_grouping($groupingid, $groupid); } $transaction->allow_commit(); } /** * Returns description of method result value * * @return null * @since Moodle 2.3 */ public static function unassign_grouping_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 */ public static function get_course_user_groups_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'Id of course (empty or 0 for all the courses where the user is enrolled).', VALUE_DEFAULT, 0), 'userid' => new external_value(PARAM_INT, 'Id of user (empty or 0 for current user).', VALUE_DEFAULT, 0), 'groupingid' => new external_value(PARAM_INT, 'returns only groups in the specified grouping', VALUE_DEFAULT, 0) ) ); } /** * Get all groups in the specified course for the specified user. * * @throws moodle_exception * @param int $courseid id of course. * @param int $userid id of user. * @param int $groupingid optional returns only groups in the specified grouping. * @return array of group objects (id, name, description, format) and possible warnings. * @since Moodle 2.9 */ public static function get_course_user_groups($courseid = 0, $userid = 0, $groupingid = 0) { global $USER; // Warnings array, it can be empty at the end but is mandatory. $warnings = array(); $params = array( 'courseid' => $courseid, 'userid' => $userid, 'groupingid' => $groupingid ); $params = self::validate_parameters(self::get_course_user_groups_parameters(), $params); $courseid = $params['courseid']; $userid = $params['userid']; $groupingid = $params['groupingid']; // Validate user. if (empty($userid)) { $userid = $USER->id; } else { $user = core_user::get_user($userid, '*', MUST_EXIST); core_user::require_active_user($user); } // Get courses. if (empty($courseid)) { $courses = enrol_get_users_courses($userid, true); $checkenrolments = false; // No need to check enrolments here since they are my courses. } else { $courses = array($courseid => get_course($courseid)); $checkenrolments = true; } // Security checks. list($courses, $warnings) = util::validate_courses(array_keys($courses), $courses, true); $usergroups = array(); foreach ($courses as $course) { // Check if we have permissions for retrieve the information. if ($userid != $USER->id && !has_capability('moodle/course:managegroups', $course->context)) { $warnings[] = array( 'item' => 'course', 'itemid' => $course->id, 'warningcode' => 'cannotmanagegroups', 'message' => "User $USER->id cannot manage groups in course $course->id", ); continue; } // Check if the user being check is enrolled in the given course. if ($checkenrolments && !is_enrolled($course->context, $userid)) { // We return a warning because the function does not fail for not enrolled users. $warnings[] = array( 'item' => 'course', 'itemid' => $course->id, 'warningcode' => 'notenrolled', 'message' => "User $userid is not enrolled in course $course->id", ); } $groups = groups_get_all_groups($course->id, $userid, $groupingid, 'g.id, g.name, g.description, g.descriptionformat, g.idnumber'); foreach ($groups as $group) { $group->name = \core_external\util::format_string($group->name, $course->context); [$group->description, $group->descriptionformat] = \core_external\util::format_text($group->description, $group->descriptionformat, $course->context, 'group', 'description', $group->id); $group->courseid = $course->id; $usergroups[] = $group; } } $results = array( 'groups' => $usergroups, 'warnings' => $warnings ); return $results; } /** * Returns description of method result value. * * @return \core_external\external_description A single structure containing groups and possible warnings. * @since Moodle 2.9 */ public static function get_course_user_groups_returns() { return new external_single_structure( array( 'groups' => new external_multiple_structure(self::group_description()), 'warnings' => new external_warnings(), ) ); } /** * Create group return value description. * * @return external_single_structure The group description */ public static function group_description() { return new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'group record id'), 'name' => new external_value(PARAM_TEXT, 'group name'), 'description' => new external_value(PARAM_RAW, 'group description text'), 'descriptionformat' => new external_format_value('description'), 'idnumber' => new external_value(PARAM_RAW, 'id number'), 'courseid' => new external_value(PARAM_INT, 'course id', VALUE_OPTIONAL), ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function get_activity_allowed_groups_parameters() { return new external_function_parameters( array( 'cmid' => new external_value(PARAM_INT, 'course module id'), 'userid' => new external_value(PARAM_INT, 'id of user, empty for current user', VALUE_DEFAULT, 0) ) ); } /** * Gets a list of groups that the user is allowed to access within the specified activity. * * @throws moodle_exception * @param int $cmid course module id * @param int $userid id of user. * @return array of group objects (id, name, description, format) and possible warnings. * @since Moodle 3.0 */ public static function get_activity_allowed_groups($cmid, $userid = 0) { global $USER; // Warnings array, it can be empty at the end but is mandatory. $warnings = array(); $params = array( 'cmid' => $cmid, 'userid' => $userid ); $params = self::validate_parameters(self::get_activity_allowed_groups_parameters(), $params); $cmid = $params['cmid']; $userid = $params['userid']; $cm = get_coursemodule_from_id(null, $cmid, 0, false, MUST_EXIST); // Security checks. $context = context_module::instance($cm->id); $coursecontext = context_course::instance($cm->course); self::validate_context($context); if (empty($userid)) { $userid = $USER->id; } $user = core_user::get_user($userid, '*', MUST_EXIST); core_user::require_active_user($user); // Check if we have permissions for retrieve the information. if ($user->id != $USER->id) { if (!has_capability('moodle/course:managegroups', $context)) { throw new moodle_exception('accessdenied', 'admin'); } // Validate if the user is enrolled in the course. $course = get_course($cm->course); if (!can_access_course($course, $user, '', true)) { // We return a warning because the function does not fail for not enrolled users. $warning = array(); $warning['item'] = 'course'; $warning['itemid'] = $cm->course; $warning['warningcode'] = '1'; $warning['message'] = "User $user->id cannot access course $cm->course"; $warnings[] = $warning; } } $usergroups = array(); if (empty($warnings)) { $groups = groups_get_activity_allowed_groups($cm, $user->id); foreach ($groups as $group) { $group->name = \core_external\util::format_string($group->name, $coursecontext); [$group->description, $group->descriptionformat] = \core_external\util::format_text($group->description, $group->descriptionformat, $coursecontext, 'group', 'description', $group->id); $group->courseid = $cm->course; $usergroups[] = $group; } } $results = array( 'groups' => $usergroups, 'canaccessallgroups' => has_capability('moodle/site:accessallgroups', $context, $user), 'warnings' => $warnings ); return $results; } /** * Returns description of method result value. * * @return \core_external\external_description A single structure containing groups and possible warnings. * @since Moodle 3.0 */ public static function get_activity_allowed_groups_returns() { return new external_single_structure( array( 'groups' => new external_multiple_structure(self::group_description()), 'canaccessallgroups' => new external_value(PARAM_BOOL, 'Whether the user will be able to access all the activity groups.', VALUE_OPTIONAL), 'warnings' => new external_warnings(), ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function get_activity_groupmode_parameters() { return new external_function_parameters( array( 'cmid' => new external_value(PARAM_INT, 'course module id') ) ); } /** * Returns effective groupmode used in a given activity. * * @throws moodle_exception * @param int $cmid course module id. * @return array containing the group mode and possible warnings. * @since Moodle 3.0 * @throws moodle_exception */ public static function get_activity_groupmode($cmid) { global $USER; // Warnings array, it can be empty at the end but is mandatory. $warnings = array(); $params = array( 'cmid' => $cmid ); $params = self::validate_parameters(self::get_activity_groupmode_parameters(), $params); $cmid = $params['cmid']; $cm = get_coursemodule_from_id(null, $cmid, 0, false, MUST_EXIST); // Security checks. $context = context_module::instance($cm->id); self::validate_context($context); $groupmode = groups_get_activity_groupmode($cm); $results = array( 'groupmode' => $groupmode, 'warnings' => $warnings ); return $results; } /** * Returns description of method result value. * * @return \core_external\external_description * @since Moodle 3.0 */ public static function get_activity_groupmode_returns() { return new external_single_structure( array( 'groupmode' => new external_value(PARAM_INT, 'group mode: 0 for no groups, 1 for separate groups, 2 for visible groups'), 'warnings' => new external_warnings(), ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.6 */ public static function update_groups_parameters() { return new external_function_parameters( array( 'groups' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'ID of the group'), 'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'), 'description' => new external_value(PARAM_RAW, 'group description text', VALUE_OPTIONAL), 'descriptionformat' => new external_format_value('description', VALUE_DEFAULT), 'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase', VALUE_OPTIONAL), 'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL), 'visibility' => new external_value(PARAM_TEXT, 'group visibility mode. 0 = Visible to all. 1 = Visible to members. ' . '2 = See own membership. 3 = Membership is hidden.', VALUE_OPTIONAL), 'participation' => new external_value(PARAM_BOOL, 'activity participation enabled? Only for "all" and "members" visibility', VALUE_OPTIONAL), 'customfields' => self::build_custom_fields_parameters_structure(), ) ), 'List of group objects. A group is found by the id, then all other details provided will be updated.' ) ) ); } /** * Update groups * * @param array $groups * @return null * @since Moodle 3.6 */ public static function update_groups($groups) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); $params = self::validate_parameters(self::update_groups_parameters(), array('groups' => $groups)); $transaction = $DB->start_delegated_transaction(); foreach ($params['groups'] as $group) { $group = (object) $group; if (trim($group->name) == '') { throw new invalid_parameter_exception('Invalid group name'); } if (!$currentgroup = $DB->get_record('groups', array('id' => $group->id))) { throw new invalid_parameter_exception("Group $group->id does not exist"); } // Check if the modified group name already exists in the course. if ($group->name != $currentgroup->name and $DB->get_record('groups', array('courseid' => $currentgroup->courseid, 'name' => $group->name))) { throw new invalid_parameter_exception('A different group with the same name already exists in the course'); } if (isset($group->visibility) || isset($group->participation)) { $hasmembers = $DB->record_exists('groups_members', ['groupid' => $group->id]); if (isset($group->visibility)) { // Validate visibility. self::validate_visibility($group->visibility); if ($hasmembers && $group->visibility != $currentgroup->visibility) { throw new invalid_parameter_exception( 'The visibility of this group cannot be changed as it currently has members.'); } } else { $group->visibility = $currentgroup->visibility; } if (isset($group->participation) && $hasmembers && $group->participation != $currentgroup->participation) { throw new invalid_parameter_exception( 'The participation mode of this group cannot be changed as it currently has members.'); } } $group->courseid = $currentgroup->courseid; // Now security checks. $context = context_course::instance($group->courseid); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $group->courseid; throw new moodle_exception('errorcoursecontextnotvalid', 'webservice', '', $exceptionparam); } require_capability('moodle/course:managegroups', $context); if (!empty($group->description)) { $group->descriptionformat = util::validate_format($group->descriptionformat); } // Custom fields. if (!empty($group->customfields)) { foreach ($group->customfields as $field) { $fieldname = self::build_custom_field_name($field['shortname']); $group->{$fieldname} = $field['value']; } } groups_update_group($group); } $transaction->allow_commit(); return null; } /** * Returns description of method result value * * @return null * @since Moodle 3.6 */ public static function update_groups_returns() { return null; } /** * Builds a structure for custom fields parameters. * * @return \core_external\external_multiple_structure */ protected static function build_custom_fields_parameters_structure(): external_multiple_structure { return new external_multiple_structure( new external_single_structure([ 'shortname' => new external_value(PARAM_ALPHANUMEXT, 'The shortname of the custom field'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field'), ]), 'Custom fields', VALUE_OPTIONAL ); } /** * Builds a structure for custom fields returns. * * @return \core_external\external_multiple_structure */ protected static function build_custom_fields_returns_structure(): external_multiple_structure { return new external_multiple_structure( new external_single_structure([ 'name' => new external_value(PARAM_RAW, 'The name of the custom field'), 'shortname' => new external_value(PARAM_RAW, 'The shortname of the custom field - to be able to build the field class in the code'), 'type' => new external_value(PARAM_ALPHANUMEXT, 'The type of the custom field - text field, checkbox...'), 'valueraw' => new external_value(PARAM_RAW, 'The raw value of the custom field'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field'), ]), 'Custom fields', VALUE_OPTIONAL ); } /** * Builds a suitable name of a custom field for a custom field handler based on provided shortname. * * @param string $shortname shortname to use. * @return string */ protected static function build_custom_field_name(string $shortname): string { return 'customfield_' . $shortname; } } grouping.php 0000644 00000012722 15215711721 0007115 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Create grouping OR edit grouping settings. * * @copyright 2006 The Open University, N.D.Freear AT open.ac.uk, J.White AT open.ac.uk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ require_once('../config.php'); require_once('lib.php'); require_once('grouping_form.php'); /// get url variables $courseid = optional_param('courseid', 0, PARAM_INT); $id = optional_param('id', 0, PARAM_INT); $delete = optional_param('delete', 0, PARAM_BOOL); $confirm = optional_param('confirm', 0, PARAM_BOOL); $url = new moodle_url('/group/grouping.php'); if ($id) { $url->param('id', $id); if (!$grouping = $DB->get_record('groupings', array('id'=>$id))) { throw new \moodle_exception('invalidgroupid'); } $grouping->description = clean_text($grouping->description); if (empty($courseid)) { $courseid = $grouping->courseid; } else if ($courseid != $grouping->courseid) { throw new \moodle_exception('invalidcourseid'); } else { $url->param('courseid', $courseid); } if (!$course = $DB->get_record('course', array('id'=>$courseid))) { throw new \moodle_exception('invalidcourseid'); } } else { $url->param('courseid', $courseid); if (!$course = $DB->get_record('course', array('id'=>$courseid))) { throw new \moodle_exception('invalidcourseid'); } $grouping = new stdClass(); $grouping->courseid = $course->id; } $PAGE->set_url($url); require_login($course); $context = context_course::instance($course->id); require_capability('moodle/course:managegroups', $context); $strgroupings = get_string('groupings', 'group'); $PAGE->set_title($strgroupings); $PAGE->set_heading($course->fullname. ': '.$strgroupings); $PAGE->set_pagelayout('admin'); navigation_node::override_active_url(new moodle_url('/group/index.php', array('id' => $course->id))); $returnurl = $CFG->wwwroot.'/group/groupings.php?id='.$course->id; if ($id and $delete) { if (!empty($grouping->idnumber) && !has_capability('moodle/course:changeidnumber', $context)) { throw new \moodle_exception('groupinghasidnumber', '', '', $grouping->name); } if (!$confirm) { $PAGE->set_title(get_string('deletegrouping', 'group')); $PAGE->set_heading($course->fullname. ': '. get_string('deletegrouping', 'group')); echo $OUTPUT->header(); $optionsyes = array('id'=>$id, 'delete'=>1, 'courseid'=>$courseid, 'sesskey'=>sesskey(), 'confirm'=>1); $optionsno = array('id'=>$courseid); $formcontinue = new single_button(new moodle_url('grouping.php', $optionsyes), get_string('yes'), 'get'); $formcancel = new single_button(new moodle_url('groupings.php', $optionsno), get_string('no'), 'get'); echo $OUTPUT->confirm(get_string('deletegroupingconfirm', 'group', $grouping->name), $formcontinue, $formcancel); echo $OUTPUT->footer(); die; } else if (confirm_sesskey()){ if (groups_delete_grouping($id)) { redirect($returnurl); } else { throw new \moodle_exception('erroreditgrouping', 'group', $returnurl); } } } // Prepare the description editor: We do support files for grouping descriptions $editoroptions = array('maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$course->maxbytes, 'trust'=>true, 'context'=>$context, 'noclean'=>true); if (!empty($grouping->id)) { $grouping = file_prepare_standard_editor($grouping, 'description', $editoroptions, $context, 'grouping', 'description', $grouping->id); } else { $grouping = file_prepare_standard_editor($grouping, 'description', $editoroptions, $context, 'grouping', 'description', null); } /// First create the form $editform = new grouping_form(null, compact('editoroptions', 'grouping')); $editform->set_data($grouping); if ($editform->is_cancelled()) { redirect($returnurl); } elseif ($data = $editform->get_data()) { $success = true; if (!has_capability('moodle/course:changeidnumber', $context)) { // Remove the idnumber if the user doesn't have permission to modify it unset($data->idnumber); } if ($data->id) { groups_update_grouping($data, $editoroptions); } else { groups_create_grouping($data, $editoroptions); } redirect($returnurl); } $strparticipants = get_string('participants'); if ($id) { $strheading = get_string('editgroupingsettings', 'group'); } else { $strheading = get_string('creategrouping', 'group'); } $PAGE->navbar->add($strparticipants, new moodle_url('/user/index.php', array('id'=>$courseid))); $PAGE->navbar->add($strgroupings, new moodle_url('/group/groupings.php', array('id'=>$courseid))); $PAGE->navbar->add($strheading); /// Print header echo $OUTPUT->header(); echo $OUTPUT->heading($strheading); $editform->display(); echo $OUTPUT->footer(); import_form.php 0000644 00000005423 15215711721 0007620 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A form for group import. * * @package core_group * @copyright 2010 Toyomoyo (http://moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } require_once($CFG->libdir.'/formslib.php'); require_once($CFG->libdir . '/csvlib.class.php'); /** * Groups import form class * * @package core_group * @copyright 2010 Toyomoyo (http://moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class groups_import_form extends moodleform { /** * Form definition */ function definition() { $mform =& $this->_form; $data = $this->_customdata; //fill in the data depending on page params //later using set_data $mform->addElement('header', 'general', get_string('general')); $filepickeroptions = array(); $filepickeroptions['filetypes'] = '*'; $filepickeroptions['maxbytes'] = get_max_upload_file_size(); $mform->addElement('filepicker', 'userfile', get_string('import'), null, $filepickeroptions); $mform->addRule('userfile', null, 'required'); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $choices = csv_import_reader::get_delimiter_list(); $mform->addElement('select', 'delimiter_name', get_string('csvdelimiter', 'group'), $choices); if (array_key_exists('cfg', $choices)) { $mform->setDefault('delimiter_name', 'cfg'); } else if (get_string('listsep', 'langconfig') == ';') { $mform->setDefault('delimiter_name', 'semicolon'); } else { $mform->setDefault('delimiter_name', 'comma'); } $choices = core_text::get_encodings(); $mform->addElement('select', 'encoding', get_string('encoding', 'group'), $choices); $mform->setDefault('encoding', 'UTF-8'); $this->add_action_buttons(true, get_string('importgroups', 'core_group')); $this->set_data($data); } } members.php 0000644 00000014667 15215711721 0006727 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Add/remove members from group. * * @copyright 2006 The Open University and others, N.D.Freear AT open.ac.uk, J.White AT open.ac.uk and others * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ require_once(__DIR__ . '/../config.php'); require_once(__DIR__ . '/lib.php'); require_once($CFG->dirroot . '/user/selector/lib.php'); require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->libdir . '/filelib.php'); $groupid = required_param('group', PARAM_INT); $cancel = optional_param('cancel', false, PARAM_BOOL); $group = $DB->get_record('groups', array('id'=>$groupid), '*', MUST_EXIST); $course = $DB->get_record('course', array('id'=>$group->courseid), '*', MUST_EXIST); $PAGE->set_url('/group/members.php', array('group'=>$groupid)); $PAGE->set_pagelayout('admin'); require_login($course); $context = context_course::instance($course->id); require_capability('moodle/course:managegroups', $context); $returnurl = $CFG->wwwroot.'/group/index.php?id='.$course->id.'&group='.$group->id; if ($cancel) { redirect($returnurl); } $groupmembersselector = new group_members_selector('removeselect', array('groupid' => $groupid, 'courseid' => $course->id)); $potentialmembersselector = new group_non_members_selector('addselect', array('groupid' => $groupid, 'courseid' => $course->id)); if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) { $userstoadd = $potentialmembersselector->get_selected_users(); if (!empty($userstoadd)) { foreach ($userstoadd as $user) { if (!groups_add_member($groupid, $user->id)) { throw new \moodle_exception('erroraddremoveuser', 'group', $returnurl); } $groupmembersselector->invalidate_selected_users(); $potentialmembersselector->invalidate_selected_users(); } } } 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)) { throw new \moodle_exception('errorremovenotpermitted', 'group', $returnurl, $user->fullname); } if (!groups_remove_member($groupid, $user->id)) { throw new \moodle_exception('erroraddremoveuser', 'group', $returnurl); } $groupmembersselector->invalidate_selected_users(); $potentialmembersselector->invalidate_selected_users(); } } } // Print the page and form $strgroups = get_string('groups'); $strparticipants = get_string('participants'); $stradduserstogroup = get_string('adduserstogroup', 'group'); $strusergroupmembership = get_string('usergroupmembership', 'group'); $groupname = format_string($group->name); $PAGE->requires->js('/group/clientlib.js'); $PAGE->navbar->add($strparticipants, new moodle_url('/user/index.php', array('id'=>$course->id))); $PAGE->navbar->add($strgroups, new moodle_url('/group/index.php', array('id'=>$course->id))); $PAGE->navbar->add($stradduserstogroup); /// Print header $PAGE->set_title("$course->shortname: $strgroups"); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('adduserstogroup', 'group').": $groupname", 3); // Store the rows we want to display in the group info. $groupinforow = array(); // Check if there is a description to display. if (!empty($group->description)) { $grouprenderer = $PAGE->get_renderer('core_group'); $groupdetailpage = new \core_group\output\group_details($groupid); echo $grouprenderer->group_details($groupdetailpage); } /// Print the editing form ?> <div id="addmembersform"> <form id="assignform" method="post" action="<?php echo $CFG->wwwroot; ?>/group/members.php?group=<?php echo $groupid; ?>"> <div> <input type="hidden" name="sesskey" value="<?php p(sesskey()); ?>" /> <table class="generaltable generalbox groupmanagementtable boxaligncenter" summary=""> <tr> <td id='existingcell'> <p> <label for="removeselect"><?php print_string('groupmembers', 'group'); ?></label> </p> <?php $groupmembersselector->display(); ?> </td> <td id='buttonscell'> <p class="arrow_button"> <input class="btn btn-secondary" name="add" id="add" type="submit" value="<?php echo $OUTPUT->larrow().' '.get_string('add'); ?>" title="<?php print_string('add'); ?>" /><br /> <input class="btn btn-secondary" name="remove" id="remove" type="submit" value="<?php echo get_string('remove').' '.$OUTPUT->rarrow(); ?>" title="<?php print_string('remove'); ?>" /> </p> </td> <td id='potentialcell'> <p> <label for="addselect"><?php print_string('potentialmembs', 'group'); ?></label> </p> <?php $potentialmembersselector->display(); ?> </td> <td> <p><?php echo($strusergroupmembership) ?></p> <div id="group-usersummary"></div> </td> </tr> <tr><td colspan="3" id='backcell'> <input class="btn btn-secondary" type="submit" name="cancel" value="<?php print_string('backtogroups', 'group'); ?>" /> </td></tr> </table> </div> </form> </div> <?php //outputs the JS array used to display the other groups users are in $potentialmembersselector->print_user_summaries($course->id); //this must be after calling display() on the selectors so their setup JS executes first $PAGE->requires->js_init_call('init_add_remove_members_page', null, false, $potentialmembersselector->get_js_module()); echo $OUTPUT->footer(); groupings.php 0000644 00000011320 15215711721 0007271 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Allows a creator to edit groupings * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ require_once '../config.php'; require_once $CFG->dirroot.'/group/lib.php'; $courseid = required_param('id', PARAM_INT); $PAGE->set_url('/group/groupings.php', array('id'=>$courseid)); if (!$course = $DB->get_record('course', array('id'=>$courseid))) { throw new \moodle_exception('invalidcourseid'); } require_login($course); $context = context_course::instance($course->id); require_capability('moodle/course:managegroups', $context); $strgrouping = get_string('grouping', 'group'); $strgroups = get_string('groups'); $strname = get_string('name'); $strdelete = get_string('delete'); $stredit = get_string('edit'); $srtnewgrouping = get_string('creategrouping', 'group'); $strgroups = get_string('groups'); $strgroupings = get_string('groupings', 'group'); $struses = get_string('activities'); $strparticipants = get_string('participants'); $strmanagegrping = get_string('showgroupsingrouping', 'group'); navigation_node::override_active_url(new moodle_url('/group/index.php', array('id'=>$courseid))); $PAGE->navbar->add($strgroupings); /// Print header $PAGE->set_title($strgroupings); $PAGE->set_heading($course->fullname); $PAGE->set_pagelayout('standard'); echo $OUTPUT->header(); echo $OUTPUT->render_participants_tertiary_nav($course); $data = array(); if ($groupings = $DB->get_records('groupings', array('courseid'=>$course->id), 'name')) { $canchangeidnumber = has_capability('moodle/course:changeidnumber', $context); foreach ($groupings as $gid => $grouping) { $groupings[$gid]->formattedname = format_string($grouping->name, true, array('context' => $context)); } core_collator::asort_objects_by_property($groupings, 'formattedname'); foreach($groupings as $grouping) { $line = array(); $line[0] = $grouping->formattedname; if ($groups = groups_get_all_groups($courseid, 0, $grouping->id)) { $groupnames = array(); foreach ($groups as $group) { $groupnames[] = format_string($group->name); } $line[1] = implode(', ', $groupnames); } else { $line[1] = get_string('none'); } $line[2] = $DB->count_records('course_modules', array('course'=>$course->id, 'groupingid'=>$grouping->id)); $url = new moodle_url('/group/grouping.php', array('id' => $grouping->id)); $buttons = html_writer::link($url, $OUTPUT->pix_icon('t/edit', $stredit, 'core', array('class' => 'iconsmall')), array('title' => $stredit)); if (empty($grouping->idnumber) || $canchangeidnumber) { // It's only possible to delete groups without an idnumber unless the user has the changeidnumber capability. $url = new moodle_url('/group/grouping.php', array('id' => $grouping->id, 'delete' => 1)); $buttons .= html_writer::link($url, $OUTPUT->pix_icon('t/delete', $strdelete, 'core', array('class' => 'iconsmall')), array('title' => $strdelete)); } else { $buttons .= $OUTPUT->spacer(); } $url = new moodle_url('/group/assign.php', array('id' => $grouping->id)); $buttons .= html_writer::link($url, $OUTPUT->pix_icon('t/groups', $strmanagegrping, 'core', array('class' => 'iconsmall')), array('title' => $strmanagegrping)); $line[3] = $buttons; $data[] = $line; } } $table = new html_table(); $table->head = array($strgrouping, $strgroups, $struses, $stredit); $table->size = array('30%', '50%', '10%', '10%'); $table->align = array('left', 'left', 'center', 'center'); $table->width = '90%'; $table->data = $data; echo html_writer::table($table); echo $OUTPUT->container_start('buttons'); echo $OUTPUT->single_button(new moodle_url('grouping.php', array('courseid'=>$courseid)), $srtnewgrouping); echo $OUTPUT->container_end(); echo $OUTPUT->footer(); index.php 0000644 00000023007 15215711721 0006370 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The main group management user interface. * * @copyright 2006 The Open University, N.D.Freear AT open.ac.uk, J.White AT open.ac.uk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ require_once('../config.php'); require_once('lib.php'); $courseid = required_param('id', PARAM_INT); $groupid = optional_param('group', false, PARAM_INT); $userid = optional_param('user', false, PARAM_INT); $action = optional_param('action', false, PARAM_TEXT); // Support either single group= parameter, or array groups[]. if ($groupid) { $groupids = array($groupid); } else { $groupids = optional_param_array('groups', array(), PARAM_INT); } $singlegroup = (count($groupids) == 1); $returnurl = $CFG->wwwroot.'/group/index.php?id='.$courseid; // Get the course information so we can print the header and // check the course id is valid. $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $url = new moodle_url('/group/index.php', array('id' => $courseid)); navigation_node::override_active_url($url); if ($userid) { $url->param('user', $userid); } if ($groupid) { $url->param('group', $groupid); } $PAGE->set_url($url); // Make sure that the user has permissions to manage groups. require_login($course); $context = context_course::instance($course->id); require_capability('moodle/course:managegroups', $context); $PAGE->requires->js('/group/clientlib.js', true); $PAGE->requires->js('/group/module.js', true); // Check for multiple/no group errors. if (!$singlegroup) { switch($action) { case 'ajax_getmembersingroup': case 'showgroupsettingsform': case 'showaddmembersform': case 'updatemembers': throw new \moodle_exception('errorselectone', 'group', $returnurl); } } switch ($action) { case false: // OK, display form. break; case 'ajax_getmembersingroup': $roles = array(); $userfieldsapi = \core_user\fields::for_identity($context)->with_userpic(); [ 'selects' => $userfieldsselects, 'joins' => $userfieldsjoin, 'params' => $userfieldsparams ] = (array)$userfieldsapi->get_sql('u', true, '', '', false); $extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); if ($groupmemberroles = groups_get_members_by_role($groupids[0], $courseid, 'u.id, ' . $userfieldsselects, null, '', $userfieldsparams, $userfieldsjoin)) { $viewfullnames = has_capability('moodle/site:viewfullnames', $context); foreach ($groupmemberroles as $roleid => $roledata) { $shortroledata = new stdClass(); $shortroledata->name = html_entity_decode($roledata->name, ENT_QUOTES, 'UTF-8'); $shortroledata->users = array(); foreach ($roledata->users as $member) { $shortmember = new stdClass(); $shortmember->id = $member->id; $shortmember->name = fullname($member, $viewfullnames); if ($extrafields) { $extrafieldsdisplay = []; foreach ($extrafields as $field) { // No escaping here, handled client side in response to AJAX request. $extrafieldsdisplay[] = $member->{$field}; } $shortmember->name .= ' (' . implode(', ', $extrafieldsdisplay) . ')'; } $shortroledata->users[] = $shortmember; } $roles[] = $shortroledata; } } echo json_encode($roles); die; // Client side JavaScript takes it from here. case 'deletegroup': if (count($groupids) == 0) { throw new \moodle_exception('errorselectsome', 'group', $returnurl); } $groupidlist = implode(',', $groupids); redirect(new moodle_url('/group/delete.php', array('courseid' => $courseid, 'groups' => $groupidlist))); break; case 'showcreateorphangroupform': redirect(new moodle_url('/group/group.php', array('courseid' => $courseid))); break; case 'showautocreategroupsform': redirect(new moodle_url('/group/autogroup.php', array('courseid' => $courseid))); break; case 'showimportgroups': redirect(new moodle_url('/group/import.php', array('id' => $courseid))); break; case 'showgroupsettingsform': redirect(new moodle_url('/group/group.php', array('courseid' => $courseid, 'id' => $groupids[0]))); break; case 'updategroups': // Currently reloading. break; case 'removemembers': break; case 'showaddmembersform': redirect(new moodle_url('/group/members.php', array('group' => $groupids[0]))); break; case 'updatemembers': // Currently reloading. break; case 'enablemessaging': set_groups_messaging($groupids, true); redirect($returnurl, get_string('messagingenabled', 'group', count($groupids)), null, \core\output\notification::NOTIFY_SUCCESS); break; case 'disablemessaging': set_groups_messaging($groupids, false); redirect($returnurl, get_string('messagingdisabled', 'group', count($groupids)), null, \core\output\notification::NOTIFY_SUCCESS); break; default: // ERROR. throw new \moodle_exception('unknowaction', '', $returnurl); break; } // Print the page and form. $strgroups = get_string('groups'); $strparticipants = get_string('participants'); // Print header. $PAGE->set_title($strgroups); $PAGE->set_heading($course->fullname); $PAGE->set_pagelayout('standard'); echo $OUTPUT->header(); echo $OUTPUT->render_participants_tertiary_nav($course); $groups = groups_get_all_groups($courseid); $selectedname = null; $preventgroupremoval = array(); // Get list of groups to render. $groupoptions = array(); if ($groups) { foreach ($groups as $group) { $selected = false; $usercount = $DB->count_records('groups_members', array('groupid' => $group->id)); $groupname = format_string($group->name, true, ['context' => $context, 'escape' => false]) . ' (' . $usercount . ')'; if (in_array($group->id, $groupids)) { $selected = true; if ($singlegroup) { // Only keep selected name if there is one group selected. $selectedname = $groupname; } } if (!empty($group->idnumber) && !has_capability('moodle/course:changeidnumber', $context)) { $preventgroupremoval[$group->id] = true; } $groupoptions[] = (object) [ 'value' => $group->id, 'selected' => $selected, 'text' => s($groupname) ]; } } // Get list of group members to render if there is a single selected group. $members = array(); if ($singlegroup) { $userfieldsapi = \core_user\fields::for_identity($context)->with_userpic(); [ 'selects' => $userfieldsselects, 'joins' => $userfieldsjoin, 'params' => $userfieldsparams ] = (array)$userfieldsapi->get_sql('u', true, '', '', false); $extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); if ($groupmemberroles = groups_get_members_by_role(reset($groupids), $courseid, 'u.id, ' . $userfieldsselects, null, '', $userfieldsparams, $userfieldsjoin)) { $viewfullnames = has_capability('moodle/site:viewfullnames', $context); foreach ($groupmemberroles as $roleid => $roledata) { $users = array(); foreach ($roledata->users as $member) { $shortmember = new stdClass(); $shortmember->value = $member->id; $shortmember->text = fullname($member, $viewfullnames); if ($extrafields) { $extrafieldsdisplay = []; foreach ($extrafields as $field) { $extrafieldsdisplay[] = s($member->{$field}); } $shortmember->text .= ' (' . implode(', ', $extrafieldsdisplay) . ')'; } $users[] = $shortmember; } $members[] = (object)[ 'role' => html_entity_decode($roledata->name, ENT_QUOTES, 'UTF-8'), 'rolemembers' => $users ]; } } } $disableaddedit = !$singlegroup; $disabledelete = !empty($groupids); $caneditmessaging = \core_message\api::can_create_group_conversation($USER->id, $context); $renderable = new \core_group\output\index_page($courseid, $groupoptions, $selectedname, $members, $disableaddedit, $disabledelete, $preventgroupremoval, $caneditmessaging); $output = $PAGE->get_renderer('core_group'); echo $output->render($renderable); echo $OUTPUT->footer(); autogroup.php 0000644 00000023752 15215711721 0007315 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Create and allocate users to groups * * @package core_group * @copyright Matt Clarkson mattc@catalyst.net.nz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once('lib.php'); require_once('autogroup_form.php'); if (!defined('AUTOGROUP_MIN_RATIO')) { define('AUTOGROUP_MIN_RATIO', 0.7); // means minimum member count is 70% in the smallest group } $courseid = required_param('courseid', PARAM_INT); $PAGE->set_url('/group/autogroup.php', array('courseid' => $courseid)); if (!$course = $DB->get_record('course', array('id'=>$courseid))) { throw new \moodle_exception('invalidcourseid'); } // Make sure that the user has permissions to manage groups. require_login($course); $context = context_course::instance($courseid); require_capability('moodle/course:managegroups', $context); $returnurl = $CFG->wwwroot.'/group/index.php?id='.$course->id; $strgroups = get_string('groups'); $strparticipants = get_string('participants'); $strautocreategroups = get_string('autocreategroups', 'group'); $PAGE->set_title($strgroups); $PAGE->set_heading($course->fullname. ': '.$strgroups); $PAGE->set_pagelayout('admin'); navigation_node::override_active_url(new moodle_url('/group/index.php', array('id' => $courseid))); // Print the page and form $preview = ''; $error = ''; /// Get applicable roles - used in menus etc later on $rolenames = role_fix_names(get_profile_roles($context), $context, ROLENAME_BOTH, true); /// Create the form $editform = new autogroup_form(null, array('roles' => $rolenames)); $editform->set_data(array('courseid' => $courseid, 'seed' => time())); /// Handle form submission if ($editform->is_cancelled()) { redirect($returnurl); } elseif ($data = $editform->get_data()) { /// Allocate members from the selected role to groups switch ($data->allocateby) { case 'no': case 'random': case 'lastname': $orderby = 'lastname, firstname'; break; case 'firstname': $orderby = 'firstname, lastname'; break; case 'idnumber': $orderby = 'idnumber'; break; default: throw new \moodle_exception('unknoworder'); } $source = array(); if ($data->cohortid) { $source['cohortid'] = $data->cohortid; } if ($data->groupingid) { $source['groupingid'] = $data->groupingid; } if ($data->groupid) { $source['groupid'] = $data->groupid; } // Display only active users if the option was selected or they do not have the capability to view suspended users. $onlyactive = !empty($data->includeonlyactiveenrol) || !has_capability('moodle/course:viewsuspendedusers', $context); // TODO Does not support custom user profile fields (MDL-70456). $extrafields = \core_user\fields::get_identity_fields($context, false); $users = groups_get_potential_members($data->courseid, $data->roleid, $source, $orderby, !empty($data->notingroup), $onlyactive, $extrafields); $usercnt = count($users); if ($data->allocateby == 'random') { srand($data->seed); shuffle($users); } $groups = array(); // Plan the allocation if ($data->groupby == 'groups') { $numgrps = $data->number; $userpergrp = floor($usercnt/$numgrps); } else { // members $numgrps = ceil($usercnt/$data->number); $userpergrp = $data->number; if (!empty($data->nosmallgroups) and $usercnt % $data->number != 0) { // If there would be one group with a small number of member reduce the number of groups $missing = $userpergrp * $numgrps - $usercnt; if ($missing > $userpergrp * (1-AUTOGROUP_MIN_RATIO)) { // spread the users from the last small group $numgrps--; $userpergrp = floor($usercnt/$numgrps); } } } // allocate the users - all groups equal count first for ($i=0; $i<$numgrps; $i++) { $groups[$i] = array(); $groups[$i]['name'] = groups_parse_name(trim($data->namingscheme), $i); $groups[$i]['members'] = array(); if ($data->allocateby == 'no') { continue; // do not allocate users } for ($j=0; $j<$userpergrp; $j++) { if (empty($users)) { break 2; } $user = array_shift($users); $groups[$i]['members'][$user->id] = $user; } } // now distribute the rest if ($data->allocateby != 'no') { for ($i=0; $i<$numgrps; $i++) { if (empty($users)) { break 1; } $user = array_shift($users); $groups[$i]['members'][$user->id] = $user; } } if (isset($data->preview)) { $table = new html_table(); if ($data->allocateby == 'no') { $table->head = array(get_string('groupscount', 'group', $numgrps)); $table->size = array('100%'); $table->align = array('left'); $table->width = '40%'; } else { $table->head = array(get_string('groupscount', 'group', $numgrps), get_string('groupmembers', 'group'), get_string('usercounttotal', 'group', $usercnt)); $table->size = array('20%', '70%', '10%'); $table->align = array('left', 'left', 'center'); $table->width = '90%'; } $table->data = array(); $viewfullnames = has_capability('moodle/site:viewfullnames', $context); foreach ($groups as $group) { $line = array(); if (groups_get_group_by_name($courseid, $group['name'])) { $line[] = '<span class="notifyproblem">'.get_string('groupnameexists', 'group', $group['name']).'</span>'; $error = get_string('groupnameexists', 'group', $group['name']); } else { $line[] = $group['name']; } if ($data->allocateby != 'no') { $unames = array(); foreach ($group['members'] as $user) { $fullname = fullname($user, $viewfullnames); if ($extrafields) { $extrafieldsdisplay = []; foreach ($extrafields as $field) { $extrafieldsdisplay[] = s($user->{$field}); } $fullname .= ' (' . implode(', ', $extrafieldsdisplay) . ')'; } $unames[] = $fullname; } $line[] = implode(', ', $unames); $line[] = count($group['members']); } $table->data[] = $line; } $preview .= html_writer::table($table); } else { $grouping = null; $createdgrouping = null; $createdgroups = array(); $failed = false; // prepare grouping if (!empty($data->grouping)) { if ($data->grouping < 0) { $grouping = new stdClass(); $grouping->courseid = $COURSE->id; $grouping->name = trim($data->groupingname); $grouping->id = groups_create_grouping($grouping); $createdgrouping = $grouping->id; } else { $grouping = groups_get_grouping($data->grouping); } } // Save the groups data foreach ($groups as $key=>$group) { if (groups_get_group_by_name($courseid, $group['name'])) { $error = get_string('groupnameexists', 'group', $group['name']); $failed = true; break; } $newgroup = new stdClass(); $newgroup->courseid = $data->courseid; $newgroup->name = $group['name']; $newgroup->enablemessaging = $data->enablemessaging ?? 0; $newgroup->visibility = GROUPS_VISIBILITY_ALL; $groupid = groups_create_group($newgroup); $createdgroups[] = $groupid; foreach($group['members'] as $user) { groups_add_member($groupid, $user->id); } if ($grouping) { // Ask this function not to invalidate the cache, we'll do that manually once at the end. groups_assign_grouping($grouping->id, $groupid, null, false); } } // Invalidate the course groups cache seeing as we've changed it. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); if ($failed) { foreach ($createdgroups as $groupid) { groups_delete_group($groupid); } if ($createdgrouping) { groups_delete_grouping($createdgrouping); } } else { redirect($returnurl); } } } $PAGE->navbar->add($strparticipants, new moodle_url('/user/index.php', array('id'=>$courseid))); $PAGE->navbar->add($strgroups, new moodle_url('/group/index.php', array('id'=>$courseid))); $PAGE->navbar->add($strautocreategroups); echo $OUTPUT->header(); echo $OUTPUT->heading($strautocreategroups); if ($error != '') { echo $OUTPUT->notification($error); } /// Display the form $editform->display(); if($preview !== '') { echo $OUTPUT->heading(get_string('groupspreview', 'group')); echo $preview; } echo $OUTPUT->footer(); clientlib.js 0000644 00000021236 15215711721 0007055 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Client-side JavaScript for group management interface. * @copyright vy-shane AT moodle.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ /** * Class UpdatableGroupsCombo */ function UpdatableGroupsCombo(wwwRoot, courseId) { this.wwwRoot = wwwRoot; this.courseId = courseId; this.connectCallback = { success: function(o) { if (o.responseText !== undefined) { var groupsComboEl = document.getElementById("groups"); var membersComboEl = document.getElementById("members"); if (membersComboEl) { // Clear the members list box. while (membersComboEl.firstChild) { membersComboEl.removeChild(membersComboEl.firstChild); } } if (groupsComboEl && o.responseText) { var groups = eval("("+o.responseText+")"); // Populate the groups list box. for (var i=0; i<groups.length; i++) { var optionEl = document.createElement("option"); optionEl.setAttribute("value", groups[i].id); optionEl.title = groups[i].name; optionEl.innerHTML = groups[i].name; groupsComboEl.appendChild(optionEl); } } } // Remove the loader gif image. removeLoaderImgs("groupsloader", "groupslabel"); }, failure: function(o) { removeLoaderImgs("membersloader", "memberslabel"); this.currentTransId = null; } }; } /** * Class UpdatableMembersCombo */ function UpdatableMembersCombo(wwwRoot, courseId) { this.wwwRoot = wwwRoot; this.courseId = courseId; this.connectCallback = { success: function(t, o) { if (o.responseText !== undefined) { var selectEl = document.getElementById("members"); if (selectEl && o.responseText) { var roles = eval("("+o.responseText+")"); // Clear the members list box. if (selectEl) { while (selectEl.firstChild) { selectEl.removeChild(selectEl.firstChild); } } // Populate the members list box. for (var i=0; i<roles.length; i++) { var optgroupEl = document.createElement("optgroup"); optgroupEl.setAttribute("label",roles[i].name); for(var j=0; j<roles[i].users.length; j++) { var optionEl = document.createElement("option"); optionEl.setAttribute("value", roles[i].users[j].id); optionEl.title = roles[i].users[j].name; optionEl.innerHTML = Y.Escape.html(roles[i].users[j].name); optgroupEl.appendChild(optionEl); } selectEl.appendChild(optgroupEl); } } } // Remove the loader gif image. removeLoaderImgs("membersloader", "memberslabel"); }, failure: function() { removeLoaderImgs("membersloader", "memberslabel"); } }; // Hide the updatemembers input since AJAX will take care of this. var updatemembers = Y.one('#updatemembers'); if (updatemembers) { updatemembers.hide(); } } /** * When a group is selected, we need to update the members. * The Add/Remove Users button also needs to be disabled/enabled * depending on whether or not a group is selected */ UpdatableMembersCombo.prototype.refreshMembers = function () { // Get group selector and check selection type var selectEl = document.getElementById("groups"); var selectionCount=0,groupId=0; if( selectEl ) { for (var i = 0; i < selectEl.options.length; i++) { if(selectEl.options[i].selected) { selectionCount++; if(!groupId) { groupId=selectEl.options[i].value; } } } } var singleSelection=selectionCount == 1; // Add the loader gif image (we only load for single selections) if(singleSelection) { createLoaderImg("membersloader", "memberslabel", this.wwwRoot); } // Update the label. var spanEl = document.getElementById("thegroup"); if (singleSelection) { spanEl.innerHTML = selectEl.options[selectEl.selectedIndex].title; } else { spanEl.innerHTML = ' '; } // Clear the members list box. selectEl = document.getElementById("members"); if (selectEl) { while (selectEl.firstChild) { selectEl.removeChild(selectEl.firstChild); } } document.getElementById("showaddmembersform").disabled = !singleSelection; document.getElementById("showeditgroupsettingsform").disabled = !singleSelection; document.getElementById("deletegroup").disabled = selectionCount == 0; if(singleSelection) { var sUrl = this.wwwRoot + "/group/index.php?id=" + this.courseId + "&group=" + groupId + "&action=ajax_getmembersingroup"; var self = this; YUI().use('io', function (Y) { Y.io(sUrl, { method: 'GET', context: this, on: self.connectCallback }); }); } }; var createLoaderImg = function (elClass, parentId, wwwRoot) { var parentEl = document.getElementById(parentId); if (!parentEl) { return false; } if (document.getElementById("loaderImg")) { // A loader image already exists. return false; } var loadingImg = document.createElement("img"); loadingImg.setAttribute("src", M.util.image_url('/i/ajaxloader', 'moodle')); loadingImg.setAttribute("class", elClass); loadingImg.setAttribute("alt", "Loading"); loadingImg.setAttribute("id", "loaderImg"); parentEl.appendChild(loadingImg); return true; }; var removeLoaderImgs = function (elClass, parentId) { var parentEl = document.getElementById(parentId); if (parentEl) { var loader = document.getElementById("loaderImg"); if (loader) { parentEl.removeChild(loader); } } }; /** * Updates the current groups information shown about a user when a user is selected. * * @global {Array} userSummaries * userSummaries is added to the page via /user/selector/lib.php - group_non_members_selector::print_user_summaries() * as a global that can be used by this function. */ function updateUserSummary() { var selectEl = document.getElementById('addselect'), summaryDiv = document.getElementById('group-usersummary'), length = selectEl.length, selectCnt = 0, selectIdx = -1, i; for (i = 0; i < length; i++) { if (selectEl.options[i].selected) { selectCnt++; selectIdx = i; } } if (selectCnt == 1 && userSummaries[selectIdx]) { summaryDiv.innerHTML = userSummaries[selectIdx]; } else { summaryDiv.innerHTML = ''; } return true; } function init_add_remove_members_page(Y) { var add = Y.one('#add'); var addselect = M.core_user.get_user_selector('addselect'); add.set('disabled', addselect.is_selection_empty()); addselect.on('user_selector:selectionchanged', function(isempty) { add.set('disabled', isempty); }); var remove = Y.one('#remove'); var removeselect = M.core_user.get_user_selector('removeselect'); remove.set('disabled', removeselect.is_selection_empty()); removeselect.on('user_selector:selectionchanged', function(isempty) { remove.set('disabled', isempty); }); addselect = document.getElementById('addselect'); addselect.onchange = updateUserSummary; } module.js 0000644 00000003040 15215711721 0006366 0 ustar 00 /* * To change this template, choose Tools | Templates * and open the template in the editor. */ M.core_group = { hoveroverlay : null }; M.core_group.init_index = function(Y, wwwroot, courseid) { M.core_group.groupsCombo = new UpdatableGroupsCombo(wwwroot, courseid); M.core_group.membersCombo = new UpdatableMembersCombo(wwwroot, courseid); }; M.core_group.groupslist = function(Y, preventgroupremoval) { var actions = { init : function() { // We need to add check_deletable both on change for the groups, and then call it the first time the page loads Y.one('#groups').on('change', this.check_deletable, this); this.check_deletable(); }, check_deletable : function() { // Ensure that if the 'preventremoval' attribute is set, the delete button is greyed out var candelete = true; var optionselected = false; Y.one('#groups').get('options').each(function(option) { if (option.get('selected')) { optionselected = true; if (option.getAttribute('value') in preventgroupremoval) { candelete = false; } } }, this); var deletebutton = Y.one('#deletegroup'); if (candelete && optionselected) { deletebutton.removeAttribute('disabled'); } else { deletebutton.setAttribute('disabled', 'disabled'); } } } actions.init(); }; templates/index.mustache 0000644 00000016707 15215711721 0011421 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_group/index Template for the Groups page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * courseid int The course ID. * selectedgroup string The initially selected group. * editgroupsettingsdisabled bool Whether to disable the "Edit group settings" button on load. * deletegroupdisabled bool Whether to disable the "Delete selected group" button on load. * addmembersdisabled bool Whether to disable the "Add/remove users" button on load. * groups array The list of groups. * members array The list of members, grouped based on roles. * undeletablegroups string A JSON string containing an array of group IDs that a user cannot delete. * messagingsettingsvisible bool Wether the messaging settings buttons should be visible. Example context (json): { "courseid": "1", "selectedgroup": "Group 1 (3)", "editgroupsettingsdisabled": false, "deletegroupdisabled": false, "addmembersdisabled": false, "messagingenabled": true, "groups": [ { "value": "1", "text": "Group 1 (3)", "selected": true }, { "value": "2", "text": "Group 2 (2)" } ], "members": [ { "role": "Student", "rolemembers": [ { "value": "1", "text": "John Doe" }, { "value": "2", "text": "Jane Doe" }, { "value": "3", "text": "John Smith" } ] } ], "undeletablegroups": "[1: true, 3: true]" } }} <form id="groupeditform" action="index.php" method="post"> <div class="container-fluid groupmanagementtable"> <div class="row rtl-compatible"> <div class="col-md-6 mb-1"> <input type="hidden" name="id" value="{{courseid}}"> <div class="mb-3"> <label for="groups"> <span id="groupslabel">{{#str}}groups{{/str}}</span> <span id="thegrouping"> </span> </label> <select name="groups[]" multiple="multiple" id="groups" size="15" class="form-control input-block-level"> {{#groups}} <option value="{{value}}" {{#selected}}selected="selected"{{/selected}} title="{{{text}}}">{{{text}}}</option> {{/groups}} </select> </div> <h3> {{#str}} withselected, group {{/str}} </h3> <div class="mb-3"> <button type="submit" name="action" id="updatemembers" value="updatemembers" class="btn btn-secondary">{{#str}}showmembersforgroup, group{{/str}}</button> </div> <div class="mb-3"> <button type="submit" name="action" id="showeditgroupsettingsform" value="showgroupsettingsform" {{#editgroupsettingsdisabled}}disabled="disabled"{{/editgroupsettingsdisabled}} class="btn btn-secondary">{{#str}}editgroupsettings, group{{/str}}</button> </div> <div class="mb-3"> <button type="submit" name="action" id="deletegroup" value="deletegroup" {{#deletegroupdisabled}}disabled="disabled"{{/deletegroupdisabled}} class="btn btn-secondary">{{#str}}deleteselectedgroup, group{{/str}}</button> </div> {{#messagingsettingsvisible}} <div class="mb-3"> <button type="submit" name="action" id="disablemessaging" value="disablemessaging" class="btn btn-secondary" disabled="disabled">{{#str}}disablemessagingaction, group{{/str}}</button> </div> <div class="mb-3"> <button type="submit" name="action" id="enablemessaging" value="enablemessaging" class="btn btn-secondary" disabled="disabled">{{#str}}enablemessagingaction, group{{/str}}</button> </div> {{/messagingsettingsvisible}} <h3> {{#str}} manageactions, group {{/str}} </h3> <div class="mb-3"> <button type="submit" name="action" id="showcreateorphangroupform" value="showcreateorphangroupform" class="btn btn-secondary">{{#str}}creategroup, group{{/str}}</button> </div> <div class="mb-3"> <button type="submit" name="action" id="showautocreategroupsform" value="showautocreategroupsform" class="btn btn-secondary">{{#str}}autocreategroups, group{{/str}}</button> </div> <div class="mb-3"> <button type="submit" name="action" id="showimportgroups" value="showimportgroups" class="btn btn-secondary">{{#str}}importgroups, group{{/str}}</button> </div> </div> <div class="col-md-6 mb-1"> <div class="mb-3"> <label for="members"> <span id="memberslabel">{{#str}}membersofselectedgroup, group{{/str}}</span> <span id="thegroup">{{{selectedgroup}}}</span> </label> <select size="15" multiple="multiple" class="form-control input-block-level" id="members" name="user"> {{#members}} <optgroup label="{{role}}"> {{#rolemembers}} <option value="{{value}}" title="{{text}}">{{{text}}}</option> {{/rolemembers}} </optgroup> {{/members}} </select> </div> <div class="mb-3"> <button type="submit" value="showaddmembersform" class="btn btn-secondary" {{#addmembersdisabled}}disabled="disabled"{{/addmembersdisabled}} name="action" id="showaddmembersform">{{#str}}adduserstogroup, group{{/str}}</button> </div> </div> </div> </div> </form> {{#js}} require(['jquery', 'core/yui'], function($) { $("#groups").change(function() { M.core_group.membersCombo.refreshMembers(); }); M.core_group.init_index(Y, "{{wwwroot}}", {{courseid}}); var undeletableGroups = JSON.parse('{{{undeletablegroups}}}'); M.core_group.groupslist(Y, undeletableGroups); }); {{/js}} {{#js}} require(['core_group/index'], (module) => module.init()); {{/js}} templates/comboboxsearch/searchbody.mustache 0000644 00000004147 15215711721 0015426 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_group/comboboxsearch/searchbody Wrapping template for search input. Context variables required for this template: * courseid - The id of the course to search within. * groupid - The id of the group to search within. * currentvalue - The prefill value for the search input if provided Example context (json): { "courseid": 2, "groupid": 25, "currentvalue": "bar" } }} <div class="flex-column h-100 w-100"> <span class="d-none" data-region="courseid" data-courseid="{{courseid}}"></span> <span class="d-none" data-region="groupid" data-groupid="{{groupid}}"></span> <span class="d-none" data-region="instance" data-instance="{{instance}}"></span> {{< core/search_input_auto }} {{$label}}{{#str}} searchgroups, core {{/str}}{{/label}} {{$placeholder}}{{#str}} searchgroups, core {{/str}}{{/placeholder}} {{$value}}{{currentvalue}}{{/value}} {{$additionalattributes}} role="combobox" aria-expanded="true" aria-controls="groups-{{instance}}-result-listbox" aria-autocomplete="list" data-input-element="input-{{uniqid}}-{{instance}}" {{/additionalattributes}} {{/ core/search_input_auto }} <input type="hidden" name="search" id="input-{{uniqid}}-{{instance}}"/> <div data-region="searchplaceholder"></div> </div> templates/comboboxsearch/resultset.mustache 0000644 00000003456 15215711721 0015337 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_group/comboboxsearch/resultset Wrapping template for returned result items. Context variables required for this template: * groups - Our returned groups to render. * instance - The instance ID of the combo box. * searchterm - The entered text to find these results. * hasgroups - Allow the handling where no users exist for the returned search term. Example context (json): { "groups": [ { "id": 2, "name": "Foo bar", "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2" }, { "id": 3, "name": "Bar Foo", "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3" } ], "instance": 25, "searchterm": "Foo", "hasresults": true } }} {{<core/local/comboboxsearch/resultset}} {{$listid}}groups{{/listid}} {{$results}} {{#groups}} {{>core_group/comboboxsearch/resultitem}} {{/groups}} {{/results}} {{/core/local/comboboxsearch/resultset}} templates/comboboxsearch/resultitem.mustache 0000644 00000003005 15215711721 0015470 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_group/comboboxsearch/resultitem Template for the individual result item. Context variables required for this template: * id - Group system ID. * name - Groups' name. * groupimageurl - The link of the groups picture. Example context (json): { "id": 2, "name": "Foo bar", "groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2" } }} {{<core/local/comboboxsearch/resultitem }} {{$content}} <div class="pe-2 ps-1 w-25"> <img class="rounded-circle mx-auto img-fluid" src="{{groupimageurl}}" alt=""/> </div> <div class="pe-3 w-75"> <span class="d-block w-100 p-0 text-truncate"> {{name}} </span> </div> {{/content}} {{/core/local/comboboxsearch/resultitem}} templates/comboboxsearch/group_selector.mustache 0000644 00000003061 15215711721 0016331 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_group/comboboxsearch/group_selector The group selector trigger element. Context variables required for this template: * label - The label text fot the group selector element. * group - The value of the group selector element (id of the preselected group) * selectedgroup - The text of the selected group option. Example context (json): { "label": "Select separate groups", "group": "21", "selectedgroup": "Group 1" } }} <span class="d-none" data-region="groupid" data-groupid="{{group}}"></span> <div class="align-items-center d-flex"> <div class="d-block pe-3 text-truncate"> <span class="d-block m-0 small" aria-hidden="true"> {{label}} </span> <span class="p-0 font-weight-bold" data-selected-option> {{selectedgroup}} </span> </div> </div> templates/group_details.mustache 0000644 00000003455 15215711721 0013147 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_group/group_details Template for the Groups page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string Group Name * pictureurl string Group image url * description string Group description * edit string edit link to edit the group Example context (json): { "name": "Group Name", "pictureurl": "https://raw.githubusercontent.com/moodle/moodle/master/pix/g/f1.png", "description": "This is the description for Group Name", "editurl": "http://www.moodle.org" } }} {{#name}} <div class="groupinfobox container-fluid card p-3"> {{#pictureurl}} <div class="group-image"><img class="grouppicture" src="{{{pictureurl}}}" alt="{{{name}}}" title="{{{name}}}"/></div> {{/pictureurl}} {{#editurl}} <div class="group-edit"><a href="{{editurl}}">{{#pix}}t/edit, core, {{#str}}editgroupprofile{{/str}}{{/pix}}</a></div> {{/editurl}} <h3 class="">{{{name}}}</h3> <div class="group-description">{{{description}}}</div> </div> {{/name}} overview.php 0000644 00000033677 15215711721 0007145 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Print an overview of groupings & group membership * * @copyright Matt Clarkson mattc@catalyst.net.nz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ require_once('../config.php'); require_once($CFG->libdir . '/filelib.php'); define('OVERVIEW_NO_GROUP', -1); // The fake group for users not in a group. define('OVERVIEW_GROUPING_GROUP_NO_GROUPING', -1); // The fake grouping for groups that have no grouping. define('OVERVIEW_GROUPING_NO_GROUP', -2); // The fake grouping for users with no group. $courseid = required_param('id', PARAM_INT); $groupid = optional_param('group', 0, PARAM_INT); $groupingid = optional_param('grouping', 0, PARAM_INT); $dataformat = optional_param('dataformat', '', PARAM_ALPHA); $returnurl = $CFG->wwwroot.'/group/index.php?id='.$courseid; $rooturl = $CFG->wwwroot.'/group/overview.php?id='.$courseid; if (!$course = $DB->get_record('course', array('id'=>$courseid))) { throw new \moodle_exception('invalidcourse'); } $url = new moodle_url('/group/overview.php', array('id'=>$courseid)); if ($groupid !== 0) { $url->param('group', $groupid); } if ($groupingid !== 0) { $url->param('grouping', $groupingid); } $PAGE->set_url($url); // Make sure that the user has permissions to manage groups. require_login($course); $context = context_course::instance($courseid); require_capability('moodle/course:managegroups', $context); $strgroups = get_string('groups'); $strparticipants = get_string('participants'); $stroverview = get_string('overview', 'group'); $strgrouping = get_string('grouping', 'group'); $strgroup = get_string('group', 'group'); $strnotingrouping = get_string('notingrouping', 'group'); $strfiltergroups = get_string('filtergroups', 'group'); $strnogroups = get_string('nogroups', 'group'); $strdescription = get_string('description'); $strnotingroup = get_string('notingrouplist', 'group'); $strnogroup = get_string('nogroup', 'group'); $strnogrouping = get_string('nogrouping', 'group'); // This can show all users and all groups in a course. // This is lots of data so allow this script more resources. raise_memory_limit(MEMORY_EXTRA); // Get all groupings and sort them by formatted name. $groupings = $DB->get_records('groupings', array('courseid'=>$courseid), 'name'); foreach ($groupings as $gid => $grouping) { $groupings[$gid]->formattedname = format_string($grouping->name, true, array('context' => $context)); } core_collator::asort_objects_by_property($groupings, 'formattedname'); $members = array(); foreach ($groupings as $grouping) { $members[$grouping->id] = array(); } // Groups not in a grouping. $members[OVERVIEW_GROUPING_GROUP_NO_GROUPING] = array(); // Get all groups and sort them by formatted name. $groups = $DB->get_records('groups', array('courseid'=>$courseid), 'name'); foreach ($groups as $id => $group) { $groups[$id]->formattedname = format_string($group->name, true, ['context' => $context]); } core_collator::asort_objects_by_property($groups, 'formattedname'); $params = array('courseid'=>$courseid); if ($groupid) { $groupwhere = "AND g.id = :groupid"; $params['groupid'] = $groupid; } else { $groupwhere = ""; } if ($groupingid) { if ($groupingid < 0) { // No grouping filter. $groupingwhere = "AND gg.groupingid IS NULL"; } else { $groupingwhere = "AND gg.groupingid = :groupingid"; $params['groupingid'] = $groupingid; } } else { $groupingwhere = ""; } list($sort, $sortparams) = users_order_by_sql('u'); $userfieldsapi = \core_user\fields::for_identity($context)->with_userpic(); [ 'selects' => $userfieldsselects, 'joins' => $userfieldsjoin, 'params' => $userfieldsparams ] = (array)$userfieldsapi->get_sql('u', true); $extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); $allnames = 'u.id ' . $userfieldsselects; $sql = "SELECT g.id AS groupid, gg.groupingid, u.id AS userid, $allnames, u.idnumber, u.username FROM {groups} g LEFT JOIN {groupings_groups} gg ON g.id = gg.groupid LEFT JOIN {groups_members} gm ON g.id = gm.groupid LEFT JOIN {user} u ON gm.userid = u.id $userfieldsjoin WHERE g.courseid = :courseid $groupwhere $groupingwhere ORDER BY g.name, $sort"; $rs = $DB->get_recordset_sql($sql, array_merge($params, $sortparams, $userfieldsparams)); foreach ($rs as $row) { $user = username_load_fields_from_object((object) [], $row, null, array_merge(['id' => 'userid', 'username', 'idnumber'], $extrafields)); if (!$row->groupingid) { $row->groupingid = OVERVIEW_GROUPING_GROUP_NO_GROUPING; } if (!array_key_exists($row->groupid, $members[$row->groupingid])) { $members[$row->groupingid][$row->groupid] = array(); } if (!empty($user->id)) { $members[$row->groupingid][$row->groupid][] = $user; } } $rs->close(); // Add 'no groupings' / 'no groups' selectors. $groupings[OVERVIEW_GROUPING_GROUP_NO_GROUPING] = (object)array( 'id' => OVERVIEW_GROUPING_GROUP_NO_GROUPING, 'formattedname' => $strnogrouping, ); $groups[OVERVIEW_NO_GROUP] = (object)array( 'id' => OVERVIEW_NO_GROUP, 'courseid' => $courseid, 'idnumber' => '', 'name' => $strnogroup, 'formattedname' => $strnogroup, 'description' => '', 'descriptionformat' => FORMAT_HTML, 'enrolmentkey' => '', 'picture' => 0, 'timecreated' => 0, 'timemodified' => 0, ); // Add users who are not in a group. if ($groupid <= 0 && $groupingid <= 0) { list($esql, $params) = get_enrolled_sql($context, null, 0, true); $sql = "SELECT u.id, $allnames, u.idnumber, u.username FROM {user} u JOIN ($esql) e ON e.id = u.id LEFT JOIN ( SELECT gm.userid FROM {groups_members} gm JOIN {groups} g ON g.id = gm.groupid WHERE g.courseid = :courseid ) grouped ON grouped.userid = u.id $userfieldsjoin WHERE grouped.userid IS NULL ORDER BY $sort"; $params['courseid'] = $courseid; $nogroupusers = $DB->get_records_sql($sql, array_merge($params, $userfieldsparams)); if ($nogroupusers) { $members[OVERVIEW_GROUPING_NO_GROUP][OVERVIEW_NO_GROUP] = $nogroupusers; } } // Export groups if requested. if ($dataformat !== '') { $columnnames = array( 'grouping' => $strgrouping, 'group' => $strgroup, 'firstname' => get_string('firstname'), 'lastname' => get_string('lastname'), ); $extrafields = \core_user\fields::get_identity_fields($context, false); foreach ($extrafields as $field) { $columnnames[$field] = \core_user\fields::get_display_name($field); } $alldata = array(); // Generate file name. $shortname = format_string($course->shortname, true, array('context' => $context))."_groups"; $i = 0; foreach ($members as $gpgid => $groupdata) { if ($groupingid and $groupingid != $gpgid) { if ($groupingid > 0 || $gpgid > 0) { // Still show 'not in group' when 'no grouping' selected. continue; // Do not export. } } if ($gpgid < 0) { // Display 'not in group' for grouping id == OVERVIEW_GROUPING_NO_GROUP. if ($gpgid == OVERVIEW_GROUPING_NO_GROUP) { $groupingname = $strnotingroup; } else { $groupingname = $strnotingrouping; } } else { $groupingname = $groupings[$gpgid]->formattedname; } if (empty($groupdata)) { $alldata[$i] = array_fill_keys(array_keys($columnnames), ''); $alldata[$i]['grouping'] = $groupingname; $i++; } foreach ($groupdata as $gpid => $users) { if ($groupid and $groupid != $gpid) { continue; } if (empty($users)) { $alldata[$i] = array_fill_keys(array_keys($columnnames), ''); $alldata[$i]['grouping'] = $groupingname; $alldata[$i]['group'] = $groups[$gpid]->formattedname; $i++; } foreach ($users as $option => $user) { $alldata[$i]['grouping'] = $groupingname; $alldata[$i]['group'] = $groups[$gpid]->formattedname; $alldata[$i]['firstname'] = $user->firstname; $alldata[$i]['lastname'] = $user->lastname; foreach ($extrafields as $field) { $alldata[$i][$field] = $user->$field; } $i++; } } } \core\dataformat::download_data( $shortname, $dataformat, $columnnames, $alldata, function($record, $supportshtml) use ($extrafields) { if ($supportshtml) { foreach ($extrafields as $extrafield) { $record[$extrafield] = s($record[$extrafield]); } } return $record; }); die; } // Main page content. navigation_node::override_active_url(new moodle_url('/group/index.php', array('id'=>$courseid))); $PAGE->navbar->add(get_string('overview', 'group')); /// Print header $PAGE->set_title($strgroups); $PAGE->set_heading($course->fullname); $PAGE->set_pagelayout('standard'); echo $OUTPUT->header(); echo $OUTPUT->render_participants_tertiary_nav($course); echo $strfiltergroups; $options = array(); $options[0] = get_string('all'); foreach ($groupings as $grouping) { $options[$grouping->id] = strip_tags($grouping->formattedname); } $popupurl = new moodle_url($rooturl.'&group='.$groupid); $select = new single_select($popupurl, 'grouping', $options, $groupingid, array()); $select->label = $strgrouping; $select->formid = 'selectgrouping'; echo $OUTPUT->render($select); $options = array(); $options[0] = get_string('all'); foreach ($groups as $group) { $options[$group->id] = $group->formattedname; } $popupurl = new moodle_url($rooturl.'&grouping='.$groupingid); $select = new single_select($popupurl, 'group', $options, $groupid, array()); $select->label = $strgroup; $select->formid = 'selectgroup'; echo $OUTPUT->render($select); /// Print table $printed = false; foreach ($members as $gpgid=>$groupdata) { if ($groupingid and $groupingid != $gpgid) { if ($groupingid > 0 || $gpgid > 0) { // Still show 'not in group' when 'no grouping' selected. continue; // Do not show. } } $table = new html_table(); $table->head = array(get_string('groupscount', 'group', count($groupdata)), get_string('groupmembers', 'group'), get_string('usercount', 'group')); $table->size = array('20%', '70%', '10%'); $table->align = array('left', 'left', 'center'); $table->width = '90%'; $table->data = array(); foreach ($groupdata as $gpid=>$users) { if ($groupid and $groupid != $gpid) { continue; } $line = array(); $name = print_group_picture($groups[$gpid], $course->id, false, true, false) . $groups[$gpid]->formattedname; $description = file_rewrite_pluginfile_urls($groups[$gpid]->description, 'pluginfile.php', $context->id, 'group', 'description', $gpid); $options = new stdClass; $options->noclean = true; $options->overflowdiv = true; $line[] = $name; $viewfullnames = has_capability('moodle/site:viewfullnames', $context); $fullnames = array(); foreach ($users as $user) { $displayname = fullname($user, $viewfullnames); if ($extrafields) { $extrafieldsdisplay = []; foreach ($extrafields as $field) { $extrafieldsdisplay[] = s($user->{$field}); } $displayname .= ' (' . implode(', ', $extrafieldsdisplay) . ')'; } $fullnames[] = html_writer::link(new moodle_url('/user/view.php', ['id' => $user->id, 'course' => $course->id]), $displayname); } $line[] = implode(', ', $fullnames); $line[] = count($users); $table->data[] = $line; } if ($groupid and empty($table->data)) { continue; } if ($gpgid < 0) { // Display 'not in group' for grouping id == OVERVIEW_GROUPING_NO_GROUP. if ($gpgid == OVERVIEW_GROUPING_NO_GROUP) { echo $OUTPUT->heading($strnotingroup, 3); } else { echo $OUTPUT->heading($strnotingrouping, 3); } } else { echo $OUTPUT->heading($groupings[$gpgid]->formattedname, 3); $description = file_rewrite_pluginfile_urls($groupings[$gpgid]->description, 'pluginfile.php', $context->id, 'grouping', 'description', $gpgid); $options = new stdClass; $options->overflowdiv = true; echo $OUTPUT->box(format_text($description, $groupings[$gpgid]->descriptionformat, $options), 'generalbox boxwidthnarrow boxaligncenter'); } echo html_writer::table($table); $printed = true; } // Add buttons for exporting groups/groupings. echo $OUTPUT->download_dataformat_selector(get_string('exportgroupsgroupings', 'group'), 'overview.php', 'dataformat', [ 'id' => $courseid, 'group' => $groupid, 'grouping' => $groupingid, ]); echo $OUTPUT->footer(); grouping_customfield.php 0000644 00000002625 15215711721 0011514 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Manage grouping custom fields * * @package core_group * @author Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> * @copyright 2023 Catalyst IT Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_group\customfield\grouping_handler; use core_customfield\output\management; require_once('../config.php'); require_once($CFG->libdir . '/adminlib.php'); admin_externalpage_setup('grouping_customfield'); $output = $PAGE->get_renderer('core_customfield'); $handler = grouping_handler::create(); $outputpage = new management($handler); echo $output->header(), $output->heading(new lang_string('grouping_customfield', 'admin')), $output->render($outputpage), $output->footer(); grouping_form.php 0000644 00000011565 15215711721 0010144 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A form for creating and editing groupings. * * @copyright 2006 The Open University, N.D.Freear AT open.ac.uk, J.White AT open.ac.uk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } require_once($CFG->dirroot.'/lib/formslib.php'); /** * Grouping form class * * @copyright 2006 The Open University, N.D.Freear AT open.ac.uk, J.White AT open.ac.uk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ class grouping_form extends moodleform { /** * Form definition */ function definition() { global $USER, $CFG, $COURSE; $coursecontext = context_course::instance($COURSE->id); $mform =& $this->_form; $editoroptions = $this->_customdata['editoroptions']; $grouping = $this->_customdata['grouping']; $mform->addElement('header', 'general', get_string('general', 'form')); $mform->addElement('text','name', get_string('groupingname', 'group'),'maxlength="254" size="50"'); $mform->addRule('name', get_string('required'), 'required', null, 'server'); $mform->setType('name', PARAM_TEXT); $mform->addElement('text','idnumber', get_string('idnumbergrouping'), 'maxlength="100" size="10"'); $mform->addHelpButton('idnumber', 'idnumbergrouping'); $mform->setType('idnumber', PARAM_RAW); if (!has_capability('moodle/course:changeidnumber', $coursecontext)) { $mform->hardFreeze('idnumber'); } $mform->addElement('editor', 'description_editor', get_string('groupingdescription', 'group'), null, $editoroptions); $mform->setType('description_editor', PARAM_RAW); $handler = \core_group\customfield\grouping_handler::create(); $handler->instance_form_definition($mform, empty($grouping->id) ? 0 : $grouping->id); $handler->instance_form_before_set_data($grouping); $mform->addElement('hidden','id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'courseid'); $mform->setType('courseid', PARAM_INT); $this->add_action_buttons(); } /** * Form validation * * @param array $data * @param array $files * @return array $errors An array of validataion errors for the form. */ function validation($data, $files) { global $COURSE, $DB; $errors = parent::validation($data, $files); $name = trim($data['name']); if (isset($data['idnumber'])) { $idnumber = trim($data['idnumber']); } else { $idnumber = ''; } if ($data['id'] and $grouping = $DB->get_record('groupings', array('id'=>$data['id']))) { if (core_text::strtolower($grouping->name) != core_text::strtolower($name)) { if (groups_get_grouping_by_name($COURSE->id, $name)) { $errors['name'] = get_string('groupingnameexists', 'group', $name); } } if (!empty($idnumber) && $grouping->idnumber != $idnumber) { if (groups_get_grouping_by_idnumber($COURSE->id, $idnumber)) { $errors['idnumber']= get_string('idnumbertaken'); } } } else if (groups_get_grouping_by_name($COURSE->id, $name)) { $errors['name'] = get_string('groupingnameexists', 'group', $name); } else if (!empty($idnumber) && groups_get_grouping_by_idnumber($COURSE->id, $idnumber)) { $errors['idnumber']= get_string('idnumbertaken'); } $handler = \core_group\customfield\grouping_handler::create(); $errors = array_merge($errors, $handler->instance_form_validation($data, $files)); return $errors; } /** * Apply a logic after data is set. */ public function definition_after_data() { $groupid = $this->_form->getElementValue('id'); $handler = \core_group\customfield\grouping_handler::create(); $handler->instance_form_definition_after_data($this->_form, empty($groupid) ? 0 : $groupid); } } group_form.php 0000644 00000024410 15215711721 0007437 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A form for the creation and editing of groups. * * @copyright 2006 The Open University, N.D.Freear AT open.ac.uk, J.White AT open.ac.uk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ defined('MOODLE_INTERNAL') || die; use core_group\visibility; require_once($CFG->dirroot.'/lib/formslib.php'); /** * Group form class * * @copyright 2006 The Open University, N.D.Freear AT open.ac.uk, J.White AT open.ac.uk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ class group_form extends moodleform { /** * Definition of the form */ function definition() { global $USER, $CFG, $COURSE; $coursecontext = context_course::instance($COURSE->id); $mform =& $this->_form; $editoroptions = $this->_customdata['editoroptions']; $group = $this->_customdata['group']; $mform->addElement('header', 'general', get_string('general', 'form')); $mform->addElement('text','name', get_string('groupname', 'group'),'maxlength="254" size="50"'); $mform->addRule('name', get_string('required'), 'required', null, 'client'); $mform->setType('name', PARAM_TEXT); $mform->addElement('text','idnumber', get_string('idnumbergroup'), 'maxlength="100" size="10"'); $mform->addHelpButton('idnumber', 'idnumbergroup'); $mform->setType('idnumber', PARAM_RAW); if (!has_capability('moodle/course:changeidnumber', $coursecontext)) { $mform->hardFreeze('idnumber'); } $mform->addElement('editor', 'description_editor', get_string('groupdescription', 'group'), null, $editoroptions); $mform->setType('description_editor', PARAM_RAW); $mform->addElement('passwordunmask', 'enrolmentkey', get_string('enrolmentkey', 'group'), 'maxlength="254" size="24"', get_string('enrolmentkey', 'group')); $mform->addHelpButton('enrolmentkey', 'enrolmentkey', 'group'); $mform->setType('enrolmentkey', PARAM_RAW); $visibilityoptions = [ GROUPS_VISIBILITY_ALL => get_string('visibilityall', 'group'), GROUPS_VISIBILITY_MEMBERS => get_string('visibilitymembers', 'group'), GROUPS_VISIBILITY_OWN => get_string('visibilityown', 'group'), GROUPS_VISIBILITY_NONE => get_string('visibilitynone', 'group') ]; $mform->addElement('select', 'visibility', get_string('visibility', 'group'), $visibilityoptions); $mform->addHelpButton('visibility', 'visibility', 'group'); $mform->setType('visibility', PARAM_INT); $mform->addElement('advcheckbox', 'participation', '', get_string('participation', 'group')); $mform->addHelpButton('participation', 'participation', 'group'); $mform->setType('participation', PARAM_BOOL); $mform->setDefault('participation', 1); $mform->hideIf('participation', 'visibility', 'in', [GROUPS_VISIBILITY_OWN, GROUPS_VISIBILITY_NONE]); // Group conversation messaging. if (\core_message\api::can_create_group_conversation($USER->id, $coursecontext)) { $mform->addElement('selectyesno', 'enablemessaging', get_string('enablemessaging', 'group')); $mform->addHelpButton('enablemessaging', 'enablemessaging', 'group'); $mform->hideIf('enablemessaging', 'visibility', 'in', [GROUPS_VISIBILITY_OWN, GROUPS_VISIBILITY_NONE]); } $mform->addElement('static', 'currentpicture', get_string('currentpicture')); $mform->addElement('checkbox', 'deletepicture', get_string('delete')); $mform->setDefault('deletepicture', 0); $mform->addElement('filepicker', 'imagefile', get_string('newpicture', 'group')); $mform->addHelpButton('imagefile', 'newpicture', 'group'); $handler = \core_group\customfield\group_handler::create(); $handler->instance_form_definition($mform, empty($group->id) ? 0 : $group->id); $handler->instance_form_before_set_data($group); $mform->addElement('hidden','id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden','courseid'); $mform->setType('courseid', PARAM_INT); $this->add_action_buttons(); } /** * Extend the form definition after the data has been parsed. */ public function definition_after_data() { global $COURSE, $DB, $USER; $mform = $this->_form; $groupid = $mform->getElementValue('id'); $coursecontext = context_course::instance($COURSE->id); if ($group = $DB->get_record('groups', array('id' => $groupid))) { // If can create group conversation then get if a conversation area exists and it is enabled. if (\core_message\api::can_create_group_conversation($USER->id, $coursecontext)) { if (\core_message\api::is_conversation_area_enabled('core_group', 'groups', $groupid, $coursecontext->id)) { $mform->getElement('enablemessaging')->setSelected(1); } } // Print picture. if (!($pic = print_group_picture($group, $COURSE->id, true, true, false))) { $pic = get_string('none'); if ($mform->elementExists('deletepicture')) { $mform->removeElement('deletepicture'); } } $imageelement = $mform->getElement('currentpicture'); $imageelement->setValue($pic); } else { if ($mform->elementExists('currentpicture')) { $mform->removeElement('currentpicture'); } if ($mform->elementExists('deletepicture')) { $mform->removeElement('deletepicture'); } } if ($DB->record_exists('groups_members', ['groupid' => $groupid])) { // If the group has members, lock visibility and participation fields. /** @var MoodleQuickForm_select $visibility */ $visibility = $mform->getElement('visibility'); $visibility->freeze(); /** @var MoodleQuickForm_advcheckbox $participation */ $participation = $mform->getElement('participation'); $participation->freeze(); } $handler = core_group\customfield\group_handler::create(); $handler->instance_form_definition_after_data($this->_form, empty($groupid) ? 0 : $groupid); } /** * Form validation * * @param array $data * @param array $files * @return array $errors An array of errors */ function validation($data, $files) { global $COURSE, $DB, $CFG; $errors = parent::validation($data, $files); $name = trim($data['name']); if (isset($data['idnumber'])) { $idnumber = trim($data['idnumber']); } else { $idnumber = ''; } if ($data['id'] && $group = $DB->get_record('groups', ['id' => $data['id']])) { if ($group->name != $name) { if (groups_get_group_by_name($COURSE->id, $name)) { $errors['name'] = get_string('groupnameexists', 'group', $name); } } if (!empty($idnumber) && $group->idnumber != $idnumber) { if (groups_get_group_by_idnumber($COURSE->id, $idnumber)) { $errors['idnumber']= get_string('idnumbertaken'); } } if ($data['enrolmentkey'] != '') { $errmsg = ''; if (!empty($CFG->groupenrolmentkeypolicy) && $group->enrolmentkey !== $data['enrolmentkey'] && !check_password_policy($data['enrolmentkey'], $errmsg)) { // Enforce password policy when the password is changed. $errors['enrolmentkey'] = $errmsg; } else { // Prevent twice the same enrolment key in course groups. $sql = "SELECT id FROM {groups} WHERE id <> :groupid AND courseid = :courseid AND enrolmentkey = :key"; $params = array('groupid' => $data['id'], 'courseid' => $COURSE->id, 'key' => $data['enrolmentkey']); if ($DB->record_exists_sql($sql, $params)) { $errors['enrolmentkey'] = get_string('enrolmentkeyalreadyinuse', 'group'); } } } } else if (groups_get_group_by_name($COURSE->id, $name)) { $errors['name'] = get_string('groupnameexists', 'group', $name); } else if (!empty($idnumber) && groups_get_group_by_idnumber($COURSE->id, $idnumber)) { $errors['idnumber']= get_string('idnumbertaken'); } else if ($data['enrolmentkey'] != '') { $errmsg = ''; if (!empty($CFG->groupenrolmentkeypolicy) && !check_password_policy($data['enrolmentkey'], $errmsg)) { // Enforce password policy. $errors['enrolmentkey'] = $errmsg; } else if ($DB->record_exists('groups', array('courseid' => $COURSE->id, 'enrolmentkey' => $data['enrolmentkey']))) { // Prevent the same enrolment key from being used multiple times in course groups. $errors['enrolmentkey'] = get_string('enrolmentkeyalreadyinuse', 'group'); } } $handler = \core_group\customfield\group_handler::create(); $errors = array_merge($errors, $handler->instance_form_validation($data, $files)); return $errors; } /** * Get editor options for this form * * @return array An array of options */ function get_editor_options() { return $this->_customdata['editoroptions']; } } tabs.php 0000644 00000003035 15215711721 0006211 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Prints navigation tabs * * @package core_group * @copyright 2010 Petr Skoda (http://moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $row = array(); $row[] = new tabobject('groups', new moodle_url('/group/index.php', array('id' => $courseid)), get_string('groups')); $row[] = new tabobject('groupings', new moodle_url('/group/groupings.php', array('id' => $courseid)), get_string('groupings', 'group')); $row[] = new tabobject('overview', new moodle_url('/group/overview.php', array('id' => $courseid)), get_string('overview', 'group')); echo '<div class="groupdisplay">'; echo $OUTPUT->tabtree($row, $currenttab); echo '</div>'; tests/behat/groups_import.feature 0000644 00000017405 15215711721 0013270 0 ustar 00 @core @core_group @_file_upload Feature: Importing of groups and groupings In order to import groups and grouping As a teacher I need to upload a file and verify groups and groupings can be imported Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | @javascript Scenario: Import groups and groupings as teacher Given I log in as "teacher1" And I am on the "Course 1" "groups" page And I press "Import groups" When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager And I press "Import groups" And I press "Continue" Then I should see "group-id-1" And I should see "group-id-2" And I should see "group-id-1-duplicate" And I should see "group-noid-1" And I should see "group-noid-2" # Group messaging should have been enabled for group-id-1. And I set the field "groups" to "group-id-1" And I press "Edit group settings" And I should see "Yes" in the "Group messaging" "select" And I press "Cancel" # Group messaging should not have been enabled for group-id-2. And I set the field "groups" to "group-id-2" And I press "Edit group settings" And I should see "No" in the "Group messaging" "select" And I press "Cancel" # Check groupings And I set the field "Participants tertiary navigation" to "Groupings" And I should see "Grouping-1" And I should see "Grouping-2" And I should see "Grouping-3" And I should see "group-id-1" in the "Grouping-1" "table_row" And I should see "group-id-2" in the "Grouping-2" "table_row" And I should see "group-noid-2" in the "Grouping-2" "table_row" And I should see "group-id-1-duplicate" in the "Grouping-3" "table_row" And I should see "group-noid-1" in the "Grouping-3" "table_row" @javascript Scenario: Import groups with idnumber when the user has proper permissions for the idnumber field Given I log in as "teacher1" And I am on the "Course 1" "groups" page And I press "Import groups" When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager And I press "Import groups" Then I should see "Group group-id-1 added successfully" And I should see "Group group-id-2 added successfully" And I should see "group-id-1-duplicate: Group \"group-id-1\" with an idnumber of \"group-id-1\" already exists for this course" And I should see "Group group-id-1-duplicate added successfully" And I should see "Group group-noid-1 added successfully" And I should see "Group group-noid-2 added successfully" And I press "Continue" And I set the field "groups" to "group-id-1" And I press "Edit group settings" And the field "id_idnumber" matches value "group-id-1" And I press "Cancel" And I set the field "groups" to "group-id-2" And I press "Edit group settings" And the field "id_idnumber" matches value "group-id-2" And I press "Cancel" And I set the field "groups" to "group-id-1-duplicate" And I press "Edit group settings" And the field "id_idnumber" matches value "" And I press "Cancel" And I set the field "groups" to "group-noid-1" And I press "Edit group settings" And the field "id_idnumber" matches value "" And I press "Cancel" And I set the field "groups" to "group-noid-2" And I press "Edit group settings" And the field "id_idnumber" matches value "" And I press "Cancel" @javascript Scenario: Import groups with idnumber when the user does not have proper permissions for the idnumber field Given I log in as "admin" And I am on the "Course 1" "permissions" page And I override the system permissions of "Teacher" role with: | moodle/course:changeidnumber | Prevent | And I log out And I log in as "teacher1" And I am on the "Course 1" "groups" page And I press "Import groups" When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager And I press "Import groups" And I press "Continue" Then I set the field "groups" to "group-id-1" And I press "Edit group settings" And the field "id_idnumber" matches value "" And I press "Cancel" And I set the field "groups" to "group-id-2" And I press "Edit group settings" And the field "id_idnumber" matches value "" And I press "Cancel" And I set the field "groups" to "group-id-1-duplicate" And I press "Edit group settings" And the field "id_idnumber" matches value "" And I press "Cancel" And I set the field "groups" to "group-noid-1" And I press "Edit group settings" And the field "id_idnumber" matches value "" And I press "Cancel" And I set the field "groups" to "group-noid-2" And I press "Edit group settings" And the field "id_idnumber" matches value "" And I press "Cancel" @javascript Scenario: Import groups into multiple courses as a teacher Given I log in as "teacher1" And I am on the "Course 1" "groups" page And I press "Import groups" When I upload "group/tests/fixtures/groups_import_multicourse.csv" file to "Import" filemanager And I press "Import groups" Then I should see "Group group7 added successfully" And I should see "Unknown course named \"C-non-existing\"" And I should see "Group group8 added successfully" And I should not see "group-will-not-be-created" And I should see "Group group9 added successfully" And I should see "Group group10 added successfully" And I press "Continue" And I should see "group10" And I should see "group7" And I should see "group8" And I should not see "group9" And I should not see "group-will-not-be-created" And I am on the "Course 2" "groups" page And I should see "group9" And I should not see "group-will-not-be-created" And I should not see "group7" And I should not see "group8" And I should not see "group10" And I log out @javascript Scenario: Import groups with custom field Given the following "custom field categories" exist: | name | component | area | itemid | | Category for group1 | core_group | group | 0 | | Category for grouping1 | core_group | grouping | 0 | And the following "custom fields" exist: | name | category | type | shortname | | Test Field1 | Category for group1 | text | groupfield1 | | Test Field2 | Category for grouping1 | text | groupingfield1 | And I log in as "teacher1" And I am on the "Course 1" "groups" page And I press "Import groups" When I upload "group/tests/fixtures/groups_import_with_customfield.csv" file to "Import" filemanager And I press "Import groups" Then I should see "Group Group1 added successfully" And I should see "Group Group2 added successfully" And I should see "Grouping Grouping1 added successfully" And I press "Continue" And I set the field "groups" to "Group1 (0)" And I press "Edit group settings" And the field "Test Field1" matches value "Group1-Custom" And I press "Cancel" And I set the field "groups" to "Group2 (0)" And I press "Edit group settings" And the field "Test Field1" matches value "Group2-Custom" And I press "Cancel" And I am on the "Course 1" "groupings" page Then I should see "Grouping1" And I click on "Edit" "link" in the "Grouping1" "table_row" And the field "Test Field2" matches value "Grouping1-Custom" tests/behat/custom_fields.feature 0000644 00000006003 15215711721 0013207 0 ustar 00 @core @core_group Feature: Custom profile fields in groups In order to organize participants into groups As a teacher I need to be able to view and search on custom profile fields Background: Given the following "custom profile fields" exist: | datatype | shortname | name | param2 | | text | species | Species | 255 | And the following "users" exist: | username | firstname | lastname | profile_field_species | email | | user1 | Robin | Hood | fox | email1@example.org | | user2 | Little | John | bear | email2@example.org | And the following "courses" exist: | shortname | fullname | | C1 | Course 1 | And the following "course enrolments" exist: | user | course | role | | user1 | C1 | manager | | user2 | C1 | manager | And the following "groups" exist: | name | course | idnumber | | Canines | C1 | G1 | And the following "group members" exist: | user | group | | user1 | G1 | Given the following config values are set as admin: | showuseridentity | username,profile_field_species | @javascript Scenario: Check the custom profile fields show up and can be searched on When I am logged in as "admin" And I am on the "Course 1" "groups" page # Check the Overview page. And I set the field "Participants tertiary navigation" to "Overview" And "Robin Hood (user1, fox)" "text" should exist in the "Canines" "table_row" And "Little John (user2, bear)" "text" should exist in the "No group" "table_row" # Check the groups page. And I set the field "Participants tertiary navigation" to "Groups" And I set the field "groups" to "Canines" And I should see "Robin Hood (user1, fox)" And I should not see "Little John (user2, bear)" # Check the members page. And I press "Add/remove users" And I should see "Robin Hood (user1, fox)" And I should see "Little John (user2, bear)" And I set the field "addselect" to "Little John (user2, bear)" And I press "Add" And I should see "Robin Hood (user1, fox)" And I should see "Little John (user2, bear)" And I set the field "Search" in the "#existingcell" "css_element" to "fox" And I wait "1" seconds And I should see "Robin Hood (user1, fox)" And I should not see "Little John (user2, bear)" And I set the field "Search" in the "#existingcell" "css_element" to "" And I wait "1" seconds And I set the field "removeselect" to "Little John (user2, bear)" And I press "Remove" And I set the field "removeselect" to "Robin Hood (user1, fox)" And I press "Remove" And I should see "Robin Hood (user1, fox)" And I should see "Little John (user2, bear)" And I set the field "Search" in the "#potentialcell" "css_element" to "bear" And I wait "1" seconds And I should see "Little John (user2, bear)" And I should not see "Robin Hood (user1, fox)" tests/behat/overview.feature 0000644 00000022410 15215711721 0012215 0 ustar 00 @core @core_group Feature: Group overview In order to view an overview of the groups As a teacher I need to visit the group overview page Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student0 | Student | 0 | student0@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | | student4 | Student | 4 | student4@example.com | | student5 | Student | 5 | student5@example.com | | student6 | Student | 6 | student6@example.com | | student7 | Student | 7 | student7@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student0 | C1 | student | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | | student4 | C1 | student | | student5 | C1 | student | | student6 | C1 | student | | student7 | C1 | student | And the following "groups" exist: | name | course | idnumber | | Group 1 | C1 | G1 | | Group 2 | C1 | G2 | | Group 3 | C1 | G3 | | Group 4 | C1 | G4 | And the following "group members" exist: | user | group | | student0 | G1 | | student1 | G1 | | student2 | G2 | | student3 | G3 | | student4 | G3 | | student5 | G4 | And the following "groupings" exist: | name | course | idnumber | | Grouping 1 | C1 | GG1 | | Grouping 2 | C1 | GG2 | And the following "grouping groups" exist: | grouping | group | | GG1 | G1 | | GG1 | G2 | | GG2 | G2 | | GG2 | G3 | Scenario: Filter the overview in various different ways Given I am on the "Course 1" "groups overview" page logged in as "teacher1" # Grouping All and Group All filter When I select "All" from the "Grouping" singleselect And I select "All" from the "group" singleselect # Following groups should exist in groupings. Then the group overview should include groups "Group 1, Group 2" in grouping "Grouping 1" And the group overview should include groups "Group 2,Group 3" in grouping "Grouping 2" And the group overview should include groups "Group 4" in grouping "Not in a grouping" And the group overview should include groups "No group" in grouping "Not in a group" # Following members should exit in group. And "Student 0" "text" should exist in the "Group 1" "table_row" And "Student 1" "text" should exist in the "Group 1" "table_row" And "Student 2" "text" should exist in the "Group 2" "table_row" And "Student 3" "text" should exist in the "Group 3" "table_row" And "Student 4" "text" should exist in the "Group 3" "table_row" And "Student 5" "text" should exist in the "Group 4" "table_row" And "Student 6" "text" should exist in the "No group" "table_row" And "Student 7" "text" should exist in the "No group" "table_row" # Grouping 1 and Group All filter And I select "Grouping 1" from the "Grouping" singleselect And I select "All" from the "group" singleselect # Following groups should exist in groupings. And the group overview should include groups "Group 1, Group 2" in grouping "Grouping 1" # Following groups should not exits And "Group 3" "table_row" should not exist And "No group" "table_row" should not exist # Following members should exit in group. And "Student 0" "text" should exist in the "Group 1" "table_row" And "Student 1" "text" should exist in the "Group 1" "table_row" And "Student 2" "text" should exist in the "Group 2" "table_row" # Following members should not exit in group. And I should not see "Student 3" And I should not see "Student 4" And I should not see "Student 5" And I should not see "Student 6" And I should not see "Student 7" # Grouping 2 and Group All filter And I select "Grouping 2" from the "Grouping" singleselect And I select "All" from the "group" singleselect # Following groups should exist in groupings. And the group overview should include groups "Group 2, Group 3" in grouping "Grouping 2" # Following groups should not exits And "Group 1" "table_row" should not exist And "No group" "table_row" should not exist # Following members should exit in group. And "Student 2" "text" should exist in the "Group 2" "table_row" And "Student 3" "text" should exist in the "Group 3" "table_row" And "Student 4" "text" should exist in the "Group 3" "table_row" # Following members should not exit in group. And I should not see "Student 0" And I should not see "Student 1" And I should not see "Student 5" And I should not see "Student 6" And I should not see "Student 7" # No grouping and Group All filter And I select "No grouping" from the "Grouping" singleselect And I select "All" from the "group" singleselect # Following groups should exist in groupings. And the group overview should include groups "Group 4" in grouping "Not in a grouping" And the group overview should include groups "No group" in grouping "Not in a group" # Following groups should not exits And "Group 1" "table_row" should not exist And "Group 2" "table_row" should not exist And "Group 3" "table_row" should not exist # Following members should exit in group. And "Student 5" "text" should exist in the "Group 4" "table_row" And "Student 6" "text" should exist in the "No group" "table_row" And "Student 7" "text" should exist in the "No group" "table_row" # Following members should not exit in group. And I should not see "Student 0" And I should not see "Student 1" And I should not see "Student 2" And I should not see "Student 3" And I should not see "Student 4" # Grouping All and Group 1 filter And I select "All" from the "Grouping" singleselect And I select "Group 1" from the "group" singleselect # Following groups should exist in groupings. And the group overview should include groups "Group 1" in grouping "Grouping 1" # Following groups should not exits And "Group 2" "table_row" should not exist And "Group 3" "table_row" should not exist And "Group 4" "table_row" should not exist And "No group" "table_row" should not exist # Following members should exit in group. And "Student 0" "text" should exist in the "Group 1" "table_row" And "Student 1" "text" should exist in the "Group 1" "table_row" # Following members should not exit in group. And I should not see "Student 2" And I should not see "Student 3" And I should not see "Student 4" And I should not see "Student 5" And I should not see "Student 6" And I should not see "Student 7" # Grouping All and Group 2 filter And I select "All" from the "Grouping" singleselect And I select "Group 2" from the "group" singleselect # Following groups should exist in groupings. And the group overview should include groups "Group 2" in grouping "Grouping 1" And the group overview should include groups "Group 2" in grouping "Grouping 2" # Following groups should not exits And "Group 1" "table_row" should not exist And "Group 3" "table_row" should not exist And "Group 4" "table_row" should not exist And "No group" "table_row" should not exist # Following members should exit in group. And "Student 2" "text" should exist in the "Group 2" "table_row" # Following members should not exit in group. And I should not see "Student 0" And I should not see "Student 1" And I should not see "Student 3" And I should not see "Student 4" And I should not see "Student 5" And I should not see "Student 6" And I should not see "Student 7" # Grouping All and No group filter And I select "All" from the "Grouping" singleselect And I select "No group" from the "group" singleselect # Following groups should exist in groupings. And the group overview should include groups "No group" in grouping "Not in a group" # Following groups should not exits And "Group 1" "table_row" should not exist And "Group 2" "table_row" should not exist And "Group 3" "table_row" should not exist And "Group 4" "table_row" should not exist # Following members should exit in group. And "Student 6" "text" should exist in the "No group" "table_row" And "Student 7" "text" should exist in the "No group" "table_row" # Following members should not exit in group. And I should not see "Student 0" And I should not see "Student 1" And I should not see "Student 2" And I should not see "Student 3" And I should not see "Student 4" And I should not see "Student 5" tests/behat/group_description_picture.feature 0000644 00000013540 15215711721 0015645 0 ustar 00 @core @core_group Feature: The description and picture of a group can be viewed by students and teachers In order to view the description and picture of a group As a teacher I need to create groups and add descriptions and picture to them. Background: Given the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | @javascript @_file_upload Scenario: A student can see the group description and picture when visible groups are set. Teachers can see group details. Given I am on the "Course 1" "course editing" page logged in as "teacher1" And I set the following fields to these values: | Group mode | Visible groups | And I press "Save and display" And I am on the "Course 1" "groups" page And I press "Create group" And I set the following fields to these values: | Group name | Group A | | Group description | Description for Group A | # Upload group picture And I upload "lib/tests/fixtures/gd-logo.png" file to "New picture" filemanager And I press "Save changes" And I press "Create group" And I set the following fields to these values: | Group name | Group B | And I press "Save changes" And I add "Student 1 (student1@example.com)" user to "Group A" group members And I add "Student 2 (student2@example.com)" user to "Group B" group members And I navigate to course participants And I click on "Student 1" "link" in the "participants" "table" And I click on "Group A" "link" And I should see "Description for Group A" # As teacher, confirm that group picture is displayed And "//img[@class='grouppicture']" "xpath_element" should exist And ".groupinfobox" "css_element" should exist And I set the field "type" in the "Filter 1" "fieldset" to "Groups" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group B" And I click on "Apply filters" "button" And I click on "Student 2" "link" in the "participants" "table" And I click on "Group B" "link" And I should see "Student 2" in the "participants" "table" And ".groupinfobox" "css_element" should not exist When I am on the "Course 1" course page logged in as student1 And I navigate to course participants And I click on "Student 1" "link" in the "participants" "table" And I click on "Group A" "link" # As student, confirm that group description and picture is displayed Then I should see "Description for Group A" And "//img[@class='grouppicture']" "xpath_element" should exist And I am on the "Course 1" course page logged in as student2 And I navigate to course participants And I click on "Student 2" "link" in the "participants" "table" And I click on "Group B" "link" And I should see "Student 2" in the "participants" "table" And ".groupinfobox" "css_element" should not exist @javascript @_file_upload Scenario: A student can not see the group description and picture when separate groups are set. Teachers can see group details. Given I am on the "Course 1" "course editing" page logged in as "teacher1" And I set the following fields to these values: | Group mode | Separate groups | And I press "Save and display" And I am on the "Course 1" "groups" page And I press "Create group" And I set the following fields to these values: | Group name | Group A | | Group description | Description for Group A | # Upload group picture And I upload "lib/tests/fixtures/gd-logo.png" file to "New picture" filemanager And I press "Save changes" And I press "Create group" And I set the following fields to these values: | Group name | Group B | And I press "Save changes" And I add "Student 1 (student1@example.com)" user to "Group A" group members And I add "Student 2 (student2@example.com)" user to "Group B" group members And I navigate to course participants And I click on "Student 1" "link" in the "participants" "table" And I click on "Group A" "link" And I should see "Description for Group A" # As teacher, confirm that group picture is displayed And "//img[@class='grouppicture']" "xpath_element" should exist And ".groupinfobox" "css_element" should exist And I set the field "type" in the "Filter 1" "fieldset" to "Groups" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group B" And I click on "Apply filters" "button" And I click on "Student 2" "link" in the "participants" "table" And I click on "Group B" "link" And ".groupinfobox" "css_element" should not exist When I am on the "Course 1" course page logged in as student1 And I navigate to course participants And I click on "Student 1" "link" in the "participants" "table" And I click on "Group A" "link" And I should see "Student 1" in the "participants" "table" # As student, confirm that group description and picture are not displayed Then I should not see "Description for Group A" And "//img[@class='grouppicture']" "xpath_element" should not exist And ".groupinfobox" "css_element" should not exist When I am on the "Course 1" course page logged in as student2 And I navigate to course participants And I click on "Student 2" "link" in the "participants" "table" And I click on "Group B" "link" And I should see "Student 2" in the "participants" "table" And ".groupinfobox" "css_element" should not exist tests/behat/id_uniqueness.feature 0000644 00000004555 15215711721 0013234 0 ustar 00 @core @core_group Feature: Uniqueness of Group ID number In order to create unique groups and groupings As a teacher I need to create groups with unique identificators Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | Scenario: Group ID number uniqueness Given I am on the "Course 1" "groups" page logged in as "teacher1" And I press "Create group" And I set the following fields to these values: | Group name | Group 1 | | Group ID number | G1 | And I press "Save changes" When I press "Create group" And I set the following fields to these values: | Group name | Group 2 | | Group ID number | G1 | And I press "Save changes" Then I should see "This ID number is already taken" And I set the following fields to these values: | Group ID number | G2 | And I press "Save changes" And I set the field "groups" to "Group 1 (0)" And I press "Edit group settings" And I set the following fields to these values: | Group ID number | G2 | And I press "Save changes" And I should see "This ID number is already taken" And I press "Cancel" Scenario: Grouping ID number uniqueness Given I am on the "Course 1" "groupings" page logged in as "teacher1" And I press "Create grouping" And I set the following fields to these values: | Grouping name | Grouping 1 | | Grouping ID number | GG1 | And I press "Save changes" When I press "Create grouping" And I set the following fields to these values: | Grouping name | Grouping 2 | | Grouping ID number | GG1 | And I press "Save changes" Then I should see "This ID number is already taken" And I set the following fields to these values: | Grouping ID number | GG2 | And I press "Save changes" And I click on "Edit" "link" in the "Grouping 1" "table_row" And I set the following fields to these values: | Grouping ID number | GG2 | And I press "Save changes" And I should see "This ID number is already taken" And I press "Cancel" tests/behat/update_groups.feature 0000644 00000016233 15215711721 0013236 0 ustar 00 @core @core_group Feature: Automatic updating of groups and groupings In order to check the expected results occur when updating groups and groupings in different scenarios As a teacher I need to create groups and groupings under different scenarios and check that the expected result occurs when attempting to update them. Background: Given the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" And I am on the "Course 1" "groups" page And I press "Create group" And I set the following fields to these values: | Group name | Group (without ID) | And I press "Save changes" And I press "Create group" And I set the following fields to these values: | Group name | Group (with ID) | | Group ID number | An ID | And I press "Save changes" And I set the field "Participants tertiary navigation" to "Groupings" And I press "Create grouping" And I set the following fields to these values: | Grouping name | Grouping (without ID) | And I press "Save changes" And I press "Create grouping" And I set the following fields to these values: | Grouping name | Grouping (with ID) | | Grouping ID number | An ID | And I press "Save changes" And I set the field "Participants tertiary navigation" to "Groups" @javascript Scenario: Update groups and groupings with ID numbers Given I set the field "groups" to "Group (with ID)" And I press "Edit group settings" And the field "idnumber" matches value "An ID" And I set the following fields to these values: | Group name | Group (with ID) (updated) | | Group ID number | An ID (updated) | When I press "Save changes" Then I should see "Group (with ID) (updated)" And I set the field "groups" to "Group (with ID) (updated)" And I press "Edit group settings" And the field "idnumber" matches value "An ID (updated)" And I press "Save changes" And I set the field "Participants tertiary navigation" to "Groupings" And I click on "Edit" "link" in the "Grouping (with ID)" "table_row" And the field "idnumber" matches value "An ID" And I set the following fields to these values: | Grouping name | Grouping (with ID) (updated) | | Grouping ID number | An ID (updated) | And I press "Save changes" And I should see "Grouping (with ID) (updated)" And I click on "Edit" "link" in the "Grouping (with ID) (updated)" "table_row" And the field "idnumber" matches value "An ID (updated)" @javascript @skip_chrome_zerosize Scenario: Update groups and groupings with ID numbers without the 'moodle/course:changeidnumber' capability Given the following "role capability" exists: | role | editingteacher | | moodle/course:changeidnumber | prevent | And I log in as "teacher1" And I am on the "Course 1" "groups" page And I set the field "groups" to "Group (with ID)" When I press "Edit group settings" Then the "idnumber" "field" should be readonly And the field "idnumber" matches value "An ID" And I set the following fields to these values: | Group name | Group (with ID) (updated) | And I press "Save changes" And I should see "Group (with ID) (updated)" And I set the field "groups" to "Group (with ID) (updated)" And I press "Edit group settings" And the "idnumber" "field" should be readonly And the field "idnumber" matches value "An ID" And I press "Save changes" And I set the field "Participants tertiary navigation" to "Groupings" And I click on "Edit" "link" in the "Grouping (with ID)" "table_row" And the "idnumber" "field" should be readonly And the field "idnumber" matches value "An ID" And I set the following fields to these values: | Grouping name | Grouping (with ID) (updated) | And I press "Save changes" And I should see "Grouping (with ID) (updated)" And I click on "Edit" "link" in the "Grouping (with ID) (updated)" "table_row" And the "idnumber" "field" should be readonly And the field "idnumber" matches value "An ID" @javascript Scenario: Update groups with enrolment key Given the following "courses" exist: | fullname | shortname | | Course 2 | C2 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C2 | editingteacher | And I log out And I log in as "teacher1" And I am on the "Course 1" "groups" page And I set the field "groups" to "Group (with ID)" And I press "Edit group settings" And I set the following fields to these values: | Enrolment key | badpasswd | When I press "Save changes" Then I should see "Passwords must have at least 1 digit(s)" And I set the following fields to these values: | Enrolment key | Abcdef-1 | And I press "Save changes" And I set the field "groups" to "Group (with ID)" And I press "Edit group settings" And I press "Save changes" And I should not see "This enrolment key is already used for another group." And I set the field "groups" to "Group (without ID)" And I press "Edit group settings" And I set the following fields to these values: | Enrolment key | Abcdef-1 | And I press "Save changes" And I should see "This enrolment key is already used for another group." And I set the following fields to these values: | Enrolment key | Abcdef-2 | And I press "Save changes" And I should not see "This enrolment key is already used for another group." And I am on the "Course 2" "groups" page And I press "Create group" And I set the following fields to these values: | Group name | Group A | And I press "Save changes" And I should not see "This enrolment key is already used for another group." And I set the field "groups" to "Group A" And I press "Edit group settings" And I set the following fields to these values: | Enrolment key | Abcdef-1 | And I press "Save changes" And I should not see "This enrolment key is already used for another group." @javascript Scenario: Visibility and Participation settings are locked once a group has members Given I set the field "groups" to "Group (with ID)" And I press "Edit group settings" And "visibility" "select" should exist And the field "Group membership visibility" matches value "Visible" And the "participation" "checkbox" should be enabled And the field "Show group in dropdown menu for activities in group mode" matches value "1" When the following "group members" exist: | user | group | | teacher1 | An ID | And I reload the page Then "visibility" "select" should not exist And "Visible" "text" should exist And the "participation" "checkbox" should be disabled And the field "Show group in dropdown menu for activities in group mode" matches value "1" tests/behat/role_visibility.feature 0000644 00000004514 15215711721 0013564 0 ustar 00 @core @core_group Feature: Test role visibility for the groups management page In order to control access As an admin I need to control which roles can see each other Background: Set up some groups Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "users" exist: | username | firstname | lastname | email | | learner1 | Learner | 1 | learner1@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | | manager1 | Manager | 1 | manager1@example.com | And the following "course enrolments" exist: | user | course | role | | learner1 | C1 | student | | teacher1 | C1 | editingteacher | | manager1 | C1 | manager | And the following "groups" exist: | name | course | idnumber | | Group 1 | C1 | G1 | And the following "group members" exist: | user | group | | learner1 | G1 | | teacher1 | G1 | | manager1 | G1 | Scenario: Check the default roles are visible Given I log in as "manager1" And I am on the "Course 1" "groups" page When I set the field "groups" to "Group 1 (3)" And I press "Show members for group" Then "optgroup[label='No roles']" "css_element" should not exist in the "#members" "css_element" And "optgroup[label='Student']" "css_element" should exist in the "#members" "css_element" And "optgroup[label='Teacher']" "css_element" should exist in the "#members" "css_element" And "optgroup[label='Manager']" "css_element" should exist in the "#members" "css_element" And I log out Scenario: Do not allow managers to view any roles and check they are hidden Given I log in as "teacher1" And I am on the "Course 1" "groups" page When I set the field "groups" to "Group 1 (3)" And I press "Show members for group" Then "optgroup[label='No roles']" "css_element" should exist in the "#members" "css_element" And "optgroup[label='Student']" "css_element" should exist in the "#members" "css_element" And "optgroup[label='Teacher']" "css_element" should exist in the "#members" "css_element" And "optgroup[label='Manager']" "css_element" should not exist in the "#members" "css_element" And I log out tests/behat/group_customfields.feature 0000644 00000006550 15215711721 0014273 0 ustar 00 @core @core_group @core_customfield @javascript Feature: Add and use group custom fields In order to store an extra information about groups As an admin I need to create group customs fields and be able to populate them on group creation Background: Given the following "custom field categories" exist: | name | component | area | itemid | | Category for group1 | core_group | group | 0 | | Category for grouping1 | core_group | grouping | 0 | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | Scenario: Create a new group custom field and use the field for a new group When I log in as "admin" And I navigate to "Courses > Groups > Group custom fields" in site administration Then I should see "Category for group1" And I click on "Add a new custom field" "link" And I click on "Short text" "link" And I set the following fields to these values: | Name | Test field | | Short name | testfield | And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue" Then the following should exist in the "generaltable" table: | Custom field | Short name | Type | | Test field | testfield | Short text | Then I log in as "teacher1" And I am on the "Course 1" "groups" page And I press "Create group" Then I should see "Category for group1" And I should see "Test field" And I set the following fields to these values: | Group name | My new group | | Test field | Custom field text | And I press "Save changes" Then the "groups" select box should contain "My new group (0)" And I set the field "groups" to "My new group (0)" And I press "Edit group settings" And the field "Test field" matches value "Custom field text" Scenario: Create a new grouping custom field and use the field for a new grouping When I log in as "admin" And I navigate to "Courses > Groups > Grouping custom fields" in site administration Then I should see "Category for grouping1" And I click on "Add a new custom field" "link" And I click on "Short text" "link" And I set the following fields to these values: | Name | Test field | | Short name | testfield | And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue" Then the following should exist in the "generaltable" table: | Custom field | Short name | Type | | Test field | testfield | Short text | Then I log in as "teacher1" And I am on the "Course 1" "groupings" page And I press "Create grouping" Then I should see "Category for grouping1" And I should see "Test field" And I set the following fields to these values: | Grouping name | My new grouping | | Test field | Custom field text | And I press "Save changes" Then I should see "My new grouping" And I click on "Edit" "link" in the "My new grouping" "table_row" And the field "Test field" matches value "Custom field text" tests/behat/behat_groups.php 0000644 00000007237 15215711721 0012177 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Behat groups-related steps definitions. * * @package core_group * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; /** * Groups-related steps definitions. * * @package core_group * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_groups extends behat_base { /** * Add the specified user to the group. You should be in the groups page when running this step. The user should be specified like "Firstname Lastname (user@example.com)". * * @Given /^I add "(?P<user_fullname_string>(?:[^"]|\\")*)" user to "(?P<group_name_string>(?:[^"]|\\")*)" group members$/ * @throws ElementNotFoundException Thrown by behat_base::find * @param string $username * @param string $groupname */ public function i_add_user_to_group_members($userfullname, $groupname) { // Select the group in the select. $this->execute('behat_forms::i_set_the_field_to', [get_string('groups', 'core'), $this->escape($groupname)]); // Press "Add/remove users". $this->execute('behat_general::i_click_on', [get_string('adduserstogroup', 'group'), "button"]); // Select the user. $this->execute('behat_forms::i_set_the_field_to', ["addselect", $this->escape($userfullname)]); // Click add button. $this->execute('behat_general::i_click_on', [get_string('add', 'core'), "button"]); // Returning to the main groups page. $this->execute('behat_general::i_click_on', [get_string('backtogroups', 'group'), "button"]); } /** * A single or comma-separated list of groups expected within a grouping, on group overview page. * * @Given /^the group overview should include groups "(?P<groups_string>(?:[^"]|\\")*)" in grouping "(?P<grouping_string>(?:[^"]|\\")*)"$/ * @param string $groups one or comma seperated list of groups. * @param string $grouping grouping in which all group should be present. */ public function the_groups_overview_should_include_groups_in_grouping($groups, $grouping) { $groups = array_map('trim', explode(',', $groups)); foreach ($groups as $groupname) { // Find the table after the H3 containing the grouping name, then look for the group name in the first column. $xpath = "//h3[normalize-space(.) = '{$grouping}']/following-sibling::div[contains(@class, 'table-responsive')]" . "/table//tr//td[contains(concat(' ', normalize-space(@class), ' '), ' c0 ')][normalize-space(.) = '{$groupname}' ]"; $this->execute('behat_general::should_exist', array($xpath, 'xpath_element')); } } } tests/behat/auto_creation.feature 0000644 00000021024 15215711721 0013203 0 ustar 00 @core @core_group Feature: Automatic creation of groups In order to quickly create groups As a teacher I need to create groups automatically and allocate them in groupings if necessary Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | | student4 | Student | 4 | student4@example.com | | student5 | Student | 5 | student5@example.com | | student6 | Student | 6 | student6@example.com | | student7 | Student | 7 | student7@example.com | | student8 | Student | 8 | student8@example.com | | student9 | Student | 9 | student9@example.com | | student10 | Student | 10 | student10@example.com | | suspendedstudent11 | Suspended student | 11 | suspendedstudent11@example.com | And the following "course enrolments" exist: | user | course | role | status | | teacher1 | C1 | editingteacher | 0 | | student1 | C1 | student | 0 | | student2 | C1 | student | 0 | | student3 | C1 | student | 0 | | student4 | C1 | student | 0 | | student5 | C1 | student | 0 | | student6 | C1 | student | 0 | | student7 | C1 | student | 0 | | student8 | C1 | student | 0 | | student9 | C1 | student | 0 | | student10 | C1 | student | 0 | | suspendedstudent11 | C1 | student | 1 | And I log in as "teacher1" And I am on the "Course 1" "groups" page When I press "Auto-create groups" And I expand all fieldsets @javascript Scenario: Split automatically the course users in groups and add the groups to a new grouping Given I set the following fields to these values: | Auto create based on | Number of groups | | Group/member count | 2 | | Grouping of auto-created groups | New grouping | | Grouping name | Grouping name | And I press "Preview" Then I should see "Group members" And I should see "User count" And I should see "Group A" And I should see "Group B" And I press "Submit" And the "groups" select box should contain "Group A (5)" And the "groups" select box should contain "Group B (5)" # Check that group messaging is not enabled for the auto-created groups. And I set the field "groups" to "Group A" And I press "Edit group settings" And I should see "No" in the "Group messaging" "select" And I press "Cancel" And I set the field "groups" to "Group B" And I press "Edit group settings" And I should see "No" in the "Group messaging" "select" And I press "Cancel" # Check groupings. And I set the field "Participants tertiary navigation" to "Groupings" And I should see "Grouping name" And I click on "Show groups in grouping" "link" in the "Grouping name" "table_row" And the "removeselect" select box should contain "Group A" And the "removeselect" select box should contain "Group B" @javascript Scenario: Split automatically the course users in groups based on group member count Given I set the following fields to these values: | Auto create based on | Members per group | | Group/member count | 4 | | Grouping of auto-created groups | New grouping | | Grouping name | Grouping name | | Allocate members | Alphabetically by last name, first name | And I press "Preview" Then the following should exist in the "generaltable" table: | Groups (3) | Group members | User count (10) | | Group A | Student 1 (student1@example.com) | 4 | | Group B | Student 5 (student5@example.com) | 4 | | Group C | Student 9 (student9@example.com) | 2 | And I set the field "Prevent last small group" to "1" And I press "Preview" And I should see "Group A" in the ".generaltable" "css_element" And I should see "Group B" in the ".generaltable" "css_element" And I should see "5" in the "Group A" "table_row" And I should see "5" in the "Group B" "table_row" @javascript Scenario: Split automatically the course users in groups that are not in groups Given I press "Cancel" And I press "Create group" And I set the following fields to these values: | Group name | Group 1 | And I press "Save changes" And I press "Create group" And I set the following fields to these values: | Group name | Group 2 | And I press "Save changes" When I add "Student 1" user to "Group 1" group members And I add "Student 2" user to "Group 1" group members And I add "Student 3" user to "Group 2" group members And I add "Student 4" user to "Group 2" group members And I press "Auto-create groups" And I expand all fieldsets And I set the field "Auto create based on" to "Number of groups" And I set the field "Group/member count" to "2" And I set the field "Grouping of auto-created groups" to "No grouping" And I set the field "Ignore users in groups" to "1" And I press "Submit" And the "groups" select box should contain "Group A (3)" And the "groups" select box should contain "Group B (3)" @javascript Scenario: Split users into groups based on existing groups or groupings Given I set the following fields to these values: | Naming scheme | Group @ | | Auto create based on | Number of groups | | Group/member count | 2 | | Grouping of auto-created groups | No grouping | And I press "Submit" And I press "Auto-create groups" And I set the following fields to these values: | Naming scheme | Test @ | | Auto create based on | Number of groups | | Group/member count | 2 | | groupid | Group A | | Grouping of auto-created groups | New grouping | | Grouping name | Sub Grouping | And I press "Submit" And the "groups" select box should contain "Test A (3)" And the "groups" select box should contain "Test B (2)" And I press "Auto-create groups" And I set the following fields to these values: | Naming scheme | Test # | | Auto create based on | Number of groups | | Group/member count | 2 | | Select members from grouping | Sub Grouping | | Grouping of auto-created groups | No grouping | And I press "Submit" And the "groups" select box should contain "Test 1 (3)" And the "groups" select box should contain "Test 2 (2)" Scenario: Exclude suspended users when auto-creating groups Given I set the field "Include only active enrolments" to "1" And I set the field "Auto create based on" to "Members per group" When I set the field "Group/member count" to "11" And I press "Preview" Then I should not see "Suspended Student 11" Scenario: Include suspended users when auto-creating groups Given I set the field "Include only active enrolments" to "0" And I set the field "Auto create based on" to "Members per group" When I set the field "Group/member count" to "11" And I press "Preview" Then I should see "Suspended student 11 (suspendedstudent11@example.com)" Scenario: Do not display 'Include only active enrolments' if user does not have the 'moodle/course:viewsuspendedusers' capability Given I log out And the following "role capability" exists: | role | editingteacher | | moodle/course:viewsuspendedusers | prevent | And I log in as "teacher1" And I am on the "Course 1" "groups" page When I press "Auto-create groups" Then I should not see "Include only active enrolments" And I set the field "Group/member count" to "11" And I press "Preview" And I should not see "Suspended Student 11" @javascript Scenario: Auto-create groups with group messaging Given I set the following fields to these values: | Naming scheme | Group @ | | Auto create based on | Number of groups | | Group/member count | 2 | | Grouping of auto-created groups | No grouping | | Group messaging | Yes | And I press "Submit" And I set the field "groups" to "Group A" When I press "Edit group settings" Then I should see "Yes" in the "Group messaging" "select" And I press "Cancel" And I set the field "groups" to "Group B" And I press "Edit group settings" And I should see "Yes" in the "Group messaging" "select" tests/behat/backup_restore_groups.feature 0000644 00000005523 15215711721 0014764 0 ustar 00 @core @core_group Feature: Backup and restore a course containing groups In order to transfer groups to another course As a teacher I want to backup and restore a course retaining the groups Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | numsections | | Course 1 | C1 | topics | 1 | 3 | And the following "users" exist: | username | firstname | lastname | | teacher1 | Teacher | Teacher | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "groups" exist: | name | course | idnumber | visibility | participation | | Visible/Participation | C1 | VP | 0 | 1 | | Only visible to members/Participation | C1 | MP | 1 | 1 | | Only see own membership | C1 | O | 2 | 0 | | Not visible | C1 | N | 3 | 0 | | Visible/Non-Participation | C1 | VN | 0 | 0 | | Only visible to members/Non-Participation | C1 | MN | 1 | 0 | And the following config values are set as admin: | enableasyncbackup | 0 | And I log in as "admin" And I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into a new course using this options: | Schema | Course name | Restored course | @javascript Scenario Outline: Check restored groups Given I am on the "Restored course copy 1" "groups" page logged in as teacher1 When I set the field "Groups" to "<group>" And I press "Edit group settings" Then the following fields match these values: | Group ID number | <idnumber> | | Group membership visibility | <visibility> | | Show group in dropdown menu for activities in group mode | <participation> | Examples: | group | idnumber | visibility | participation | | Visible/Participation | VP | 0 | 1 | | Only visible to members/Participation | MP | 1 | 1 | | Only see own membership | O | 2 | 0 | | Not visible | N | 3 | 0 | | Visible/Non-Participation | VN | 0 | 0 | | Only visible to members/Non-Participation | MN | 1 | 0 | tests/behat/delete_groups.feature 0000644 00000005610 15215711721 0013213 0 ustar 00 @core @core_group Feature: Automatic deletion of groups and groupings In order to check the expected results occur when deleting groups and groupings in different scenarios As a teacher I need to create groups and groupings under different scenarios and check that the expected result occurs when attempting to delete them. Background: Given the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "groups" exist: | course | name | idnumber | | C1 | Group (without ID) | | | C1 | Group (with ID) | An ID | And the following "groupings" exist: | course | name | idnumber | | C1 | Grouping (without ID) | | | C1 | Grouping (with ID) | An ID | And I log in as "teacher1" And I am on the "Course 1" "groups" page logged in as "teacher1" @javascript Scenario: Delete groups and groupings with and without ID numbers Given I set the field "groups" to "Group (without ID) (0)" And I press "Delete" And I press "Yes" Then the "groups" select box should not contain "Group (without ID) (0)" And I set the field "groups" to "Group (with ID) (0)" And I press "Delete" And I press "Yes" And the "groups" select box should not contain "Group (with ID) (0)" And I set the field "Participants tertiary navigation" to "Groupings" And I click on "Delete" "link" in the "Grouping (without ID)" "table_row" And I press "Yes" And I should not see "Grouping (without ID)" And I click on "Delete" "link" in the "Grouping (with ID)" "table_row" And I press "Yes" And I should not see "Grouping (with ID)" @javascript @skip_chrome_zerosize Scenario: Delete groups and groupings with and without ID numbers without the 'moodle/course:changeidnumber' capability Given the following "role capability" exists: | role | editingteacher | | moodle/course:changeidnumber | prevent | And I am on the "Course 1" "groups" page When I set the field "groups" to "Group (with ID) (0)" Then the "Delete" "button" should be disabled And I set the field "groups" to "Group (without ID) (0)" And I press "Delete" And I press "Yes" And I should not see "Group (without ID)" And I set the field "Participants tertiary navigation" to "Groupings" And "Delete" "link" should not exist in the "Grouping (with ID)" "table_row" And I click on "Delete" "link" in the "Grouping (without ID)" "table_row" And I press "Yes" And I should not see "Grouping (without ID)" tests/behat/create_groups.feature 0000644 00000015741 15215711721 0013222 0 ustar 00 @core @core_group Feature: Organize students into groups In order to organize course activities in groups As a teacher I need to group students @javascript Scenario: Assign students to groups Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student0 | Student | 0 | student0@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student0 | C1 | student | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | And I log in as "teacher1" And I am on the "Course 1" "groups" page And I press "Create group" And I set the following fields to these values: | Group name | Group 1 | And I press "Save changes" And I press "Create group" And I set the following fields to these values: | Group name | Group 2 | And I press "Save changes" When I add "Student 0 (student0@example.com)" user to "Group 1" group members And I add "Student 1 (student1@example.com)" user to "Group 1" group members And I add "Student 2 (student2@example.com)" user to "Group 2" group members And I add "Student 3 (student3@example.com)" user to "Group 2" group members Then I set the field "groups" to "Group 1 (2)" And the "members" select box should contain "Student 0 (student0@example.com)" And the "members" select box should contain "Student 1 (student1@example.com)" And the "members" select box should not contain "Student 2 (student2@example.com)" And the "members" select box should not contain "Student 3 (student3@example.com)" And I set the field "groups" to "Group 2 (2)" And the "members" select box should contain "Student 2 (student2@example.com)" And the "members" select box should contain "Student 3 (student3@example.com)" And the "members" select box should not contain "Student 0 (student0@example.com)" And the "members" select box should not contain "Student 1 (student1@example.com)" And I navigate to course participants And I set the field "type" in the "Filter 1" "fieldset" to "Groups" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group 1" And I click on "Apply filters" "button" And I should see "Student 0" And I should see "Student 1" And I should not see "Student 2" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group 2" And I click on "Apply filters" "button" And I should see "Student 2" And I should see "Student 3" And I should not see "Student 0" @javascript Scenario: Assign students to groups with site user identity configured Given the following "courses" exist: | fullname | shortname | groupmode | | Course 1 | C1 | 1 | And the following "users" exist: | username | firstname | lastname | email | country | | teacher | Teacher | 1 | teacher@example.com | GB | | student | Student | 1 | student@example.com | DE | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | | student | C1 | student | And the following config values are set as admin: | showuseridentity | email,country | And I log in as "teacher" And I am on the "Course 1" "groups" page And I press "Create group" And I set the following fields to these values: | Group name | Group 1 | And I press "Save changes" When I add "Student 1 (student@example.com, DE)" user to "Group 1" group members And I set the field "groups" to "Group 1 (1)" Then the "members" select box should contain "Student 1 (student@example.com\, DE)" # Non-AJAX version of the groups page. And I press "Add/remove users" And I press "Back to groups" And the "members" select box should contain "Student 1 (student@example.com\, DE)" Scenario: Create groups and groupings without the 'moodle/course:changeidnumber' capability Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "role capability" exists: | role | editingteacher | | moodle/course:changeidnumber | prevent | And I log in as "teacher1" And I am on the "Course 1" "groups" page When I press "Create group" Then the "idnumber" "field" should be readonly And I set the following fields to these values: | Group name | The greatest group that never existed | And I press "Save changes" And I should see "The greatest group that never existed" And I am on the "Course 1" "groupings" page And I press "Create grouping" And the "idnumber" "field" should be readonly And I set the following fields to these values: | Grouping name | Not the greatest grouping, but it's ok! | And I press "Save changes" And I should see "Not the greatest grouping, but it's ok!" Scenario: Create groups with enrolment key Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | | Course 2 | C2 | 0 | 1 | And I log in as "admin" And I am on the "Course 1" "groups" page When I press "Create group" And I set the following fields to these values: | Group name | Group A | | Enrolment key | badpasswd | And I press "Save changes" And I should see "Passwords must have at least 1 digit(s)" And I set the following fields to these values: | Group name | Group A | | Enrolment key | Abcdef-1 | And I press "Save changes" And I press "Create group" And I set the following fields to these values: | Group name | Group B | | Enrolment key | Abcdef-1 | And I press "Save changes" Then I should see "This enrolment key is already used for another group." And I set the following fields to these values: | Enrolment key | Abcdef-2 | And I press "Save changes" And the "groups" select box should contain "Group B (0)" And I am on the "Course 2" "groups" page And I press "Create group" And I set the following fields to these values: | Group name | Group A | | Enrolment key | Abcdef-1 | And I should not see "This enrolment key is already used for another group." tests/behat/private_groups.feature 0000644 00000027212 15215711721 0013425 0 ustar 00 @core @core_group Feature: Private groups As a teacher In order to organise students into groups while protecting their privacy I want to define groups that are not visible to all students Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | numsections | | Course 1 | C1 | topics | 1 | 3 | And the following "users" exist: | username | firstname | lastname | | teacher1 | Teacher | Teacher | | student1 | Student | 1 | | student2 | Student | 2 | | student3 | Student | 3 | | student4 | Student | 4 | | student5 | Student | 5 | | student6 | Student | 6 | | student7 | Student | 7 | | student8 | Student | 8 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | | student4 | C1 | student | | student5 | C1 | student | | student6 | C1 | student | | student7 | C1 | student | | student8 | C1 | student | And the following "groups" exist: | name | course | idnumber | visibility | participation | | Visible/Participation | C1 | VP | 0 | 1 | | Only visible to members/Participation | C1 | MP | 1 | 1 | | Only see own membership | C1 | O | 2 | 0 | | Not visible | C1 | N | 3 | 0 | | Visible/Non-Participation | C1 | VN | 0 | 0 | | Only visible to members/Non-Participation | C1 | MN | 1 | 0 | And the following "group members" exist: | user | group | | student1 | VP | | student1 | VN | | student2 | MP | | student2 | MN | | student3 | O | | student4 | N | | student5 | VP | | student5 | VN | | student6 | MP | | student6 | MN | | student7 | O | | student8 | N | Scenario: Participants in "Visible" groups see their membership and other members: Given I am on the "C1" "enrolled users" page logged in as "student1" Then the following should exist in the "participants" table: | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | No groups | | Student 3 | No groups | | Student 4 | No groups | | Student 5 | Visible/Non-Participation, Visible/Participation | | Student 6 | No groups | | Student 7 | No groups | | Student 8 | No groups | Scenario: Participants in "Only visible to members" groups see their membership and other members, plus "Visible" Given I am on the "C1" "enrolled users" page logged in as "student2" Then the following should exist in the "participants" table: | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | Only visible to members/Non-Participation, Only visible to members/Participation | | Student 3 | No groups | | Student 4 | No groups | | Student 5 | Visible/Non-Participation, Visible/Participation | | Student 6 | Only visible to members/Non-Participation, Only visible to members/Participation | | Student 7 | No groups | | Student 8 | No groups | Scenario: Participants in "Only see own membership" groups see their membership but not other members, plus "Visible" Given I am on the "C1" "enrolled users" page logged in as "student3" Then the following should exist in the "participants" table: | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | No groups | | Student 3 | Only see own membership | | Student 4 | No groups | | Student 5 | Visible/Non-Participation, Visible/Participation | | Student 6 | No groups | | Student 7 | No groups | | Student 8 | No groups | Scenario: Participants in "Not visible" groups do not see that group, do see "Visible" Given I am on the "C1" "enrolled users" page logged in as "student4" Then the following should exist in the "participants" table: | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | No groups | | Student 3 | No groups | | Student 4 | No groups | | Student 5 | Visible/Non-Participation, Visible/Participation | | Student 6 | No groups | | Student 7 | No groups | | Student 8 | No groups | Scenario: View participants list as a teacher: Given I am on the "C1" "enrolled users" page logged in as "teacher1" Then the following should exist in the "participants" table: | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | Only visible to members/Non-Participation, Only visible to members/Participation | | Student 3 | Only see own membership | | Student 4 | Not visible | | Student 5 | Visible/Non-Participation, Visible/Participation | | Student 6 | Only visible to members/Non-Participation, Only visible to members/Participation | | Student 7 | Only see own membership | | Student 8 | Not visible | @javascript Scenario: Filtering by "Only see own membership" groups should not show other members. Given I am on the "C1" "enrolled users" page logged in as "student3" When I set the field "type" to "Groups" And I set the field "Type or select..." to "Only see own membership" And I click on "Apply filters" "button" Then the following should exist in the "participants" table: | First name | Groups | | Student 3 | Only see own membership | And the following should not exist in the "participants" table: | First name | Groups | | Student 7 | No groups | @javascript Scenario: Filtering by "No group" should show all users whose memberships I cannot see Given I am on the "C1" "enrolled users" page logged in as "student3" When I set the field "type" to "Groups" And I set the field "Type or select..." to "No group" And I click on "Apply filters" "button" Then the following should exist in the "participants" table: | First name | Groups | | Student 2 | No groups | | Student 4 | No groups | | Student 6 | No groups | | Student 7 | No groups | | Student 8 | No groups | @javascript Scenario: Filtering by not a member of "Only see own membership" groups I am a member of should show everyone except me Given I am on the "C1" "enrolled users" page logged in as "student3" When I set the field "Match" in the "Filter 1" "fieldset" to "None" And I set the field "type" to "Groups" And I set the field "Type or select..." to "Only see own membership" And I click on "Apply filters" "button" Then the following should exist in the "participants" table: | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | No groups | | Student 4 | No groups | | Student 5 | Visible/Non-Participation, Visible/Participation | | Student 6 | No groups | | Student 7 | No groups | | Student 8 | No groups | @javascript Scenario: Filtering by not a member of "No group" should only show users whose memberships I can see Given I am on the "C1" "enrolled users" page logged in as "student3" When I set the field "Match" in the "Filter 1" "fieldset" to "None" And I set the field "type" to "Groups" And I set the field "Type or select..." to "No group" And I click on "Apply filters" "button" Then the following should exist in the "participants" table: | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 3 | Only see own membership | | Student 5 | Visible/Non-Participation, Visible/Participation | tests/behat/bulk_messaging.feature 0000644 00000004716 15215711721 0013352 0 ustar 00 @core @core_group Feature: Bulk update group messaging status In order to update group messaging settings in bulk As a teacher I need to be able to select the groups and update their messaging settings using the buttons provided. Background: Given the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "groups" exist: | course | name | idnumber | | C1 | Group-A-Test | GA | | C1 | Group-B-Test | GB | And I am on the "Course 1" "groups" page logged in as "teacher1" @javascript Scenario: Bulk enable messaging in groups Given I set the field "groups" to "Group-A-Test (0)" And I press "Edit group settings" And I set the field "id_enablemessaging" to "0" And I press "Save changes" And I wait until the page is ready And the field "groups" matches value "Group-A-Test (0)" And I press "Enable messaging" And I wait until the page is ready And I should see "Successfully enabled messaging in 1 group(s)" And I set the field "groups" to "Group-A-Test (0)" And I press "Edit group settings" Then the field "id_enablemessaging" matches value "1" @javascript Scenario: Bulk disable messaging in groups Given I set the field "groups" to "Group-A-Test (0)" And I press "Edit group settings" And I set the field "id_enablemessaging" to "1" And I press "Save changes" And I wait until the page is ready And the field "groups" matches value "Group-A-Test (0)" And I press "Disable messaging" And I wait until the page is ready And I should see "Successfully disabled messaging in 1 group(s)" And I set the field "groups" to "Group-A-Test (0)" And I press "Edit group settings" Then the field "id_enablemessaging" matches value "0" @javascript Scenario: Messaging buttons are enabled when a group is selected Given I set the field "groups" to "Group-A-Test (0)" Then the field "groups" matches value "Group-A-Test (0)" And the "Enable messaging" "button" should be enabled And the "Disable messaging" "button" should be enabled tests/lib_test.php 0000644 00000136711 15215711721 0010237 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Unit tests for group lib. * * @package core_group * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_group; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/group/lib.php'); require_once($CFG->dirroot . '/lib/grouplib.php'); /** * Group lib testcase. * * @package core_group * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class lib_test extends \advanced_testcase { public function test_member_added_event(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $this->getDataGenerator()->enrol_user($user->id, $course->id); $sink = $this->redirectEvents(); groups_add_member($group->id, $user->id, 'mod_workshop', '123'); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\group_member_added', $event); $this->assertEquals($user->id, $event->relateduserid); $this->assertEquals(\context_course::instance($course->id), $event->get_context()); $this->assertEquals($group->id, $event->objectid); $url = new \moodle_url('/group/members.php', array('group' => $event->objectid)); $this->assertEquals($url, $event->get_url()); } public function test_member_removed_event(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->getDataGenerator()->create_group_member(array('userid' => $user->id, 'groupid' => $group->id)); $sink = $this->redirectEvents(); groups_remove_member($group->id, $user->id); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\group_member_removed', $event); $this->assertEquals($user->id, $event->relateduserid); $this->assertEquals(\context_course::instance($course->id), $event->get_context()); $this->assertEquals($group->id, $event->objectid); $url = new \moodle_url('/group/members.php', array('group' => $event->objectid)); $this->assertEquals($url, $event->get_url()); } public function test_group_created_event(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $sink = $this->redirectEvents(); $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\group_created', $event); $this->assertEquals(\context_course::instance($course->id), $event->get_context()); $this->assertEquals($group->id, $event->objectid); $url = new \moodle_url('/group/index.php', array('id' => $event->courseid)); $this->assertEquals($url, $event->get_url()); } public function test_grouping_created_event(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $sink = $this->redirectEvents(); $group = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\grouping_created', $event); $this->assertEquals(\context_course::instance($course->id), $event->get_context()); $this->assertEquals($group->id, $event->objectid); $url = new \moodle_url('/group/groupings.php', array('id' => $event->courseid)); $this->assertEquals($url, $event->get_url()); } public function test_group_updated_event(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $sink = $this->redirectEvents(); $data = new \stdClass(); $data->id = $group->id; $data->courseid = $course->id; $data->name = 'Backend team'; $this->setCurrentTimeStart(); groups_update_group($data); $group = $DB->get_record('groups', array('id'=>$group->id)); // Fetch record with modified timestamp. $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertTimeCurrent($group->timemodified); $this->assertInstanceOf('\core\event\group_updated', $event); $group->name = $data->name; $this->assertEquals(\context_course::instance($course->id), $event->get_context()); $this->assertEquals($group->id, $event->objectid); $url = new \moodle_url('/group/group.php', array('id' => $event->objectid)); $this->assertEquals($url, $event->get_url()); } public function test_group_updated_event_does_not_require_names(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $sink = $this->redirectEvents(); $data = new \stdClass(); $data->id = $group->id; $data->courseid = $course->id; $this->setCurrentTimeStart(); groups_update_group($data); $group = $DB->get_record('groups', array('id'=>$group->id)); // Fetch record with modified timestamp. $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertTimeCurrent($group->timemodified); $this->assertInstanceOf('\core\event\group_updated', $event); $this->assertEquals(\context_course::instance($course->id), $event->get_context()); $this->assertEquals($group->id, $event->objectid); $url = new \moodle_url('/group/group.php', array('id' => $event->objectid)); $this->assertEquals($url, $event->get_url()); } public function test_grouping_updated_event(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $sink = $this->redirectEvents(); $data = new \stdClass(); $data->id = $grouping->id; $data->courseid = $course->id; $data->name = 'Backend team'; $this->setCurrentTimeStart(); groups_update_grouping($data); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\grouping_updated', $event); // Get the timemodified from DB for comparison with snapshot. $data->timemodified = $DB->get_field('groupings', 'timemodified', array('id'=>$grouping->id)); $this->assertTimeCurrent($data->timemodified); $this->assertEquals(\context_course::instance($course->id), $event->get_context()); $this->assertEquals($grouping->id, $event->objectid); $url = new \moodle_url('/group/grouping.php', array('id' => $event->objectid)); $this->assertEquals($url, $event->get_url()); } public function test_grouping_updated_event_does_not_require_names(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $sink = $this->redirectEvents(); $data = new \stdClass(); $data->id = $grouping->id; $data->courseid = $course->id; $this->setCurrentTimeStart(); groups_update_grouping($data); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\grouping_updated', $event); // Get the timemodified from DB for comparison with snapshot. $data->timemodified = $DB->get_field('groupings', 'timemodified', array('id'=>$grouping->id)); $this->assertTimeCurrent($data->timemodified); // Following fields were not updated so the snapshot should have them the same as in original group. $data->description = $grouping->description; $data->descriptionformat = $grouping->descriptionformat; $data->configdata = $grouping->configdata; $data->idnumber = $grouping->idnumber; $data->name = $grouping->name; $data->timecreated = $grouping->timecreated; $this->assertEquals(\context_course::instance($course->id), $event->get_context()); $this->assertEquals($grouping->id, $event->objectid); $url = new \moodle_url('/group/grouping.php', array('id' => $event->objectid)); $this->assertEquals($url, $event->get_url()); } public function test_group_deleted_event(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $sink = $this->redirectEvents(); groups_delete_group($group->id); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals(\context_course::instance($course->id), $event->get_context()); $this->assertEquals($group->id, $event->objectid); $url = new \moodle_url('/group/index.php', array('id' => $event->courseid)); $this->assertEquals($url, $event->get_url()); } public function test_grouping_deleted_event(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $group = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $sink = $this->redirectEvents(); groups_delete_grouping($group->id); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\grouping_deleted', $event); $this->assertEquals(\context_course::instance($course->id), $event->get_context()); $this->assertEquals($group->id, $event->objectid); $url = new \moodle_url('/group/groupings.php', array('id' => $event->courseid)); $this->assertEquals($url, $event->get_url()); } public function test_groups_delete_group_members(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); // Test deletion of all the users. $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id)); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group2->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user2->id))); groups_delete_group_members($course->id); $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user1->id))); $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $group2->id, 'userid' => $user1->id))); $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user2->id))); // Test deletion of a specific user. $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id)); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group2->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user2->id))); groups_delete_group_members($course->id, $user2->id); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group2->id, 'userid' => $user1->id))); $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user2->id))); } public function test_groups_remove_member(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id)); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group2->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user2->id))); groups_remove_member($group1->id, $user1->id); $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group2->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user2->id))); groups_remove_member($group1->id, $user2->id); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group2->id, 'userid' => $user1->id))); $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user2->id))); groups_remove_member($group2->id, $user1->id); $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $group2->id, 'userid' => $user1->id))); } public function test_groups_delete_groupings_groups(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group1c2 = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $grouping1 = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $grouping2 = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $grouping1c2 = $this->getDataGenerator()->create_grouping(array('courseid' => $course2->id)); $this->getDataGenerator()->create_grouping_group(array('groupingid' => $grouping1->id, 'groupid' => $group1->id)); $this->getDataGenerator()->create_grouping_group(array('groupingid' => $grouping1->id, 'groupid' => $group2->id)); $this->getDataGenerator()->create_grouping_group(array('groupingid' => $grouping2->id, 'groupid' => $group1->id)); $this->getDataGenerator()->create_grouping_group(array('groupingid' => $grouping1c2->id, 'groupid' => $group1c2->id)); $this->assertTrue($DB->record_exists('groupings_groups', array('groupid' => $group1->id, 'groupingid' => $grouping1->id))); $this->assertTrue($DB->record_exists('groupings_groups', array('groupid' => $group2->id, 'groupingid' => $grouping1->id))); $this->assertTrue($DB->record_exists('groupings_groups', array('groupid' => $group1->id, 'groupingid' => $grouping2->id))); $this->assertTrue($DB->record_exists('groupings_groups', array('groupid' => $group1c2->id, 'groupingid' => $grouping1c2->id))); groups_delete_groupings_groups($course->id); $this->assertFalse($DB->record_exists('groupings_groups', array('groupid' => $group1->id, 'groupingid' => $grouping1->id))); $this->assertFalse($DB->record_exists('groupings_groups', array('groupid' => $group2->id, 'groupingid' => $grouping1->id))); $this->assertFalse($DB->record_exists('groupings_groups', array('groupid' => $group1->id, 'groupingid' => $grouping2->id))); $this->assertTrue($DB->record_exists('groupings_groups', array('groupid' => $group1c2->id, 'groupingid' => $grouping1c2->id))); } public function test_groups_delete_groups(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group1c2 = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $grouping1 = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $grouping2 = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $user1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping1->id)); $this->assertTrue($DB->record_exists('groups', array('id' => $group1->id, 'courseid' => $course->id))); $this->assertTrue($DB->record_exists('groups', array('id' => $group2->id, 'courseid' => $course->id))); $this->assertTrue($DB->record_exists('groups', array('id' => $group1c2->id, 'courseid' => $course2->id))); $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groupings', array('id' => $grouping1->id, 'courseid' => $course->id))); $this->assertTrue($DB->record_exists('groupings_groups', array('groupid' => $group1->id, 'groupingid' => $grouping1->id))); groups_delete_groups($course->id); $this->assertFalse($DB->record_exists('groups', array('id' => $group1->id, 'courseid' => $course->id))); $this->assertFalse($DB->record_exists('groups', array('id' => $group2->id, 'courseid' => $course->id))); $this->assertTrue($DB->record_exists('groups', array('id' => $group1c2->id, 'courseid' => $course2->id))); $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $group1->id, 'userid' => $user1->id))); $this->assertTrue($DB->record_exists('groupings', array('id' => $grouping1->id, 'courseid' => $course->id))); $this->assertFalse($DB->record_exists('groupings_groups', array('groupid' => $group1->id, 'groupingid' => $grouping1->id))); } public function test_groups_delete_groupings(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $grouping1 = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $grouping2 = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $grouping1c2 = $this->getDataGenerator()->create_grouping(array('courseid' => $course2->id)); $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping1->id)); $this->assertTrue($DB->record_exists('groups', array('id' => $group1->id, 'courseid' => $course->id))); $this->assertTrue($DB->record_exists('groupings', array('id' => $grouping1->id, 'courseid' => $course->id))); $this->assertTrue($DB->record_exists('groupings', array('id' => $grouping2->id, 'courseid' => $course->id))); $this->assertTrue($DB->record_exists('groupings', array('id' => $grouping1c2->id, 'courseid' => $course2->id))); $this->assertTrue($DB->record_exists('groupings_groups', array('groupid' => $group1->id, 'groupingid' => $grouping1->id))); groups_delete_groupings($course->id); $this->assertTrue($DB->record_exists('groups', array('id' => $group1->id, 'courseid' => $course->id))); $this->assertFalse($DB->record_exists('groupings', array('id' => $grouping1->id, 'courseid' => $course->id))); $this->assertFalse($DB->record_exists('groupings', array('id' => $grouping2->id, 'courseid' => $course->id))); $this->assertTrue($DB->record_exists('groupings', array('id' => $grouping1c2->id, 'courseid' => $course2->id))); $this->assertFalse($DB->record_exists('groupings_groups', array('groupid' => $group1->id, 'groupingid' => $grouping1->id))); } /** * Test custom field for group. * @covers ::groups_create_group * @covers ::groups_get_group */ public function test_groups_with_customfield(): void { $this->resetAfterTest(); $this->setAdminUser(); $course1 = self::getDataGenerator()->create_course(); $course2 = self::getDataGenerator()->create_course(); $groupfieldcategory = self::getDataGenerator()->create_custom_field_category([ 'component' => 'core_group', 'area' => 'group', ]); $groupcustomfield = self::getDataGenerator()->create_custom_field([ 'shortname' => 'testgroupcustomfield1', 'type' => 'text', 'categoryid' => $groupfieldcategory->get('id'), ]); $groupingfieldcategory = self::getDataGenerator()->create_custom_field_category([ 'component' => 'core_group', 'area' => 'grouping', ]); $groupingcustomfield = self::getDataGenerator()->create_custom_field([ 'shortname' => 'testgroupingcustomfield1', 'type' => 'text', 'categoryid' => $groupingfieldcategory->get('id'), ]); $group1 = self::getDataGenerator()->create_group([ 'courseid' => $course1->id, 'customfield_testgroupcustomfield1' => 'Custom input for group1', ]); $group2 = self::getDataGenerator()->create_group([ 'courseid' => $course2->id, 'customfield_testgroupcustomfield1' => 'Custom input for group2', ]); $grouping1 = self::getDataGenerator()->create_grouping([ 'courseid' => $course1->id, 'customfield_testgroupingcustomfield1' => 'Custom input for grouping1', ]); $grouping2 = self::getDataGenerator()->create_grouping([ 'courseid' => $course2->id, 'customfield_testgroupingcustomfield1' => 'Custom input for grouping2', ]); $grouphandler = \core_group\customfield\group_handler::create(); $data = $grouphandler->export_instance_data_object($group1->id); $this->assertSame('Custom input for group1', $data->testgroupcustomfield1); $data = $grouphandler->export_instance_data_object($group2->id); $this->assertSame('Custom input for group2', $data->testgroupcustomfield1); $groupinghandler = \core_group\customfield\grouping_handler::create(); $data = $groupinghandler->export_instance_data_object($grouping1->id); $this->assertSame('Custom input for grouping1', $data->testgroupingcustomfield1); $data = $groupinghandler->export_instance_data_object($grouping2->id); $this->assertSame('Custom input for grouping2', $data->testgroupingcustomfield1); $group1->customfield_testgroupcustomfield1 = 'Updated input for group1'; $group2->customfield_testgroupcustomfield1 = 'Updated input for group2'; groups_update_group($group1); groups_update_group($group2); $data = $grouphandler->export_instance_data_object($group1->id); $this->assertSame('Updated input for group1', $data->testgroupcustomfield1); $data = $grouphandler->export_instance_data_object($group2->id); $this->assertSame('Updated input for group2', $data->testgroupcustomfield1); $group = groups_get_group($group1->id, '*', IGNORE_MISSING, true); $this->assertCount(1, $group->customfields); $customfield = reset($group->customfields); $this->assertSame('Updated input for group1', $customfield['value']); $grouping1->customfield_testgroupingcustomfield1 = 'Updated input for grouping1'; $grouping2->customfield_testgroupingcustomfield1 = 'Updated input for grouping2'; groups_update_grouping($grouping1); groups_update_grouping($grouping2); $data = $groupinghandler->export_instance_data_object($grouping1->id); $this->assertSame('Updated input for grouping1', $data->testgroupingcustomfield1); $data = $groupinghandler->export_instance_data_object($grouping2->id); $this->assertSame('Updated input for grouping2', $data->testgroupingcustomfield1); $grouping = groups_get_grouping($grouping1->id, '*', IGNORE_MISSING, true); $this->assertCount(1, $grouping->customfields); $customfield = reset($grouping->customfields); $this->assertSame('Updated input for grouping1', $customfield['value']); } public function test_groups_create_autogroups(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group3 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $grouping1 = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id)); $this->getDataGenerator()->create_grouping_group(array('groupid' => $group2->id, 'groupingid' => $grouping1->id)); $this->getDataGenerator()->create_grouping_group(array('groupid' => $group3->id, 'groupingid' => $grouping1->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $user4 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $this->getDataGenerator()->enrol_user($user3->id, $course->id); $this->getDataGenerator()->enrol_user($user4->id, $course->id); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $user3->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3->id, 'userid' => $user4->id)); // Test autocreate group based on all course users. $users = groups_get_potential_members($course->id); $group4 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); foreach ($users as $user) { $this->getDataGenerator()->create_group_member(array('groupid' => $group4->id, 'userid' => $user->id)); } $this->assertEquals(4, $DB->count_records('groups_members', array('groupid' => $group4->id))); // Test autocreate group based on existing group. $source = array(); $source['groupid'] = $group1->id; $users = groups_get_potential_members($course->id, 0, $source); $group5 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); foreach ($users as $user) { $this->getDataGenerator()->create_group_member(array('groupid' => $group5->id, 'userid' => $user->id)); } $this->assertEquals(2, $DB->count_records('groups_members', array('groupid' => $group5->id))); // Test autocreate group based on existing grouping. $source = array(); $source['groupingid'] = $grouping1->id; $users = groups_get_potential_members($course->id, 0, $source); $group6 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); foreach ($users as $user) { $this->getDataGenerator()->create_group_member(array('groupid' => $group6->id, 'userid' => $user->id)); } $this->assertEquals(2, $DB->count_records('groups_members', array('groupid' => $group6->id))); } /** * Test groups_create_group enabling a group conversation. */ public function test_groups_create_group_with_conversation(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course1 = $this->getDataGenerator()->create_course(); $coursecontext1 = \context_course::instance($course1->id); // Create two groups and only one group with enablemessaging = 1. $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 0)); $conversations = $DB->get_records('message_conversations', [ 'contextid' => $coursecontext1->id, 'component' => 'core_group', 'itemtype' => 'groups', 'enabled' => \core_message\api::MESSAGE_CONVERSATION_ENABLED ] ); $this->assertCount(1, $conversations); $conversation = reset($conversations); // Check groupid was stored in itemid on conversation area. $this->assertEquals($group1a->id, $conversation->itemid); $conversations = $DB->get_records('message_conversations', ['id' => $conversation->id]); $this->assertCount(1, $conversations); $conversation = reset($conversations); // Check group name was stored in conversation. $this->assertEquals($group1a->name, $conversation->name); } /** * Test groups_update_group enabling and disabling a group conversation. */ public function test_groups_update_group_conversation(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course1 = $this->getDataGenerator()->create_course(); $coursecontext1 = \context_course::instance($course1->id); // Create two groups and only one group with enablemessaging = 1. $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 0)); $conversations = $DB->get_records('message_conversations', [ 'contextid' => $coursecontext1->id, 'component' => 'core_group', 'itemtype' => 'groups', 'enabled' => \core_message\api::MESSAGE_CONVERSATION_ENABLED ] ); $this->assertCount(1, $conversations); // Check that the conversation area is created when group messaging is enabled in the course group. $group1b->enablemessaging = 1; groups_update_group($group1b); $conversations = $DB->get_records('message_conversations', [ 'contextid' => $coursecontext1->id, 'component' => 'core_group', 'itemtype' => 'groups', 'enabled' => \core_message\api::MESSAGE_CONVERSATION_ENABLED ], 'id ASC'); $this->assertCount(2, $conversations); $conversation1a = array_shift($conversations); $conversation1b = array_shift($conversations); $conversation1b = $DB->get_record('message_conversations', ['id' => $conversation1b->id]); // Check for group1b that group name was stored in conversation. $this->assertEquals($group1b->name, $conversation1b->name); $group1b->enablemessaging = 0; groups_update_group($group1b); $this->assertEquals(0, $DB->get_field("message_conversations", "enabled", ['id' => $conversation1b->id])); // Check that the name of the conversation is changed when the name of the course group is updated. $group1b->name = 'New group name'; groups_update_group($group1b); $conversation1b = $DB->get_record('message_conversations', ['id' => $conversation1b->id]); $this->assertEquals($group1b->name, $conversation1b->name); } /** * Test groups_add_member to conversation. */ public function test_groups_add_member_conversation(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course1 = $this->getDataGenerator()->create_course(); $coursecontext1 = \context_course::instance($course1->id); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); $this->getDataGenerator()->enrol_user($user3->id, $course1->id); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); // Add users to group1. $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id)); $conversation = \core_message\api::get_conversation_by_area( 'core_group', 'groups', $group1->id, $coursecontext1->id ); // Check if the users has been added to the conversation. $this->assertEquals(2, $DB->count_records('message_conversation_members', ['conversationid' => $conversation->id])); // Check if the user has been added to the conversation when the conversation is disabled. \core_message\api::disable_conversation($conversation->id); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user3->id)); $this->assertEquals(3, $DB->count_records('message_conversation_members', ['conversationid' => $conversation->id])); } /** * Test groups_remove_member to conversation. */ public function test_groups_remove_member_conversation(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course1 = $this->getDataGenerator()->create_course(); $coursecontext1 = \context_course::instance($course1->id); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); $this->getDataGenerator()->enrol_user($user3->id, $course1->id); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user3->id)); $conversation = \core_message\api::get_conversation_by_area( 'core_group', 'groups', $group1->id, $coursecontext1->id ); // Check if there are three users in the conversation. $this->assertEquals(3, $DB->count_records('message_conversation_members', ['conversationid' => $conversation->id])); // Check if after removing one member in the conversation there are two members. groups_remove_member($group1->id, $user1->id); $this->assertEquals(2, $DB->count_records('message_conversation_members', ['conversationid' => $conversation->id])); // Check if the user has been removed from the conversation when the conversation is disabled. \core_message\api::disable_conversation($conversation->id); groups_remove_member($group1->id, $user2->id); $this->assertEquals(1, $DB->count_records('message_conversation_members', ['conversationid' => $conversation->id])); } /** * Test if you enable group messaging in a group with members these are added to the conversation. */ public function test_add_members_group_updated_conversation_enabled(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course1 = $this->getDataGenerator()->create_course(); $coursecontext1 = \context_course::instance($course1->id); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); $this->getDataGenerator()->enrol_user($user3->id, $course1->id); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 0)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user3->id)); $conversation = \core_message\api::get_conversation_by_area( 'core_group', 'groups', $group1->id, $coursecontext1->id ); // No conversation should exist as 'enablemessaging' was set to 0. $this->assertFalse($conversation); // Check that the three users are in the conversation when group messaging is enabled in the course group. $group1->enablemessaging = 1; groups_update_group($group1); $conversation = \core_message\api::get_conversation_by_area( 'core_group', 'groups', $group1->id, $coursecontext1->id ); $this->assertEquals(3, $DB->count_records('message_conversation_members', ['conversationid' => $conversation->id])); } public function test_groups_get_members_by_role(): void { $this->resetAfterTest(); $this->setAdminUser(); $course1 = $this->getDataGenerator()->create_course(); $user1 = $this->getDataGenerator()->create_user(['username' => 'user1', 'idnumber' => 1]); $user2 = $this->getDataGenerator()->create_user(['username' => 'user2', 'idnumber' => 2]); $user3 = $this->getDataGenerator()->create_user(['username' => 'user3', 'idnumber' => 3]); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 0); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 1); $this->getDataGenerator()->enrol_user($user3->id, $course1->id, 1); $group1 = $this->getDataGenerator()->create_group(['courseid' => $course1->id]); $this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $user1->id]); $this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $user2->id]); $this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $user3->id]); // Test basic usage. $result = groups_get_members_by_role($group1->id, $course1->id); $this->assertEquals(1, count($result[0]->users)); $this->assertEquals(2, count($result[1]->users)); $this->assertEquals($user1->firstname, reset($result[0]->users)->firstname); $this->assertEquals($user1->username, reset($result[0]->users)->username); // Test with specified fields. $result = groups_get_members_by_role($group1->id, $course1->id, 'u.firstname, u.lastname'); $this->assertEquals(1, count($result[0]->users)); $this->assertEquals($user1->firstname, reset($result[0]->users)->firstname); $this->assertEquals($user1->lastname, reset($result[0]->users)->lastname); $this->assertEquals(false, isset(reset($result[0]->users)->username)); // Test with sorting. $result = groups_get_members_by_role($group1->id, $course1->id, 'u.username', 'u.username DESC'); $this->assertEquals(1, count($result[0]->users)); $this->assertEquals($user3->username, reset($result[1]->users)->username); $result = groups_get_members_by_role($group1->id, $course1->id, 'u.username', 'u.username ASC'); $this->assertEquals(1, count($result[0]->users)); $this->assertEquals($user2->username, reset($result[1]->users)->username); // Test with extra WHERE. $result = groups_get_members_by_role( $group1->id, $course1->id, 'u.username', null, 'u.idnumber > :number', ['number' => 2]); $this->assertEquals(1, count($result)); $this->assertEquals(1, count($result[1]->users)); $this->assertEquals($user3->username, reset($result[1]->users)->username); // Test with join. set_user_preference('reptile', 'snake', $user1); $result = groups_get_members_by_role($group1->id, $course1->id, 'u.username, up.value', null, 'up.name = :prefname', ['prefname' => 'reptile'], 'JOIN {user_preferences} up ON up.userid = u.id'); $this->assertEquals('snake', reset($result[0]->users)->value); } /** * Tests set_groups_messaging * * @covers ::set_groups_messaging */ public function test_set_groups_messaging(): void { $this->resetAfterTest(); $this->setAdminUser(); $dg = $this->getDataGenerator(); $course = $dg->create_course(); // Create some groups in the course. $groupids = []; for ($i = 0; $i < 5; $i++) { $group = new \stdClass(); $group->courseid = $course->id; $group->name = 'group-'.$i; $group->enablemessaging = 0; $groupids[] = groups_create_group($group); } // They should all initially be disabled. $alldisabledinitially = $this->check_groups_messaging_status_is($groupids, $course->id, false); $this->assertTrue($alldisabledinitially); // Enable messaging for all the groups. set_groups_messaging($groupids, true); // Check they were all enabled. $allenabled = $this->check_groups_messaging_status_is($groupids, $course->id, true); $this->assertTrue($allenabled); // Disable messaging for all the groups. set_groups_messaging($groupids, false); // Check they were all disabled. $alldisabled = $this->check_groups_messaging_status_is($groupids, $course->id, false); $this->assertTrue($alldisabled); } /** * Tests set group messaging where it doesn't exist * * @covers ::set_groups_messaging */ public function test_set_groups_messaging_doesnt_exist(): void { $this->resetAfterTest(); $this->setAdminUser(); $groupids = [-1]; $this->expectException('dml_exception'); set_groups_messaging($groupids, false); } /** * Checks the given list of groups to verify their messaging settings. * * @param array $groupids array of group ids * @param int $courseid the course the groups are in * @param bool $desired the desired setting value * @return bool true if all groups $enablemessaging setting matches the given $desired value, else false */ private function check_groups_messaging_status_is(array $groupids, int $courseid, bool $desired) { $context = \context_course::instance($courseid); foreach ($groupids as $groupid) { $conversation = \core_message\api::get_conversation_by_area( 'core_group', 'groups', $groupid, $context->id ); // An empty conversation means it has not been enabled yet. if (empty($conversation)) { $conversation = (object) [ 'enabled' => 0 ]; } if ($desired !== boolval($conversation->enabled)) { return false; } } return true; } } tests/fixtures/groups_import_with_customfield.csv 0000644 00000000410 15215711721 0016633 0 ustar 00 groupname, description, groupidnumber, groupingname, customfield_groupfield1, grouping_customfield_groupingfield1 Group1, Group1-Desc, group-id1, Grouping1, Group1-Custom, Grouping1-Custom Group2, Group1-Desc, group-id2, Grouping1, Group2-Custom, Grouping1-Custom tests/fixtures/groups_import.csv 0000644 00000000474 15215711721 0013214 0 ustar 00 groupname, description, groupidnumber,groupingname,enablemessaging group-id-1, group-id-1, group-id-1,Grouping-1,1 group-id-2, group-id-2, group-id-2,Grouping-2,0 group-id-1-duplicate, Duplicate of group-id-1, group-id-1,Grouping-3,0 group-noid-1, group-noid-1,,Grouping-3,0 group-noid-2, group-noid-2,,Grouping-2,0 tests/fixtures/groups_import_multicourse.csv 0000644 00000000145 15215711721 0015642 0 ustar 00 coursename,groupname C1,group7 C-non-existing,group-will-not-be-created C1,group8 C2,group9 ,group10 tests/privacy/provider_test.php 0000644 00000154072 15215711721 0013000 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy provider tests. * * @package core_group * @category test * @copyright 2018 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_group\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\tests\provider_testcase; use core_privacy\local\metadata\collection; use core_group\privacy\provider; use core_privacy\local\request\writer; /** * Privacy provider test for core_group. * * @copyright 2018 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** * Test for provider::get_metadata(). */ public function test_get_metadata(): void { $collection = new collection('core_group'); $newcollection = provider::get_metadata($collection); $itemcollection = $newcollection->get_collection(); $this->assertCount(2, $itemcollection); $table = array_shift($itemcollection); $this->assertEquals('groups_members', $table->get_name()); $this->assertEquals('privacy:metadata:groups', $table->get_summary()); $privacyfields = $table->get_privacy_fields(); $this->assertArrayHasKey('groupid', $privacyfields); $this->assertArrayHasKey('userid', $privacyfields); $this->assertArrayHasKey('timeadded', $privacyfields); $table = array_shift($itemcollection); $this->assertEquals('core_message', $table->get_name()); $this->assertEquals('privacy:metadata:core_message', $table->get_summary()); } /** * Test for provider::export_groups() to export manual group memberships. */ public function test_export_groups(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group3 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group4 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); // Add user1 to group1 and group2. $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $user1->id)); // Add user2 to group2 and group3. $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3->id, 'userid' => $user2->id)); $context = \context_course::instance($course->id); // Retrieve groups for user1. $this->setUser($user1); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); provider::export_groups($context, ''); $data = $writer->get_data([get_string('groups', 'core_group')]); $exportedgroups = $data->groups; // User1 belongs to group1 and group2. $this->assertEqualsCanonicalizing( [$group1->name, $group2->name], array_column($exportedgroups, 'name')); } /** * Test for provider::export_groups() to export group memberships of a component. */ public function test_export_groups_for_component(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group3 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group4 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group5 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $this->getDataGenerator()->enrol_user($user1->id, $course->id, null, 'self'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, null, 'self'); // Add user1 to group1 (via enrol_self) and group2 and group3. $this->getDataGenerator()->create_group_member( array('groupid' => $group1->id, 'userid' => $user1->id, 'component' => 'enrol_self')); $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3->id, 'userid' => $user1->id)); // Add user2 to group3 (via enrol_self) and group4. $this->getDataGenerator()->create_group_member( array('groupid' => $group3->id, 'userid' => $user2->id, 'component' => 'enrol_self')); $this->getDataGenerator()->create_group_member(array('groupid' => $group4->id, 'userid' => $user2->id)); $context = \context_course::instance($course->id); // Retrieve groups for user1. $this->setUser($user1); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); provider::export_groups($context, 'enrol_self'); $data = $writer->get_data([get_string('groups', 'core_group')]); $exportedgroups = $data->groups; // User1 only belongs to group1 via enrol_self. $this->assertCount(1, $exportedgroups); $exportedgroup = reset($exportedgroups); $this->assertEquals($group1->name, $exportedgroup->name); } /** * Test for provider::delete_groups_for_all_users() to delete manual group memberships. */ public function test_delete_groups_for_all_users(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user1->id, $course2->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course2->id); $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1b->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2b->id, 'userid' => $user2->id)); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $coursecontext1 = \context_course::instance($course1->id); provider::delete_groups_for_all_users($coursecontext1, ''); $this->assertEquals( 0, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); } /** * Test for provider::delete_groups_for_all_users() to delete group memberships of a component. */ public function test_delete_groups_for_all_users_for_component(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'self'); $this->getDataGenerator()->enrol_user($user1->id, $course2->id, null, 'self'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, null, 'self'); $this->getDataGenerator()->enrol_user($user2->id, $course2->id, null, 'self'); $this->getDataGenerator()->create_group_member( array('groupid' => $group1a->id, 'userid' => $user1->id, 'component' => 'enrol_self')); $this->getDataGenerator()->create_group_member(array('groupid' => $group1b->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member( array('groupid' => $group2a->id, 'userid' => $user1->id, 'component' => 'enrol_self')); $this->getDataGenerator()->create_group_member(array('groupid' => $group2b->id, 'userid' => $user2->id)); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $coursecontext1 = \context_course::instance($course1->id); provider::delete_groups_for_all_users($coursecontext1, 'enrol_self'); $this->assertEquals( 1, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); } /** * Test for provider::delete_groups_for_all_users() to check deleting from cache. */ public function test_delete_groups_for_all_users_deletes_cache(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group1->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group2->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $group1->id)); $this->assertEqualsCanonicalizing([[$group1->id, $group2->id]], groups_get_user_groups($course->id, $user1->id)); $this->assertEquals([[$group1->id]], groups_get_user_groups($course->id, $user2->id)); $coursecontext = \context_course::instance($course->id); provider::delete_groups_for_all_users($coursecontext, ''); $this->assertEquals([[]], groups_get_user_groups($course->id, $user1->id)); $this->assertEquals([[]], groups_get_user_groups($course->id, $user2->id)); } /** * Test for provider::delete_groups_for_user() to delete manual group memberships. */ public function test_delete_groups_for_user(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group3a = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $group3b = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user1->id, $course2->id); $this->getDataGenerator()->enrol_user($user1->id, $course3->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course2->id); $this->getDataGenerator()->enrol_user($user2->id, $course3->id); $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1b->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2b->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3b->id, 'userid' => $user2->id)); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 3, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE gm.userid = ?", [$user1->id]) ); $this->setUser($user1); $coursecontext1 = \context_course::instance($course1->id); $coursecontext2 = \context_course::instance($course2->id); $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist($user1, 'core_group', [$coursecontext1->id, $coursecontext2->id]); provider::delete_groups_for_user($approvedcontextlist, ''); $this->assertEquals( 1, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 1, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course3->id]) ); $this->assertEquals( 1, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE gm.userid = ?", [$user1->id]) ); } /** * Test for provider::delete_groups_for_user() to delete group memberships of a component. */ public function test_delete_groups_for_user_for_component(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group3a = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $group3b = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'self'); $this->getDataGenerator()->enrol_user($user1->id, $course2->id, null, 'self'); $this->getDataGenerator()->enrol_user($user1->id, $course3->id, null, 'self'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, null, 'self'); $this->getDataGenerator()->enrol_user($user2->id, $course2->id, null, 'self'); $this->getDataGenerator()->enrol_user($user2->id, $course3->id, null, 'self'); $this->getDataGenerator()->create_group_member( array('groupid' => $group1a->id, 'userid' => $user1->id, 'component' => 'enrol_self')); $this->getDataGenerator()->create_group_member( array('groupid' => $group1b->id, 'userid' => $user2->id, 'component' => 'enrol_self')); $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2b->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3b->id, 'userid' => $user2->id)); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 3, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE gm.userid = ?", [$user1->id]) ); $this->setUser($user1); $coursecontext1 = \context_course::instance($course1->id); $coursecontext2 = \context_course::instance($course2->id); $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist($user1, 'core_group', [$coursecontext1->id, $coursecontext2->id]); provider::delete_groups_for_user($approvedcontextlist, 'enrol_self'); $this->assertEquals( 1, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course3->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE gm.userid = ?", [$user1->id]) ); } /** * Test for provider::delete_groups_for_users() to delete group memberships of a component. */ public function test_delete_groups_for_users_for_component(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group3a = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $group3b = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'self'); $this->getDataGenerator()->enrol_user($user1->id, $course2->id, null, 'self'); $this->getDataGenerator()->enrol_user($user1->id, $course3->id, null, 'self'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, null, 'self'); $this->getDataGenerator()->enrol_user($user2->id, $course2->id, null, 'self'); $this->getDataGenerator()->enrol_user($user2->id, $course3->id, null, 'self'); $this->getDataGenerator()->create_group_member( array('groupid' => $group1a->id, 'userid' => $user1->id, 'component' => 'enrol_self')); $this->getDataGenerator()->create_group_member( array('groupid' => $group1b->id, 'userid' => $user2->id, 'component' => 'enrol_self')); $this->getDataGenerator()->create_group_member( array('groupid' => $group2a->id, 'userid' => $user1->id, 'component' => 'enrol_self')); $this->getDataGenerator()->create_group_member( array('groupid' => $group2b->id, 'userid' => $user2->id, 'component' => 'enrol_self')); $this->getDataGenerator()->create_group_member(array('groupid' => $group3a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3b->id, 'userid' => $user2->id)); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 3, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE gm.userid = ?", [$user1->id]) ); // Delete user1 and user2 from groups in course1. $coursecontext1 = \context_course::instance($course1->id); $approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext1, 'core_group', [$user1->id, $user2->id]); provider::delete_groups_for_users($approveduserlist, 'enrol_self'); $this->assertEquals( 0, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course3->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE gm.userid = ?", [$user1->id]) ); // Delete user1 and user2 from course3. $coursecontext3 = \context_course::instance($course3->id); $approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext3, 'core_group', [$user1->id, $user2->id]); provider::delete_groups_for_users($approveduserlist, 'enrol_self'); $this->assertEquals( 0, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course3->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE gm.userid = ?", [$user1->id]) ); } /** * Test for provider::delete_groups_for_user() to check deleting from cache. */ public function test_delete_groups_for_user_deletes_cache(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->getDataGenerator()->create_group_member(array('userid' => $user->id, 'groupid' => $group1->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user->id, 'groupid' => $group2->id)); $this->assertEqualsCanonicalizing([[$group1->id, $group2->id]], groups_get_user_groups($course->id, $user->id)); $this->setUser($user); $coursecontext = \context_course::instance($course->id); $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist($user, 'core_group', [$coursecontext->id]); provider::delete_groups_for_user($approvedcontextlist, ''); $this->assertEquals([[]], groups_get_user_groups($course->id, $user->id)); } /** * Test for provider::get_contexts_for_userid(). */ public function test_get_contexts_for_userid(): void { $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group3a = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $group3b = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user1->id, $course2->id); $this->getDataGenerator()->enrol_user($user1->id, $course3->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course2->id); $this->getDataGenerator()->enrol_user($user2->id, $course3->id); $this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group1a->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group2a->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $group1b->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $group2b->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $group3b->id)); $coursecontext1 = \context_course::instance($course1->id); $coursecontext2 = \context_course::instance($course2->id); // User1 is member of some groups in course1 and course2 + self-conversation. $contextlist = provider::get_contexts_for_userid($user1->id); $contextids = array_values($contextlist->get_contextids()); $this->assertCount(3, $contextlist); // One of the user context is the one related to self-conversation. Let's test group contexts. $this->assertContainsEquals($coursecontext1->id, $contextids); $this->assertContainsEquals($coursecontext2->id, $contextids); } /** * Test for provider::get_contexts_for_userid() when there are group memberships from other components. */ public function test_get_contexts_for_userid_component(): void { $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->getDataGenerator()->create_group_member( array( 'userid' => $user->id, 'groupid' => $group1->id )); $this->getDataGenerator()->create_group_member( array( 'userid' => $user->id, 'groupid' => $group2->id, 'component' => 'enrol_meta' )); $coursecontext1 = \context_course::instance($course1->id); // User is member of some groups in course1 and course2, // but only the membership in course1 is directly managed by core_group. $contextlist = provider::get_contexts_for_userid($user->id); $this->assertEquals($coursecontext1->id, $contextlist->get_contextids()[0]); } /** * Test for provider::export_user_data(). */ public function test_export_user_data(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group3 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group4 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); // Add user1 to group1 and group2. $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $user1->id)); // Add user2 to group2 and group3. $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3->id, 'userid' => $user2->id)); $context = \context_course::instance($course->id); $this->setUser($user1); // Export all of the data for the context. $this->export_context_data_for_user($user1->id, $context, 'core_group'); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $data = $writer->get_data([get_string('groups', 'core_group')]); $exportedgroups = $data->groups; // User1 belongs to group1 and group2. $this->assertEqualsCanonicalizing( [$group1->name, $group2->name], array_column($exportedgroups, 'name')); } /** * Test for provider::delete_data_for_all_users_in_context(). */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user1->id, $course2->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course2->id); $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1b->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2b->id, 'userid' => $user2->id)); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $coursecontext1 = \context_course::instance($course1->id); provider::delete_data_for_all_users_in_context($coursecontext1); $this->assertEquals( 0, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group3a = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $group3b = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user1->id, $course2->id); $this->getDataGenerator()->enrol_user($user1->id, $course3->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course2->id); $this->getDataGenerator()->enrol_user($user2->id, $course3->id); $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1b->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2b->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group3b->id, 'userid' => $user2->id)); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 3, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE gm.userid = ?", [$user1->id]) ); $this->setUser($user1); $coursecontext1 = \context_course::instance($course1->id); $coursecontext2 = \context_course::instance($course2->id); $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist($user1, 'core_group', [$coursecontext1->id, $coursecontext2->id]); provider::delete_data_for_user($approvedcontextlist); $this->assertEquals( 1, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 1, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $this->assertEquals( 2, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course3->id]) ); $this->assertEquals( 1, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE gm.userid = ?", [$user1->id]) ); } /** * Test for provider::delete_data_for_users(). */ public function test_delete_data_for_users(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1c = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2c = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user1->id, $course2->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course2->id); $this->getDataGenerator()->enrol_user($user3->id, $course1->id); $this->getDataGenerator()->enrol_user($user3->id, $course2->id); $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1b->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group1c->id, 'userid' => $user3->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2b->id, 'userid' => $user2->id)); $this->getDataGenerator()->create_group_member(array('groupid' => $group2c->id, 'userid' => $user3->id)); $this->assertEquals( 3, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 3, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); $coursecontext1 = \context_course::instance($course1->id); $approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext1, 'core_group', [$user1->id, $user2->id]); provider::delete_data_for_users($approveduserlist); $this->assertEquals( [$user3->id], $DB->get_fieldset_sql("SELECT gm.userid FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course1->id]) ); $this->assertEquals( 3, $DB->count_records_sql("SELECT COUNT(gm.id) FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = ?", [$course2->id]) ); } /** * Test for provider::get_users_in_context(). */ public function test_get_users_in_context(): void { $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user1->id, $course2->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course2->id); $this->getDataGenerator()->enrol_user($user3->id, $course1->id); $this->getDataGenerator()->enrol_user($user3->id, $course2->id); $this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group1a->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group2a->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $group1b->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $group2b->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user3->id, 'groupid' => $group2a->id)); $coursecontext1 = \context_course::instance($course1->id); $userlist = new \core_privacy\local\request\userlist($coursecontext1, 'core_group'); \core_group\privacy\provider::get_users_in_context($userlist); // Only user1 and user2. User3 is not member of any group in course1. $this->assertCount(2, $userlist); $this->assertEqualsCanonicalizing( [$user1->id, $user2->id], $userlist->get_userids()); } } tests/externallib_test.php 0000644 00000157521 15215711721 0012004 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group; use core_customfield\field_controller; use core_external\external_api; use core_group\customfield\group_handler; use core_group\customfield\grouping_handler; use core_group_external; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/group/externallib.php'); require_once($CFG->dirroot . '/group/lib.php'); /** * Group external PHPunit tests * * @package core_group * @category external * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.4 * @covers \core_group_external */ final class externallib_test extends externallib_advanced_testcase { /** * Create group custom field for testing. * * @return field_controller */ protected function create_group_custom_field(): field_controller { $fieldcategory = self::getDataGenerator()->create_custom_field_category([ 'component' => 'core_group', 'area' => 'group', ]); return self::getDataGenerator()->create_custom_field([ 'shortname' => 'testgroupcustomfield1', 'type' => 'text', 'categoryid' => $fieldcategory->get('id'), ]); } /** * Create grouping custom field for testing. * * @return field_controller */ protected function create_grouping_custom_field(): field_controller { $fieldcategory = self::getDataGenerator()->create_custom_field_category([ 'component' => 'core_group', 'area' => 'grouping', ]); return self::getDataGenerator()->create_custom_field([ 'shortname' => 'testgroupingcustomfield1', 'type' => 'text', 'categoryid' => $fieldcategory->get('id'), ]); } /** * Test create_groups */ public function test_create_groups(): void { global $DB; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $group1 = array(); $group1['courseid'] = $course->id; $group1['name'] = 'Group Test 1'; $group1['description'] = 'Group Test 1 description'; $group1['descriptionformat'] = FORMAT_MOODLE; $group1['enrolmentkey'] = 'Test group enrol secret phrase'; $group1['idnumber'] = 'TEST1'; $group2 = array(); $group2['courseid'] = $course->id; $group2['name'] = 'Group Test 2'; $group2['description'] = 'Group Test 2 description'; $group2['visibility'] = GROUPS_VISIBILITY_MEMBERS; $group2['participation'] = false; $group3 = array(); $group3['courseid'] = $course->id; $group3['name'] = 'Group Test 3'; $group3['description'] = 'Group Test 3 description'; $group3['idnumber'] = 'TEST1'; $group4 = array(); $group4['courseid'] = $course->id; $group4['name'] = 'Group Test 4'; $group4['description'] = 'Group Test 4 description'; // Set the required capabilities by the external function $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); // Call the external function. $groups = core_group_external::create_groups(array($group1, $group2)); // We need to execute the return values cleaning process to simulate the web service server. $groups = external_api::clean_returnvalue(core_group_external::create_groups_returns(), $groups); // Checks against DB values $this->assertEquals(2, count($groups)); foreach ($groups as $group) { $dbgroup = $DB->get_record('groups', array('id' => $group['id']), '*', MUST_EXIST); switch ($dbgroup->name) { case $group1['name']: $groupdescription = $group1['description']; $groupcourseid = $group1['courseid']; $this->assertEquals($dbgroup->descriptionformat, $group1['descriptionformat']); $this->assertEquals($dbgroup->enrolmentkey, $group1['enrolmentkey']); $this->assertEquals($dbgroup->idnumber, $group1['idnumber']); // The visibility and participation attributes were not specified, so should match the default values. $groupvisibility = GROUPS_VISIBILITY_ALL; $groupparticipation = true; break; case $group2['name']: $groupdescription = $group2['description']; $groupcourseid = $group2['courseid']; $groupvisibility = $group2['visibility']; $groupparticipation = $group2['participation']; break; default: throw new \moodle_exception('unknowgroupname'); break; } $this->assertEquals($dbgroup->description, $groupdescription); $this->assertEquals($dbgroup->courseid, $groupcourseid); $this->assertEquals($dbgroup->visibility, $groupvisibility); $this->assertEquals($dbgroup->participation, $groupparticipation); } try { $froups = core_group_external::create_groups(array($group3)); $this->fail('Exception expected due to already existing idnumber.'); } catch (\moodle_exception $e) { $this->assertInstanceOf('moodle_exception', $e); $this->assertEquals(get_string('idnumbertaken', 'error'), $e->getMessage()); } // Call without required capability $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid); $this->expectException(\required_capability_exception::class); $froups = core_group_external::create_groups(array($group4)); } /** * Test create_groups with custom fields. */ public function test_create_groups_with_customfields(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = self::getDataGenerator()->create_course(); $this->create_group_custom_field(); $group = [ 'courseid' => $course->id, 'name' => 'Create groups test (with custom fields)', 'description' => 'Description for create groups test with custom fields', 'customfields' => [ [ 'shortname' => 'testgroupcustomfield1', 'value' => 'Test group value 1', ], ], ]; $createdgroups = core_group_external::create_groups([$group]); $createdgroups = external_api::clean_returnvalue(core_group_external::create_groups_returns(), $createdgroups); $this->assertCount(1, $createdgroups); $createdgroup = reset($createdgroups); $dbgroup = $DB->get_record('groups', ['id' => $createdgroup['id']], '*', MUST_EXIST); $this->assertEquals($group['name'], $dbgroup->name); $this->assertEquals($group['description'], $dbgroup->description); $data = group_handler::create()->export_instance_data_object($createdgroup['id'], true); $this->assertEquals('Test group value 1', $data->testgroupcustomfield1); } /** * Test that creating a group with an invalid visibility value throws an exception. * * @covers \core_group_external::create_groups * @return void */ public function test_create_group_invalid_visibility(): void { $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $group1 = array(); $group1['courseid'] = $course->id; $group1['name'] = 'Group Test 1'; $group1['description'] = 'Group Test 1 description'; $group1['visibility'] = 1000; // Set the required capabilities by the external function. $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); // Call the external function. $this->expectException('invalid_parameter_exception'); core_group_external::create_groups([$group1]); } /** * Test update_groups */ public function test_update_groups(): void { global $DB; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; $group1data['description'] = 'Group Test 1 description'; $group1data['descriptionformat'] = FORMAT_MOODLE; $group1data['enrolmentkey'] = 'Test group enrol secret phrase'; $group1data['idnumber'] = 'TEST1'; $group2data = array(); $group2data['courseid'] = $course->id; $group2data['name'] = 'Group Test 2'; $group2data['description'] = 'Group Test 2 description'; $group2data['idnumber'] = 'TEST2'; // Set the required capabilities by the external function. $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); // Create the test groups. $group1 = self::getDataGenerator()->create_group($group1data); $group2 = self::getDataGenerator()->create_group($group2data); $group1data['id'] = $group1->id; unset($group1data['courseid']); $group2data['id'] = $group2->id; unset($group2data['courseid']); // No exceptions should be triggered. $group1data['idnumber'] = 'CHANGED'; core_group_external::update_groups(array($group1data)); $group2data['description'] = 'Group Test 2 description CHANGED'; $group2data['visibility'] = GROUPS_VISIBILITY_MEMBERS; core_group_external::update_groups(array($group2data)); foreach ([$group1, $group2] as $group) { $dbgroup = $DB->get_record('groups', array('id' => $group->id), '*', MUST_EXIST); switch ($dbgroup->name) { case $group1data['name']: $this->assertEquals($dbgroup->idnumber, $group1data['idnumber']); $groupdescription = $group1data['description']; // Visibility was not specified, so should match the default value. $groupvisibility = GROUPS_VISIBILITY_ALL; break; case $group2data['name']: $this->assertEquals($dbgroup->idnumber, $group2data['idnumber']); $groupdescription = $group2data['description']; $groupvisibility = $group2data['visibility']; break; default: throw new \moodle_exception('unknowngroupname'); break; } $this->assertEquals($dbgroup->description, $groupdescription); $this->assertEquals($dbgroup->visibility, $groupvisibility); } // Taken idnumber exception. $group1data['idnumber'] = 'TEST2'; try { $groups = core_group_external::update_groups(array($group1data)); $this->fail('Exception expected due to already existing idnumber.'); } catch (\moodle_exception $e) { $this->assertInstanceOf('moodle_exception', $e); $this->assertEquals(get_string('idnumbertaken', 'error'), $e->getMessage()); } // Call without required capability. $group1data['idnumber'] = 'TEST1'; $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid); $this->expectException(\required_capability_exception::class); $groups = core_group_external::update_groups(array($group1data)); } /** * Test update_groups with custom fields. */ public function test_update_groups_with_customfields(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = self::getDataGenerator()->create_course(); $this->create_group_custom_field(); $group = self::getDataGenerator()->create_group(['courseid' => $course->id]); $data = group_handler::create()->export_instance_data_object($group->id, true); $this->assertNull($data->testgroupcustomfield1); $updategroup = [ 'id' => $group->id, 'name' => $group->name, 'customfields' => [ [ 'shortname' => 'testgroupcustomfield1', 'value' => 'Test value 1', ], ], ]; core_group_external::update_groups([$updategroup]); $data = group_handler::create()->export_instance_data_object($group->id, true); $this->assertEquals('Test value 1', $data->testgroupcustomfield1); } /** * Test an exception is thrown when an invalid visibility value is passed in an update. * * @covers \core_group_external::update_groups * @return void */ public function test_update_groups_invalid_visibility(): void { $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; // Set the required capabilities by the external function. $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); // Create the test group. $group1 = self::getDataGenerator()->create_group($group1data); $group1data['id'] = $group1->id; unset($group1data['courseid']); $group1data['visibility'] = 1000; $this->expectException('invalid_parameter_exception'); core_group_external::update_groups(array($group1data)); } /** * Attempting to change the visibility of a group with members should throw an exception. * * @covers \core_group_external::update_groups * @return void */ public function test_update_groups_visibility_with_members(): void { $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; // Set the required capabilities by the external function. $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); // Create the test group and add a member. $group1 = self::getDataGenerator()->create_group($group1data); $user1 = self::getDataGenerator()->create_and_enrol($course); self::getDataGenerator()->create_group_member(['userid' => $user1->id, 'groupid' => $group1->id]); $group1data['id'] = $group1->id; unset($group1data['courseid']); $group1data['visibility'] = GROUPS_VISIBILITY_MEMBERS; $this->expectExceptionMessage('The visibility of this group cannot be changed as it currently has members.'); core_group_external::update_groups(array($group1data)); } /** * Attempting to change the participation field of a group with members should throw an exception. * * @covers \core_group_external::update_groups * @return void */ public function test_update_groups_participation_with_members(): void { $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; // Set the required capabilities by the external function. $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); // Create the test group and add a member. $group1 = self::getDataGenerator()->create_group($group1data); $user1 = self::getDataGenerator()->create_and_enrol($course); self::getDataGenerator()->create_group_member(['userid' => $user1->id, 'groupid' => $group1->id]); $group1data['id'] = $group1->id; unset($group1data['courseid']); $group1data['participation'] = false; $this->expectExceptionMessage('The participation mode of this group cannot be changed as it currently has members.'); core_group_external::update_groups(array($group1data)); } /** * Test get_groups */ public function test_get_groups(): void { global $DB; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; $group1data['description'] = 'Group Test 1 description'; $group1data['descriptionformat'] = FORMAT_MOODLE; $group1data['enrolmentkey'] = 'Test group enrol secret phrase'; $group1data['idnumber'] = 'TEST1'; $group2data = array(); $group2data['courseid'] = $course->id; $group2data['name'] = 'Group Test 2'; $group2data['description'] = 'Group Test 2 description'; $group2data['visibility'] = GROUPS_VISIBILITY_MEMBERS; $group2data['participation'] = false; $group1 = self::getDataGenerator()->create_group($group1data); $group2 = self::getDataGenerator()->create_group($group2data); // Set the required capabilities by the external function $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); // Call the external function. $groups = core_group_external::get_groups(array($group1->id, $group2->id)); // We need to execute the return values cleaning process to simulate the web service server. $groups = external_api::clean_returnvalue(core_group_external::get_groups_returns(), $groups); // Checks against DB values $this->assertEquals(2, count($groups)); foreach ($groups as $group) { $dbgroup = $DB->get_record('groups', array('id' => $group['id']), '*', MUST_EXIST); switch ($dbgroup->name) { case $group1->name: $groupdescription = $group1->description; $groupcourseid = $group1->courseid; // The visibility and participation attributes were not specified, so should match the default values. $groupvisibility = GROUPS_VISIBILITY_ALL; $groupparticipation = true; $this->assertEquals($dbgroup->descriptionformat, $group1->descriptionformat); $this->assertEquals($dbgroup->enrolmentkey, $group1->enrolmentkey); $this->assertEquals($dbgroup->idnumber, $group1->idnumber); break; case $group2->name: $groupdescription = $group2->description; $groupcourseid = $group2->courseid; $groupvisibility = $group2->visibility; $groupparticipation = $group2->participation; break; default: throw new \moodle_exception('unknowgroupname'); break; } $this->assertEquals($dbgroup->description, $groupdescription); $this->assertEquals($dbgroup->courseid, $groupcourseid); $this->assertEquals($dbgroup->visibility, $groupvisibility); $this->assertEquals($dbgroup->participation, $groupparticipation); } // Call without required capability $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid); $this->expectException(\required_capability_exception::class); $groups = core_group_external::get_groups(array($group1->id, $group2->id)); } /** * Test get_groups with customfields. */ public function test_get_groups_with_customfields(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = self::getDataGenerator()->create_course(); $this->create_group_custom_field(); $group = self::getDataGenerator()->create_group([ 'courseid' => $course->id, 'customfield_testgroupcustomfield1' => 'Test group value 1', ]); // Call the external function. $groups = core_group_external::get_groups([$group->id]); // We need to execute the return values cleaning process to simulate the web service server. $groups = external_api::clean_returnvalue(core_group_external::get_groups_returns(), $groups); $this->assertEquals(1, count($groups)); $groupresult = reset($groups); $this->assertEquals(1, count($groupresult['customfields'])); $customfield = reset($groupresult['customfields']); $this->assertEquals('testgroupcustomfield1', $customfield['shortname']); $this->assertEquals('Test group value 1', $customfield['value']); } /** * Test delete_groups */ public function test_delete_groups(): void { global $DB; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; $group1data['description'] = 'Group Test 1 description'; $group1data['descriptionformat'] = FORMAT_MOODLE; $group1data['enrolmentkey'] = 'Test group enrol secret phrase'; $group2data = array(); $group2data['courseid'] = $course->id; $group2data['name'] = 'Group Test 2'; $group2data['description'] = 'Group Test 2 description'; $group3data['courseid'] = $course->id; $group3data['name'] = 'Group Test 3'; $group3data['description'] = 'Group Test 3 description'; $group1 = self::getDataGenerator()->create_group($group1data); $group2 = self::getDataGenerator()->create_group($group2data); $group3 = self::getDataGenerator()->create_group($group3data); // Set the required capabilities by the external function $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); // Checks against DB values $groupstotal = $DB->count_records('groups', array()); $this->assertEquals(3, $groupstotal); // Call the external function. core_group_external::delete_groups(array($group1->id, $group2->id)); // Checks against DB values $groupstotal = $DB->count_records('groups', array()); $this->assertEquals(1, $groupstotal); // Call without required capability $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid); $this->expectException(\required_capability_exception::class); $froups = core_group_external::delete_groups(array($group3->id)); } /** * Test create and update groupings. * @return void */ public function test_create_update_groupings(): void { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $course = self::getDataGenerator()->create_course(); $grouping1data = array(); $grouping1data['courseid'] = $course->id; $grouping1data['name'] = 'Grouping 1 Test'; $grouping1data['description'] = 'Grouping 1 Test description'; $grouping1data['descriptionformat'] = FORMAT_MOODLE; $grouping1data['idnumber'] = 'TEST'; $grouping1 = self::getDataGenerator()->create_grouping($grouping1data); $grouping1data['name'] = 'Another group'; try { $groupings = core_group_external::create_groupings(array($grouping1data)); $this->fail('Exception expected due to already existing idnumber.'); } catch (\moodle_exception $e) { $this->assertInstanceOf('moodle_exception', $e); $this->assertEquals(get_string('idnumbertaken', 'error'), $e->getMessage()); } // No exception should be triggered. $grouping1data['id'] = $grouping1->id; $grouping1data['idnumber'] = 'CHANGED'; unset($grouping1data['courseid']); core_group_external::update_groupings(array($grouping1data)); $grouping2data = array(); $grouping2data['courseid'] = $course->id; $grouping2data['name'] = 'Grouping 2 Test'; $grouping2data['description'] = 'Grouping 2 Test description'; $grouping2data['descriptionformat'] = FORMAT_MOODLE; $grouping2data['idnumber'] = 'TEST'; $grouping2 = self::getDataGenerator()->create_grouping($grouping2data); $grouping2data['id'] = $grouping2->id; $grouping2data['idnumber'] = 'CHANGED'; unset($grouping2data['courseid']); try { $groupings = core_group_external::update_groupings(array($grouping2data)); $this->fail('Exception expected due to already existing idnumber.'); } catch (\moodle_exception $e) { $this->assertInstanceOf('moodle_exception', $e); $this->assertEquals(get_string('idnumbertaken', 'error'), $e->getMessage()); } } /** * Test create_groupings with custom fields. */ public function test_create_groupings_with_customfields(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = self::getDataGenerator()->create_course(); $this->create_grouping_custom_field(); $grouping = [ 'courseid' => $course->id, 'name' => 'Create groupings test (with custom fields)', 'description' => 'Description for create groupings test with custom fields', 'idnumber' => 'groupingidnumber1', 'customfields' => [ [ 'shortname' => 'testgroupingcustomfield1', 'value' => 'Test grouping value 1', ], ], ]; $createdgroupings = core_group_external::create_groupings([$grouping]); $createdgroupings = external_api::clean_returnvalue(core_group_external::create_groupings_returns(), $createdgroupings); $this->assertCount(1, $createdgroupings); $createdgrouping = reset($createdgroupings); $dbgroup = $DB->get_record('groupings', ['id' => $createdgrouping['id']], '*', MUST_EXIST); $this->assertEquals($grouping['name'], $dbgroup->name); $this->assertEquals($grouping['description'], $dbgroup->description); $this->assertEquals($grouping['idnumber'], $dbgroup->idnumber); $data = grouping_handler::create()->export_instance_data_object($createdgrouping['id'], true); $this->assertEquals('Test grouping value 1', $data->testgroupingcustomfield1); } /** * Test update_groups with custom fields. */ public function test_update_groupings_with_customfields(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = self::getDataGenerator()->create_course(); $this->create_grouping_custom_field(); $grouping = self::getDataGenerator()->create_grouping(['courseid' => $course->id]); $data = grouping_handler::create()->export_instance_data_object($grouping->id, true); $this->assertNull($data->testgroupingcustomfield1); $updategroup = [ 'id' => $grouping->id, 'name' => $grouping->name, 'description' => $grouping->description, 'customfields' => [ [ 'shortname' => 'testgroupingcustomfield1', 'value' => 'Test grouping value 1', ], ], ]; core_group_external::update_groupings([$updategroup]); $data = grouping_handler::create()->export_instance_data_object($grouping->id, true); $this->assertEquals('Test grouping value 1', $data->testgroupingcustomfield1); } /** * Test get_groupings */ public function test_get_groupings(): void { global $DB; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $groupingdata = array(); $groupingdata['courseid'] = $course->id; $groupingdata['name'] = 'Grouping Test'; $groupingdata['description'] = 'Grouping Test description'; $groupingdata['descriptionformat'] = FORMAT_MOODLE; $grouping = self::getDataGenerator()->create_grouping($groupingdata); // Set the required capabilities by the external function. $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); // Call the external function without specifying the optional parameter. $groupings = core_group_external::get_groupings(array($grouping->id)); // We need to execute the return values cleaning process to simulate the web service server. $groupings = external_api::clean_returnvalue(core_group_external::get_groupings_returns(), $groupings); $this->assertEquals(1, count($groupings)); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; $group1data['description'] = 'Group Test 1 description'; $group1data['descriptionformat'] = FORMAT_MOODLE; $group2data = array(); $group2data['courseid'] = $course->id; $group2data['name'] = 'Group Test 2'; $group2data['description'] = 'Group Test 2 description'; $group2data['descriptionformat'] = FORMAT_MOODLE; $group1 = self::getDataGenerator()->create_group($group1data); $group2 = self::getDataGenerator()->create_group($group2data); groups_assign_grouping($grouping->id, $group1->id); groups_assign_grouping($grouping->id, $group2->id); // Call the external function specifying that groups are returned. $groupings = core_group_external::get_groupings(array($grouping->id), true); // We need to execute the return values cleaning process to simulate the web service server. $groupings = external_api::clean_returnvalue(core_group_external::get_groupings_returns(), $groupings); $this->assertEquals(1, count($groupings)); $this->assertEquals(2, count($groupings[0]['groups'])); foreach ($groupings[0]['groups'] as $group) { $dbgroup = $DB->get_record('groups', array('id' => $group['id']), '*', MUST_EXIST); $dbgroupinggroups = $DB->get_record('groupings_groups', array('groupingid' => $groupings[0]['id'], 'groupid' => $group['id']), '*', MUST_EXIST); switch ($dbgroup->name) { case $group1->name: $groupdescription = $group1->description; $groupcourseid = $group1->courseid; break; case $group2->name: $groupdescription = $group2->description; $groupcourseid = $group2->courseid; break; default: throw new \moodle_exception('unknowgroupname'); break; } $this->assertEquals($dbgroup->description, $groupdescription); $this->assertEquals($dbgroup->courseid, $groupcourseid); } } /** * Test get_groupings with customfields. */ public function test_get_groupings_with_customfields(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = self::getDataGenerator()->create_course(); $this->create_grouping_custom_field(); $grouping = self::getDataGenerator()->create_grouping([ 'courseid' => $course->id, 'customfield_testgroupingcustomfield1' => 'Test grouping value 1', ]); $this->create_group_custom_field(); $group = self::getDataGenerator()->create_group([ 'courseid' => $course->id, 'customfield_testgroupcustomfield1' => 'Test group value 1', ]); groups_assign_grouping($grouping->id, $group->id); // Call the external function. $groupings = core_group_external::get_groupings([$grouping->id]); // We need to execute the return values cleaning process to simulate the web service server. $groupings = external_api::clean_returnvalue(core_group_external::get_groupings_returns(), $groupings); $this->assertEquals(1, count($groupings)); $groupingresult = reset($groupings); $this->assertEquals(1, count($groupingresult['customfields'])); $customfield = reset($groupingresult['customfields']); $this->assertEquals('testgroupingcustomfield1', $customfield['shortname']); $this->assertEquals('Test grouping value 1', $customfield['value']); $this->assertArrayNotHasKey('groups', $groupingresult); // Call the external function with return group parameter. $groupings = core_group_external::get_groupings([$grouping->id], true); // We need to execute the return values cleaning process to simulate the web service server. $groupings = external_api::clean_returnvalue(core_group_external::get_groupings_returns(), $groupings); $this->assertEquals(1, count($groupings)); $groupingresult = reset($groupings); $this->assertEquals(1, count($groupingresult['customfields'])); $this->assertArrayHasKey('groups', $groupingresult); $this->assertEquals(1, count($groupingresult['groups'])); $groupresult = reset($groupingresult['groups']); $this->assertEquals(1, count($groupresult['customfields'])); $customfield = reset($groupresult['customfields']); $this->assertEquals('testgroupcustomfield1', $customfield['shortname']); $this->assertEquals('Test group value 1', $customfield['value']); } /** * Test delete_groupings. */ public function test_delete_groupings(): void { global $DB; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $groupingdata1 = array(); $groupingdata1['courseid'] = $course->id; $groupingdata1['name'] = 'Grouping Test'; $groupingdata1['description'] = 'Grouping Test description'; $groupingdata1['descriptionformat'] = FORMAT_MOODLE; $groupingdata2 = array(); $groupingdata2['courseid'] = $course->id; $groupingdata2['name'] = 'Grouping Test'; $groupingdata2['description'] = 'Grouping Test description'; $groupingdata2['descriptionformat'] = FORMAT_MOODLE; $groupingdata3 = array(); $groupingdata3['courseid'] = $course->id; $groupingdata3['name'] = 'Grouping Test'; $groupingdata3['description'] = 'Grouping Test description'; $groupingdata3['descriptionformat'] = FORMAT_MOODLE; $grouping1 = self::getDataGenerator()->create_grouping($groupingdata1); $grouping2 = self::getDataGenerator()->create_grouping($groupingdata2); $grouping3 = self::getDataGenerator()->create_grouping($groupingdata3); // Set the required capabilities by the external function. $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); // Checks against DB values. $groupingstotal = $DB->count_records('groupings', array()); $this->assertEquals(3, $groupingstotal); // Call the external function. core_group_external::delete_groupings(array($grouping1->id, $grouping2->id)); // Checks against DB values. $groupingstotal = $DB->count_records('groupings', array()); $this->assertEquals(1, $groupingstotal); // Call without required capability. $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid); $this->expectException(\required_capability_exception::class); core_group_external::delete_groupings(array($grouping3->id)); } /** * Test get_groups */ public function test_get_course_user_groups(): void { global $DB; $this->resetAfterTest(true); $student1 = self::getDataGenerator()->create_user(); $student2 = self::getDataGenerator()->create_user(); $teacher = self::getDataGenerator()->create_user(); $course = self::getDataGenerator()->create_course(); $anothercourse = self::getDataGenerator()->create_course(); $emptycourse = self::getDataGenerator()->create_course(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($student1->id, $anothercourse->id, $studentrole->id); $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); $this->getDataGenerator()->enrol_user($teacher->id, $emptycourse->id, $teacherrole->id); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; $group1data['description'] = 'Group Test 1 description'; $group1data['idnumber'] = 'TEST1'; $group2data = array(); $group2data['courseid'] = $course->id; $group2data['name'] = 'Group Test 2'; $group2data['description'] = 'Group Test 2 description'; $group3data = array(); $group3data['courseid'] = $anothercourse->id; $group3data['name'] = 'Group Test 3'; $group3data['description'] = 'Group Test 3 description'; $group3data['idnumber'] = 'TEST3'; $group1 = self::getDataGenerator()->create_group($group1data); $group2 = self::getDataGenerator()->create_group($group2data); $group3 = self::getDataGenerator()->create_group($group3data); groups_add_member($group1->id, $student1->id); groups_add_member($group1->id, $student2->id); groups_add_member($group2->id, $student1->id); groups_add_member($group3->id, $student1->id); // Create a grouping. $groupingdata = array(); $groupingdata['courseid'] = $course->id; $groupingdata['name'] = 'Grouping Test'; $groupingdata['description'] = 'Grouping Test description'; $groupingdata['descriptionformat'] = FORMAT_MOODLE; $grouping = self::getDataGenerator()->create_grouping($groupingdata); // Grouping only containing group1. groups_assign_grouping($grouping->id, $group1->id); $this->setUser($student1); $groups = core_group_external::get_course_user_groups($course->id, $student1->id); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); // Check that I see my groups. $this->assertCount(2, $groups['groups']); $this->assertEquals($course->id, $groups['groups'][0]['courseid']); $this->assertEquals($course->id, $groups['groups'][1]['courseid']); // Check that I only see my groups inside the given grouping. $groups = core_group_external::get_course_user_groups($course->id, $student1->id, $grouping->id); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); // Check that I see my groups in the grouping. $this->assertCount(1, $groups['groups']); $this->assertEquals($group1->id, $groups['groups'][0]['id']); // Check optional parameters (all student 1 courses and current user). $groups = core_group_external::get_course_user_groups(); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); // Check that I see my groups in all my courses. $this->assertCount(3, $groups['groups']); $this->setUser($student2); $groups = core_group_external::get_course_user_groups($course->id, $student2->id); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); // Check that I see my groups. $this->assertCount(1, $groups['groups']); $this->assertEquals($group1data['name'], $groups['groups'][0]['name']); $this->assertEquals($group1data['description'], $groups['groups'][0]['description']); $this->assertEquals($group1data['idnumber'], $groups['groups'][0]['idnumber']); $this->setUser($teacher); $groups = core_group_external::get_course_user_groups($course->id, $student1->id); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); // Check that a teacher can see student groups in given course. $this->assertCount(2, $groups['groups']); $groups = core_group_external::get_course_user_groups($course->id, $student2->id); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); // Check that a teacher can see student groups in given course. $this->assertCount(1, $groups['groups']); $groups = core_group_external::get_course_user_groups(0, $student1->id); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); // Check that a teacher can see student groups in all the user courses if the teacher is enrolled in the course. $this->assertCount(2, $groups['groups']); // Teacher only see groups in first course. $this->assertCount(1, $groups['warnings']); // Enrolment warnings. $this->assertEquals('1', $groups['warnings'][0]['warningcode']); // Enrol teacher in second course. $this->getDataGenerator()->enrol_user($teacher->id, $anothercourse->id, $teacherrole->id); $groups = core_group_external::get_course_user_groups(0, $student1->id); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); // Check that a teacher can see student groups in all the user courses if the teacher is enrolled in the course. $this->assertCount(3, $groups['groups']); // Check permissions. $this->setUser($student1); // Student can's see other students group. $groups = core_group_external::get_course_user_groups($course->id, $student2->id); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); $this->assertCount(1, $groups['warnings']); $this->assertEquals('cannotmanagegroups', $groups['warnings'][0]['warningcode']); // Not enrolled course. $groups = core_group_external::get_course_user_groups($emptycourse->id, $student2->id); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); $this->assertCount(1, $groups['warnings']); $this->assertEquals('1', $groups['warnings'][0]['warningcode']); $this->setUser($teacher); // Check user checking not enrolled in given course. $groups = core_group_external::get_course_user_groups($emptycourse->id, $student1->id); $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups); $this->assertCount(1, $groups['warnings']); $this->assertEquals('notenrolled', $groups['warnings'][0]['warningcode']); } /** * Test get_activity_allowed_groups */ public function test_get_activity_allowed_groups(): void { global $DB; $this->resetAfterTest(true); $generator = self::getDataGenerator(); $student = $generator->create_user(); $otherstudent = $generator->create_user(); $teacher = $generator->create_user(); $course = $generator->create_course(); $othercourse = $generator->create_course(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $generator->enrol_user($student->id, $course->id, $studentrole->id); $generator->enrol_user($otherstudent->id, $othercourse->id, $studentrole->id); $generator->enrol_user($teacher->id, $course->id, $teacherrole->id); $forum1 = $generator->create_module("forum", array('course' => $course->id), array('groupmode' => VISIBLEGROUPS)); $forum2 = $generator->create_module("forum", array('course' => $othercourse->id)); $forum3 = $generator->create_module("forum", array('course' => $course->id), array('visible' => 0)); // Request data for tests. $cm1 = get_coursemodule_from_instance("forum", $forum1->id); $cm2 = get_coursemodule_from_instance("forum", $forum2->id); $cm3 = get_coursemodule_from_instance("forum", $forum3->id); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; $group1data['description'] = 'Group Test 1 description'; $group1data['idnumber'] = 'TEST1'; $group2data = array(); $group2data['courseid'] = $course->id; $group2data['name'] = 'Group Test 2'; $group2data['description'] = 'Group Test 2 description'; $group2data['idnumber'] = 'TEST2'; $group1 = $generator->create_group($group1data); $group2 = $generator->create_group($group2data); groups_add_member($group1->id, $student->id); groups_add_member($group2->id, $student->id); $this->setUser($student); // First try possible errors. try { $data = core_group_external::get_activity_allowed_groups($cm2->id); } catch (\moodle_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } try { $data = core_group_external::get_activity_allowed_groups($cm3->id); } catch (\moodle_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } // Retrieve my groups. $groups = core_group_external::get_activity_allowed_groups($cm1->id); $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups); $this->assertCount(2, $groups['groups']); $this->assertFalse($groups['canaccessallgroups']); foreach ($groups['groups'] as $group) { if ($group['name'] == $group1data['name']) { $this->assertEquals($group1data['description'], $group['description']); $this->assertEquals($group1data['idnumber'], $group['idnumber']); } else { $this->assertEquals($group2data['description'], $group['description']); $this->assertEquals($group2data['idnumber'], $group['idnumber']); } } $this->setUser($teacher); // Retrieve other users groups. $groups = core_group_external::get_activity_allowed_groups($cm1->id, $student->id); $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups); $this->assertCount(2, $groups['groups']); // We are checking the $student passed as parameter so this will return false. $this->assertFalse($groups['canaccessallgroups']); // Check warnings. Trying to get groups for a user not enrolled in course. $groups = core_group_external::get_activity_allowed_groups($cm1->id, $otherstudent->id); $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups); $this->assertCount(1, $groups['warnings']); $this->assertFalse($groups['canaccessallgroups']); // Checking teacher groups. $groups = core_group_external::get_activity_allowed_groups($cm1->id); $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups); $this->assertCount(2, $groups['groups']); // Teachers by default can access all groups. $this->assertTrue($groups['canaccessallgroups']); } /** * Test get_activity_groupmode */ public function test_get_activity_groupmode(): void { global $DB; $this->resetAfterTest(true); $generator = self::getDataGenerator(); $student = $generator->create_user(); $course = $generator->create_course(); $othercourse = $generator->create_course(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $generator->enrol_user($student->id, $course->id, $studentrole->id); $forum1 = $generator->create_module("forum", array('course' => $course->id), array('groupmode' => VISIBLEGROUPS)); $forum2 = $generator->create_module("forum", array('course' => $othercourse->id)); $forum3 = $generator->create_module("forum", array('course' => $course->id), array('visible' => 0)); // Request data for tests. $cm1 = get_coursemodule_from_instance("forum", $forum1->id); $cm2 = get_coursemodule_from_instance("forum", $forum2->id); $cm3 = get_coursemodule_from_instance("forum", $forum3->id); $this->setUser($student); $data = core_group_external::get_activity_groupmode($cm1->id); $data = external_api::clean_returnvalue(core_group_external::get_activity_groupmode_returns(), $data); $this->assertEquals(VISIBLEGROUPS, $data['groupmode']); try { $data = core_group_external::get_activity_groupmode($cm2->id); } catch (\moodle_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } try { $data = core_group_external::get_activity_groupmode($cm3->id); } catch (\moodle_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } } /** * Test add_group_members. */ public function test_add_group_members(): void { global $DB; $this->resetAfterTest(true); $student1 = self::getDataGenerator()->create_user(); $student2 = self::getDataGenerator()->create_user(); $student3 = self::getDataGenerator()->create_user(); $course = self::getDataGenerator()->create_course(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($student3->id, $course->id, $studentrole->id); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; $group1data['description'] = 'Group Test 1 description'; $group1data['idnumber'] = 'TEST1'; $group1 = self::getDataGenerator()->create_group($group1data); // Checks against DB values. $memberstotal = $DB->count_records('groups_members', ['groupid' => $group1->id]); $this->assertEquals(0, $memberstotal); // Set the required capabilities by the external function. $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); core_group_external::add_group_members([ 'members' => [ 'groupid' => $group1->id, 'userid' => $student1->id, ] ]); core_group_external::add_group_members([ 'members' => [ 'groupid' => $group1->id, 'userid' => $student2->id, ] ]); core_group_external::add_group_members([ 'members' => [ 'groupid' => $group1->id, 'userid' => $student3->id, ] ]); // Checks against DB values. $memberstotal = $DB->count_records('groups_members', ['groupid' => $group1->id]); $this->assertEquals(3, $memberstotal); } /** * Test delete_group_members. */ public function test_delete_group_members(): void { global $DB; $this->resetAfterTest(true); $student1 = self::getDataGenerator()->create_user(); $student2 = self::getDataGenerator()->create_user(); $student3 = self::getDataGenerator()->create_user(); $course = self::getDataGenerator()->create_course(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($student3->id, $course->id, $studentrole->id); $group1data = array(); $group1data['courseid'] = $course->id; $group1data['name'] = 'Group Test 1'; $group1data['description'] = 'Group Test 1 description'; $group1data['idnumber'] = 'TEST1'; $group1 = self::getDataGenerator()->create_group($group1data); groups_add_member($group1->id, $student1->id); groups_add_member($group1->id, $student2->id); groups_add_member($group1->id, $student3->id); // Checks against DB values. $memberstotal = $DB->count_records('groups_members', ['groupid' => $group1->id]); $this->assertEquals(3, $memberstotal); // Set the required capabilities by the external function. $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id); $this->assignUserCapability('moodle/course:view', $context->id, $roleid); core_group_external::delete_group_members([ 'members' => [ 'groupid' => $group1->id, 'userid' => $student2->id, ] ]); // Checks against DB values. $memberstotal = $DB->count_records('groups_members', ['groupid' => $group1->id]); $this->assertEquals(2, $memberstotal); } } tests/customfield/grouping_handler_test.php 0000644 00000015260 15215711721 0015331 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group\customfield; use advanced_testcase; use context_course; use context_system; use moodle_url; use core_customfield\field_controller; /** * Unit tests for grouping custom field handler. * * @package core_group * @covers \core_group\customfield\group_handler * @author Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> * @copyright 2023 Catalyst IT Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class grouping_handler_test extends advanced_testcase { /** * Test custom field handler. * @var \core_customfield\handler */ protected $handler; /** * Setup. */ public function setUp(): void { parent::setUp(); $this->handler = grouping_handler::create(); } /** * Create grouping custom field for testing. * * @return field_controller */ protected function create_grouping_custom_field(): field_controller { $fieldcategory = self::getDataGenerator()->create_custom_field_category([ 'component' => 'core_group', 'area' => 'grouping', ]); return self::getDataGenerator()->create_custom_field([ 'shortname' => 'testfield1', 'type' => 'text', 'categoryid' => $fieldcategory->get('id'), ]); } /** * Test configuration context. */ public function test_get_configuration_context(): void { $this->assertInstanceOf(context_system::class, $this->handler->get_configuration_context()); } /** * Test getting config URL. */ public function test_get_configuration_url(): void { $this->assertInstanceOf(moodle_url::class, $this->handler->get_configuration_url()); $this->assertEquals('/group/grouping_customfield.php', $this->handler->get_configuration_url()->out_as_local_url()); } /** * Test getting instance context. */ public function test_get_instance_context(): void { global $COURSE; $this->resetAfterTest(); $course = self::getDataGenerator()->create_course(); $grouping = self::getDataGenerator()->create_grouping(['courseid' => $course->id]); $this->assertInstanceOf(context_course::class, $this->handler->get_instance_context()); $this->assertSame(context_course::instance($COURSE->id), $this->handler->get_instance_context()); $this->assertInstanceOf(context_course::class, $this->handler->get_instance_context($grouping->id)); $this->assertSame(context_course::instance($course->id), $this->handler->get_instance_context($grouping->id)); } /** * Test can configure check. */ public function test_can_configure(): void { $this->resetAfterTest(); $user = self::getDataGenerator()->create_user(); self::setUser($user); $this->assertFalse($this->handler->can_configure()); $roleid = self::getDataGenerator()->create_role(); assign_capability('moodle/group:configurecustomfields', CAP_ALLOW, $roleid, context_system::instance()->id, true); role_assign($roleid, $user->id, context_system::instance()->id); $this->assertTrue($this->handler->can_configure()); } /** * Test can edit functionality. */ public function test_can_edit(): void { $this->resetAfterTest(); $course = self::getDataGenerator()->create_course(); $contextid = context_course::instance($course->id)->id; $grouping = self::getDataGenerator()->create_grouping(['courseid' => $course->id]); $roleid = self::getDataGenerator()->create_role(); assign_capability('moodle/course:managegroups', CAP_ALLOW, $roleid, $contextid, true); $field = $this->create_grouping_custom_field(); $user = self::getDataGenerator()->create_user(); self::setUser($user); $this->assertFalse($this->handler->can_edit($field, $grouping->id)); role_assign($roleid, $user->id, $contextid); $this->assertTrue($this->handler->can_edit($field, $grouping->id)); } /** * Test can view functionality. */ public function test_can_view(): void { $this->resetAfterTest(); $course = self::getDataGenerator()->create_course(); $contextid = context_course::instance($course->id)->id; $grouping = self::getDataGenerator()->create_grouping(['courseid' => $course->id]); $manageroleid = self::getDataGenerator()->create_role(); assign_capability('moodle/course:managegroups', CAP_ALLOW, $manageroleid, $contextid, true); $viewroleid = self::getDataGenerator()->create_role(); assign_capability('moodle/course:view', CAP_ALLOW, $viewroleid, $contextid, true); $viewandmanageroleid = self::getDataGenerator()->create_role(); assign_capability('moodle/course:managegroups', CAP_ALLOW, $viewandmanageroleid, $contextid, true); assign_capability('moodle/course:view', CAP_ALLOW, $viewandmanageroleid, $contextid, true); $field = $this->create_grouping_custom_field(); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); self::setUser($user1); $this->assertFalse($this->handler->can_view($field, $grouping->id)); self::setUser($user2); $this->assertFalse($this->handler->can_view($field, $grouping->id)); self::setUser($user3); $this->assertFalse($this->handler->can_view($field, $grouping->id)); role_assign($manageroleid, $user1->id, $contextid); role_assign($viewroleid, $user2->id, $contextid); role_assign($viewandmanageroleid, $user3->id, $contextid); self::setUser($user1); $this->assertTrue($this->handler->can_view($field, $grouping->id)); self::setUser($user2); $this->assertTrue($this->handler->can_view($field, $grouping->id)); self::setUser($user3); $this->assertTrue($this->handler->can_view($field, $grouping->id)); } } tests/customfield/group_handler_test.php 0000644 00000015123 15215711721 0014631 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_group\customfield; use advanced_testcase; use context_course; use context_system; use moodle_url; use core_customfield\field_controller; /** * Unit tests for group custom field handler. * * @package core_group * @covers \core_group\customfield\group_handler * @author Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> * @copyright 2023 Catalyst IT Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class group_handler_test extends advanced_testcase { /** * Test custom field handler. * @var group_handler */ protected $handler; /** * Setup. */ public function setUp(): void { parent::setUp(); $this->handler = group_handler::create(); } /** * Create group custom field for testing. * * @return field_controller */ protected function create_group_custom_field(): field_controller { $fieldcategory = self::getDataGenerator()->create_custom_field_category([ 'component' => 'core_group', 'area' => 'group', ]); return self::getDataGenerator()->create_custom_field([ 'shortname' => 'testfield1', 'type' => 'text', 'categoryid' => $fieldcategory->get('id'), ]); } /** * Test configuration context. */ public function test_get_configuration_context(): void { $this->assertInstanceOf(context_system::class, $this->handler->get_configuration_context()); } /** * Test getting config URL. */ public function test_get_configuration_url(): void { $this->assertInstanceOf(moodle_url::class, $this->handler->get_configuration_url()); $this->assertEquals('/group/customfield.php', $this->handler->get_configuration_url()->out_as_local_url()); } /** * Test getting instance context. */ public function test_get_instance_context(): void { global $COURSE; $this->resetAfterTest(); $course = self::getDataGenerator()->create_course(); $group = self::getDataGenerator()->create_group(['courseid' => $course->id]); $this->assertInstanceOf(context_course::class, $this->handler->get_instance_context()); $this->assertSame(context_course::instance($COURSE->id), $this->handler->get_instance_context()); $this->assertInstanceOf(context_course::class, $this->handler->get_instance_context($group->id)); $this->assertSame(context_course::instance($course->id), $this->handler->get_instance_context($group->id)); } /** * Test can configure check. */ public function test_can_configure(): void { $this->resetAfterTest(); $user = self::getDataGenerator()->create_user(); self::setUser($user); $this->assertFalse($this->handler->can_configure()); $roleid = self::getDataGenerator()->create_role(); assign_capability('moodle/group:configurecustomfields', CAP_ALLOW, $roleid, context_system::instance()->id, true); role_assign($roleid, $user->id, context_system::instance()->id); $this->assertTrue($this->handler->can_configure()); } /** * Test can edit functionality. */ public function test_can_edit(): void { $this->resetAfterTest(); $course = self::getDataGenerator()->create_course(); $contextid = context_course::instance($course->id)->id; $group = self::getDataGenerator()->create_group(['courseid' => $course->id]); $roleid = self::getDataGenerator()->create_role(); assign_capability('moodle/course:managegroups', CAP_ALLOW, $roleid, $contextid, true); $field = $this->create_group_custom_field(); $user = self::getDataGenerator()->create_user(); self::setUser($user); $this->assertFalse($this->handler->can_edit($field, $group->id)); role_assign($roleid, $user->id, $contextid); $this->assertTrue($this->handler->can_edit($field, $group->id)); } /** * Test can view functionality. */ public function test_can_view(): void { $this->resetAfterTest(); $course = self::getDataGenerator()->create_course(); $contextid = context_course::instance($course->id)->id; $group = self::getDataGenerator()->create_group(['courseid' => $course->id]); $manageroleid = self::getDataGenerator()->create_role(); assign_capability('moodle/course:managegroups', CAP_ALLOW, $manageroleid, $contextid, true); $viewroleid = self::getDataGenerator()->create_role(); assign_capability('moodle/course:view', CAP_ALLOW, $viewroleid, $contextid, true); $viewandmanageroleid = self::getDataGenerator()->create_role(); assign_capability('moodle/course:managegroups', CAP_ALLOW, $viewandmanageroleid, $contextid, true); assign_capability('moodle/course:view', CAP_ALLOW, $viewandmanageroleid, $contextid, true); $field = $this->create_group_custom_field(); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); self::setUser($user1); $this->assertFalse($this->handler->can_view($field, $group->id)); self::setUser($user2); $this->assertFalse($this->handler->can_view($field, $group->id)); self::setUser($user3); $this->assertFalse($this->handler->can_view($field, $group->id)); role_assign($manageroleid, $user1->id, $contextid); role_assign($viewroleid, $user2->id, $contextid); role_assign($viewandmanageroleid, $user3->id, $contextid); self::setUser($user1); $this->assertTrue($this->handler->can_view($field, $group->id)); self::setUser($user2); $this->assertTrue($this->handler->can_view($field, $group->id)); self::setUser($user3); $this->assertTrue($this->handler->can_view($field, $group->id)); } } tests/reportbuilder/datasource/groups_test.php 0000644 00000045647 15215711721 0016033 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\datasource; use core_customfield_generator; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{boolean_select, date, select, text}; use core_reportbuilder\tests\core_reportbuilder_testcase; /** * Unit tests for groups datasource * * @package core_group * @covers \core_group\reportbuilder\datasource\groups * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class groups_test extends core_reportbuilder_testcase { /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $userone = $this->getDataGenerator()->create_and_enrol($course, 'student', ['firstname' => 'Zoe']); $usertwo = $this->getDataGenerator()->create_and_enrol($course, 'student', ['firstname' => 'Amy']); $groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Zebras']); $grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Aardvarks']); $this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $userone->id]); $this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $usertwo->id]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); // Default columns are course, group, user. Sorted by each. $courseurl = course_get_url($course); $this->assertEquals([ ["<a href=\"{$courseurl}\">{$course->fullname}</a>", $grouptwo->name, ''], ["<a href=\"{$courseurl}\">{$course->fullname}</a>", $groupone->name, fullname($usertwo)], ["<a href=\"{$courseurl}\">{$course->fullname}</a>", $groupone->name, fullname($userone)], ], array_map('array_values', $content)); } /** * Test datasource groupings reports */ public function test_datasource_groupings(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); // Create group, add to grouping. $groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Group A']); $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]); $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $groupone->id]); // Create second group, no grouping. $grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Group B']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:name', 'sortenabled' => 1]); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(2, $content); $this->assertEquals([ [ $course->fullname, $grouping->name, $groupone->name, ], [ $course->fullname, null, $grouptwo->name, ], ], array_map('array_values', $content)); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); /** @var core_customfield_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); // Create group with custom field, and single group member. $groupfieldcategory = $generator->create_category(['component' => 'core_group', 'area' => 'group']); $generator->create_field(['categoryid' => $groupfieldcategory->get('id'), 'shortname' => 'hi']); $group = $this->getDataGenerator()->create_group([ 'courseid' => $course->id, 'idnumber' => 'G101', 'enrolmentkey' => 'S', 'description' => 'My group', 'customfield_hi' => 'Hello', ]); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); // Create grouping with custom field, and single group. $groupingfieldcategory = $generator->create_category(['component' => 'core_group', 'area' => 'grouping']); $generator->create_field(['categoryid' => $groupingfieldcategory->get('id'), 'shortname' => 'bye']); $grouping = $this->getDataGenerator()->create_grouping([ 'courseid' => $course->id, 'idnumber' => 'GR101', 'description' => 'My grouping', 'customfield_bye' => 'Goodbye', ]); $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $group->id]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 0]); // Course (just to test join). $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:shortname']); // Group. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:idnumber']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:description']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:enrolmentkey']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:visibility']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:participation']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:picture']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:timemodified']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:customfield_hi']); // Grouping. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:idnumber']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:description']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:timemodified']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:customfield_bye']); // Group member. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group_member:timeadded']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group_member:component']); // User (just to test join). $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); [ $courseshortname, $groupidnumber, $groupdescription, $groupenrolmentkey, $groupvisibility, $groupparticipation, $grouppicture, $grouptimecreated, $grouptimemodified, $groupcustomfield, $groupingname, $groupingidnumber, $groupingdescription, $groupingtimecreated, $groupingtimemodified, $groupingcustomfield, $groupmembertimeadded, $groupmemebercomponent, $userusername, ] = array_values($content[0]); $this->assertEquals($course->shortname, $courseshortname); $this->assertEquals('G101', $groupidnumber); $this->assertEquals(format_text($group->description), $groupdescription); $this->assertEquals('S', $groupenrolmentkey); $this->assertEquals('Visible', $groupvisibility); $this->assertEquals('Yes', $groupparticipation); $this->assertEmpty($grouppicture); $this->assertNotEmpty($grouptimecreated); $this->assertNotEmpty($grouptimemodified); $this->assertEquals('Hello', $groupcustomfield); $this->assertEquals($grouping->name, $groupingname); $this->assertEquals('GR101', $groupingidnumber); $this->assertEquals(format_text($grouping->description), $groupingdescription); $this->assertNotEmpty($groupingtimecreated); $this->assertNotEmpty($groupingtimemodified); $this->assertEquals('Goodbye', $groupingcustomfield); $this->assertNotEmpty($groupmembertimeadded); $this->assertEmpty($groupmemebercomponent); $this->assertEquals($user->username, $userusername); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public static function datasource_filters_provider(): array { return [ // Course (just to test join). 'Filter course name' => ['course:fullname', [ 'course:fullname_operator' => text::IS_EQUAL_TO, 'course:fullname_value' => 'Test course', ], true], 'Filter course name (no match)' => ['course:fullname', [ 'course:fullname_operator' => text::IS_NOT_EQUAL_TO, 'course:fullname_value' => 'Test course', ], false], // Group. 'Filter group name' => ['group:name', [ 'group:name_operator' => text::IS_EQUAL_TO, 'group:name_value' => 'Test group', ], true], 'Filter group name (no match)' => ['group:name', [ 'group:name_operator' => text::IS_NOT_EQUAL_TO, 'group:name_value' => 'Test group', ], false], 'Filter group idnumber' => ['group:idnumber', [ 'group:idnumber_operator' => text::IS_EQUAL_TO, 'group:idnumber_value' => 'G101', ], true], 'Filter group idnumber (no match)' => ['group:idnumber', [ 'group:idnumber_operator' => text::IS_NOT_EQUAL_TO, 'group:idnumber_value' => 'G101', ], false], 'Filter group visibility' => ['group:visibility', [ 'group:visibility_operator' => select::EQUAL_TO, 'group:visibility_value' => 0, // Visible to everyone. ], true], 'Filter group visibility (no match)' => ['group:visibility', [ 'group:visibility_operator' => select::EQUAL_TO, 'group:visibility_value' => 1, // Visible to members only. ], false], 'Filter group participation' => ['group:participation', [ 'group:participation_operator' => boolean_select::CHECKED, ], true], 'Filter group participation (no match)' => ['group:participation', [ 'group:participation_operator' => boolean_select::NOT_CHECKED, ], false], 'Filter group time created' => ['group:timecreated', [ 'group:timecreated_operator' => date::DATE_RANGE, 'group:timecreated_from' => 1622502000, ], true], 'Filter group time created (no match)' => ['group:timecreated', [ 'group:timecreated_operator' => date::DATE_RANGE, 'group:timecreated_to' => 1622502000, ], false], // Grouping. 'Filter grouping name' => ['grouping:name', [ 'grouping:name_operator' => text::IS_EQUAL_TO, 'grouping:name_value' => 'Test grouping', ], true], 'Filter grouping name (no match)' => ['grouping:name', [ 'grouping:name_operator' => text::IS_NOT_EQUAL_TO, 'grouping:name_value' => 'Test grouping', ], false], 'Filter grouping idnumber' => ['grouping:idnumber', [ 'grouping:idnumber_operator' => text::IS_EQUAL_TO, 'grouping:idnumber_value' => 'GR101', ], true], 'Filter grouping idnumber (no match)' => ['grouping:idnumber', [ 'grouping:idnumber_operator' => text::IS_NOT_EQUAL_TO, 'grouping:idnumber_value' => 'GR101', ], false], 'Filter grouping time created' => ['grouping:timecreated', [ 'grouping:timecreated_operator' => date::DATE_RANGE, 'grouping:timecreated_from' => 1622502000, ], true], 'Filter grouping time created (no match)' => ['grouping:timecreated', [ 'grouping:timecreated_operator' => date::DATE_RANGE, 'grouping:timecreated_to' => 1622502000, ], false], // Group member. 'Filter group member time added' => ['group_member:timeadded', [ 'group_member:timeadded_operator' => date::DATE_RANGE, 'group_member:timeadded_from' => 1622502000, ], true], 'Filter group member time added (no match)' => ['group_member:timeadded', [ 'group_member:timeadded_operator' => date::DATE_RANGE, 'group_member:timeadded_to' => 1622502000, ], false], // User (just to test join). 'Filter user username' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'testuser', ], true], 'Filter user username (no match)' => ['user:username', [ 'user:username_operator' => text::IS_NOT_EQUAL_TO, 'user:username_value' => 'testuser', ], false], ]; } /** * Test datasource filters * * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider datasource_filters_provider */ public function test_datasource_filters( string $filtername, array $filtervalues, bool $expectmatch ): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(['fullname' => 'Test course']); $user = $this->getDataGenerator()->create_and_enrol($course, 'student', ['username' => 'testuser']); $group = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'idnumber' => 'G101', 'name' => 'Test group']); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id, 'idnumber' => 'GR101', 'name' => 'Test grouping']); $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $group->id]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single column, and given filter. $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:idnumber']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); if ($expectmatch) { $this->assertCount(1, $content); $this->assertEquals('G101', reset($content[0])); } else { $this->assertEmpty($content); } } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(['fullname' => 'Test course']); $user = $this->getDataGenerator()->create_and_enrol($course, 'student', ['username' => 'testuser']); $group = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'idnumber' => 'G101', 'name' => 'Test group']); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id, 'idnumber' => 'GR101', 'name' => 'Test grouping']); $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $group->id]); $this->datasource_stress_test_columns(groups::class); $this->datasource_stress_test_columns_aggregation(groups::class); $this->datasource_stress_test_conditions(groups::class, 'group:idnumber'); } } import.php 0000644 00000030246 15215711721 0006576 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Bulk group creation registration script from a comma separated file * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ require_once('../config.php'); require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->dirroot.'/group/lib.php'); include_once('import_form.php'); $id = required_param('id', PARAM_INT); // Course id $course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); $PAGE->set_url('/group/import.php', array('id'=>$id)); require_login($course); $context = context_course::instance($id); require_capability('moodle/course:managegroups', $context); $strimportgroups = get_string('importgroups', 'core_group'); $PAGE->navbar->add($strimportgroups); navigation_node::override_active_url(new moodle_url('/group/index.php', array('id' => $course->id))); $PAGE->set_title("$course->shortname: $strimportgroups"); $PAGE->set_heading($course->fullname); $PAGE->set_pagelayout('admin'); $returnurl = new moodle_url('/group/index.php', array('id'=>$id)); $importform = new groups_import_form(null, ['id' => $id]); // If a file has been uploaded, then process it if ($importform->is_cancelled()) { redirect($returnurl); } else if ($formdata = $importform->get_data()) { echo $OUTPUT->header(); $text = $importform->get_file_content('userfile'); $text = preg_replace('!\r\n?!', "\n", $text); require_once($CFG->libdir . '/csvlib.class.php'); $importid = csv_import_reader::get_new_iid('groupimport'); $csvimport = new csv_import_reader($importid, 'groupimport'); $delimiter = $formdata->delimiter_name; $encoding = $formdata->encoding; $readcount = $csvimport->load_csv_content($text, $encoding, $delimiter); if ($readcount === false) { throw new \moodle_exception('csvfileerror', 'error', $PAGE->url, $csvimport->get_error()); } else if ($readcount == 0) { throw new \moodle_exception('csvemptyfile', 'error', $PAGE->url, $csvimport->get_error()); } else if ($readcount == 1) { throw new \moodle_exception('csvnodata', 'error', $PAGE->url); } $csvimport->init(); unset($text); // make arrays of valid fields for error checking $required = array("groupname" => 1); $optionaldefaults = array("lang" => 1); $optional = array("coursename" => 1, "idnumber" => 1, "groupidnumber" => 1, "description" => 1, "enrolmentkey" => 1, "groupingname" => 1, "enablemessaging" => 1, ); // Check custom fields from group and grouping. $customfields = \core_group\customfield\group_handler::create()->get_fields(); $customfieldnames = []; foreach ($customfields as $customfield) { $controller = \core_customfield\data_controller::create(0, null, $customfield); $customfieldnames['customfield_' . $customfield->get('shortname')] = 1; } $customfields = \core_group\customfield\grouping_handler::create()->get_fields(); $groupingcustomfields = []; foreach ($customfields as $customfield) { $controller = \core_customfield\data_controller::create(0, null, $customfield); $groupingcustomfieldname = 'grouping_customfield_' . $customfield->get('shortname'); $customfieldnames[$groupingcustomfieldname] = 1; $groupingcustomfields[$groupingcustomfieldname] = 'customfield_' . $customfield->get('shortname'); } // --- get header (field names) --- // Using get_columns() ensures the Byte Order Mark is removed. $header = $csvimport->get_columns(); // Check for valid field names. foreach ($header as $i => $h) { // Remove whitespace. $h = trim($h); $header[$i] = $h; if (!isset($required[$h]) && !isset($optionaldefaults[$h]) && !isset($optional[$h]) && !isset($customfieldnames[$h])) { throw new \moodle_exception('invalidfieldname', 'error', $PAGE->url, $h); } if (isset($required[$h])) { $required[$h] = 2; } } // check for required fields foreach ($required as $key => $value) { if ($value < 2) { throw new \moodle_exception('fieldrequired', 'error', $PAGE->url, $key); } } $linenum = 2; // since header is line 1 while ($line = $csvimport->next()) { $newgroup = new stdClass();//to make Martin happy foreach ($optionaldefaults as $key => $value) { $newgroup->$key = current_language(); //defaults to current language } foreach ($line as $key => $value) { $record[$header[$key]] = trim($value); } if (trim(implode($line)) !== '') { // add a new group to the database // add fields to object $user foreach ($record as $name => $value) { // check for required values if (isset($required[$name]) and !$value) { throw new \moodle_exception('missingfield', 'error', $PAGE->url, $name); } else if ($name == "groupname") { $newgroup->name = $value; } else { // normal entry $newgroup->{$name} = $value; } } if (isset($newgroup->idnumber) && strlen($newgroup->idnumber)) { //if idnumber is set, we use that. //unset invalid courseid if (!$mycourse = $DB->get_record('course', array('idnumber'=>$newgroup->idnumber))) { echo $OUTPUT->notification(get_string('unknowncourseidnumber', 'error', $newgroup->idnumber)); unset($newgroup->courseid);//unset so 0 doesn't get written to database } else { $newgroup->courseid = $mycourse->id; } } else if (isset($newgroup->coursename) && strlen($newgroup->coursename)) { //else use course short name to look up //unset invalid coursename (if no id) if (!$mycourse = $DB->get_record('course', array('shortname' => $newgroup->coursename))) { echo $OUTPUT->notification(get_string('unknowncourse', 'error', $newgroup->coursename)); unset($newgroup->courseid);//unset so 0 doesn't get written to database } else { $newgroup->courseid = $mycourse->id; } } else { //else use use current id $newgroup->courseid = $id; } unset($newgroup->idnumber); unset($newgroup->coursename); //if courseid is set if (isset($newgroup->courseid)) { $linenum++; $groupname = $newgroup->name; $newgrpcoursecontext = context_course::instance($newgroup->courseid); ///Users cannot upload groups in courses they cannot update. if (!has_capability('moodle/course:managegroups', $newgrpcoursecontext) or (!is_enrolled($newgrpcoursecontext) and !has_capability('moodle/course:view', $newgrpcoursecontext))) { echo $OUTPUT->notification(get_string('nopermissionforcreation', 'group', $groupname)); } else { if (isset($newgroup->groupidnumber)) { // The CSV field for the group idnumber is groupidnumber rather than // idnumber as that field is already in use for the course idnumber. $newgroup->groupidnumber = trim($newgroup->groupidnumber); if (has_capability('moodle/course:changeidnumber', $newgrpcoursecontext)) { $newgroup->idnumber = $newgroup->groupidnumber; if ($existing = groups_get_group_by_idnumber($newgroup->courseid, $newgroup->idnumber)) { // idnumbers must be unique to a course but we shouldn't ignore group creation for duplicates $existing->name = s($existing->name); $existing->idnumber = s($existing->idnumber); $existing->problemgroup = $groupname; echo $OUTPUT->notification(get_string('groupexistforcoursewithidnumber', 'error', $existing)); unset($newgroup->idnumber); } } // Always drop the groupidnumber key. It's not a valid database field unset($newgroup->groupidnumber); } if ($groupid = groups_get_group_by_name($newgroup->courseid, $groupname)) { echo $OUTPUT->notification("$groupname :".get_string('groupexistforcourse', 'error', $groupname)); } else if ($groupid = groups_create_group($newgroup)) { echo $OUTPUT->notification(get_string('groupaddedsuccesfully', 'group', $groupname), 'notifysuccess'); } else { echo $OUTPUT->notification(get_string('groupnotaddederror', 'error', $groupname)); continue; } // Add group to grouping if (isset($newgroup->groupingname) && strlen($newgroup->groupingname)) { $groupingname = $newgroup->groupingname; if (! $groupingid = groups_get_grouping_by_name($newgroup->courseid, $groupingname)) { $data = new stdClass(); $data->courseid = $newgroup->courseid; $data->name = $groupingname; // Add customfield if exists. foreach ($header as $fieldname) { if (isset($customfieldnames[$fieldname]) && isset($newgroup->$fieldname)) { $data->{$groupingcustomfields[$groupingcustomfieldname]} = $newgroup->$fieldname; } } if ($groupingid = groups_create_grouping($data)) { echo $OUTPUT->notification(get_string('groupingaddedsuccesfully', 'group', $groupingname), 'notifysuccess'); } else { echo $OUTPUT->notification(get_string('groupingnotaddederror', 'error', $groupingname)); continue; } } // if we have reached here we definitely have a groupingid $a = array('groupname' => $groupname, 'groupingname' => $groupingname); try { groups_assign_grouping($groupingid, $groupid); echo $OUTPUT->notification(get_string('groupaddedtogroupingsuccesfully', 'group', $a), 'notifysuccess'); } catch (Exception $e) { echo $OUTPUT->notification(get_string('groupnotaddedtogroupingerror', 'error', $a)); } } } } unset ($newgroup); } } $csvimport->close(); echo $OUTPUT->single_button($returnurl, get_string('continue'), 'get'); echo $OUTPUT->footer(); die; } /// Print the form echo $OUTPUT->header(); echo $OUTPUT->heading_with_help($strimportgroups, 'importgroups', 'core_group'); $importform->display(); echo $OUTPUT->footer(); amd/src/comboboxsearch/group.js 0000644 00000021037 15215711721 0012571 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Allow the user to search for groups. * * @module core_group/comboboxsearch/group * @copyright 2023 Mathew May <mathew.solutions> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import search_combobox from 'core/comboboxsearch/search_combobox'; import {groupFetch} from 'core_group/comboboxsearch/repository'; import {renderForPromise, replaceNodeContents} from 'core/templates'; import {debounce} from 'core/utils'; import Notification from 'core/notification'; export default class GroupSearch extends search_combobox { courseID; cmID; bannedFilterFields = ['id', 'link', 'groupimageurl']; /** * Construct the class. * * @param {int|null} cmid ID of the course module initiating the group search (optional). */ constructor(cmid = null) { super(); this.selectors = {...this.selectors, courseid: '[data-region="courseid"]', placeholder: '.groupsearchdropdown [data-region="searchplaceholder"]', }; const component = document.querySelector(this.componentSelector()); this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid; // Override the instance since the body is built outside the constructor for the combobox. this.instance = component.querySelector(this.selectors.instance).dataset.instance; this.cmID = cmid; const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`); searchValueElement.addEventListener('change', () => { this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard. const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`); if (valueElement.value !== searchValueElement.value) { valueElement.value = searchValueElement.value; valueElement.dispatchEvent(new Event('change', {bubbles: true})); } searchValueElement.value = ''; }); this.$component.on('hide.bs.dropdown', () => { this.searchInput.removeAttribute('aria-activedescendant'); const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role="listbox"]`); listbox.querySelectorAll('.active[role="option"]').forEach(option => { option.classList.remove('active'); }); listbox.scrollTop = 0; // Use setTimeout to make sure the following code is executed after the click event is handled. setTimeout(() => { if (this.searchInput.value !== '') { this.searchInput.value = ''; this.searchInput.dispatchEvent(new Event('input', {bubbles: true})); } }); }); this.renderDefault().catch(Notification.exception); } /** * Initialise an instance of the class. * * @param {int|null} cmid ID of the course module initiating the group search (optional). */ static init(cmid = null) { return new GroupSearch(cmid); } /** * The overall div that contains the searching widget. * * @returns {string} */ componentSelector() { return '.group-search'; } /** * The dropdown div that contains the searching widget result space. * * @returns {string} */ dropdownSelector() { return '.groupsearchdropdown'; } /** * Build the content then replace the node. */ async renderDropdown() { const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', { groups: this.getMatchedResults(), hasresults: this.getMatchedResults().length > 0, instance: this.instance, searchterm: this.getSearchTerm(), }); replaceNodeContents(this.selectors.placeholder, html, js); // Remove aria-activedescendant when the available options change. this.searchInput.removeAttribute('aria-activedescendant'); } /** * Build the content then replace the node by default we want our form to exist. */ async renderDefault() { this.setMatchedResults(await this.filterDataset(await this.getDataset())); this.filterMatchDataset(); await this.renderDropdown(); this.updateNodes(); } /** * Get the data we will be searching against in this component. * * @returns {Promise<*>} */ async fetchDataset() { return await groupFetch(this.courseID, this.cmID).then((r) => r.groups); } /** * Dictate to the search component how and what we want to match upon. * * @param {Array} filterableData * @returns {Array} The users that match the given criteria. */ async filterDataset(filterableData) { // Sometimes we just want to show everything. if (this.getPreppedSearchTerm() === '') { return filterableData; } return filterableData.filter((group) => Object.keys(group).some((key) => { if (group[key] === "" || this.bannedFilterFields.includes(key)) { return false; } return group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm()); })); } /** * Given we have a subset of the dataset, set the field that we matched upon to inform the end user. */ filterMatchDataset() { this.setMatchedResults( this.getMatchedResults().map((group) => { return { id: group.id, name: group.name, groupimageurl: group.groupimageurl, }; }) ); } /** * The handler for when a user interacts with the component. * * @param {MouseEvent} e The triggering event that we are working with. */ async clickHandler(e) { if (e.target.closest(this.selectors.clearSearch)) { e.stopPropagation(); // Clear the entered search query in the search bar. this.searchInput.value = ''; this.setSearchTerms(this.searchInput.value); this.searchInput.focus(); this.clearSearchButton.classList.add('d-none'); // Display results. await this.filterrenderpipe(); } } /** * The handler for when a user changes the value of the component (selects an option from the dropdown). * * @param {Event} e The change event. */ changeHandler(e) { window.location = this.selectOneLink(e.target.value); } /** * Override the input event listener for the text input area. */ registerInputHandlers() { // Register & handle the text input. this.searchInput.addEventListener('input', debounce(async() => { this.setSearchTerms(this.searchInput.value); // We can also require a set amount of input before search. if (this.getSearchTerm() === '') { // Hide the "clear" search button in the search bar. this.clearSearchButton.classList.add('d-none'); } else { // Display the "clear" search button in the search bar. this.clearSearchButton.classList.remove('d-none'); } // User has given something for us to filter against. await this.filterrenderpipe(); }, 300)); } /** * Build up the view all link that is dedicated to a particular result. * We will call this function when a user interacts with the combobox to redirect them to show their results in the page. * * @param {Number} groupID The ID of the group selected. */ selectOneLink(groupID) { throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`); } } amd/src/comboboxsearch/repository.js 0000644 00000003002 15215711721 0013644 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A repo for the comboboxsearch group type. * * @module core_group/comboboxsearch/repository * @copyright 2023 Mathew May <mathew.solutions> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import ajax from "core/ajax"; /** * Given a course ID and optionally a module ID, we want to fetch the groups, so we may fetch their users. * * @method groupFetch * @param {int} courseid ID of the course to fetch the groups of. * @param {int|null} cmid ID of the course module initiating the group search (optional). * @return {object} jQuery promise */ export const groupFetch = (courseid, cmid = null) => { const request = { methodname: 'core_group_get_groups_for_selector', args: { courseid: courseid, cmid: cmid, }, }; return ajax.call([request])[0]; }; amd/src/index.js 0000644 00000004633 15215711721 0007551 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @module core_group/index * @copyright 2022 Matthew Hilton <matthewhilton@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import GroupPicker from "./grouppicker"; const groupPicker = new GroupPicker(); /** * Initialise page. */ export const init = () => { // Init event listeners. groupPicker.getDomElement().addEventListener("change", updateBulkActionButtons); // Call initially to set initial button state. updateBulkActionButtons(); }; /** * Updates the bulk action buttons depending on specific conditions. */ export const updateBulkActionButtons = () => { const groupsSelected = groupPicker.getSelectedValues(); const aGroupIsSelected = groupsSelected.length !== 0; // Collate the conditions where each button is enabled/disabled. const bulkActionsEnabledStatuses = { 'enablemessaging': aGroupIsSelected, 'disablemessaging': aGroupIsSelected }; // Update the status of each button. Object.entries(bulkActionsEnabledStatuses).map(([buttonId, enabled]) => setElementEnabled(buttonId, enabled)); }; /** * Adds or removes the given element's disabled attribute. * @param {string} domElementId ID of the dom element (without the #) * @param {bool} enabled If false, the disable attribute is applied, else it is removed. */ export const setElementEnabled = (domElementId, enabled) => { const element = document.getElementById(domElementId); if (!element) { // If there is no element, we do nothing. // The element could be purposefully hidden or removed. return; } if (!enabled) { element.setAttribute('disabled', 'disabled'); } else { element.removeAttribute('disabled'); } }; amd/src/grouppicker.js 0000644 00000004023 15215711721 0010765 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @module core_group/groupPicker * @copyright 2022 Matthew Hilton <matthewhilton@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class used for interfacing with the group select picker. * * @class core_group/GroupPicker */ export default class GroupPicker { /** * Creates the group picker class and finds the corresponding DOM element. * * @param {String} elementId The DOM element id of the <select> input * @throws Error if the element was not found. */ constructor(elementId = "groups") { const pickerDomElement = document.getElementById(elementId); if (!pickerDomElement) { throw new Error("Groups picker was not found."); } this.element = pickerDomElement; } /** * Returns the DOM element this class is linked to. * * @returns {HTMLElement} The DOM element */ getDomElement() { return this.element; } /** * Returns the selected group values. * * @returns {Number[]} The group IDs that are currently selected. */ getSelectedValues() { const selectedOptionElements = Array.from(this.element.querySelectorAll("option:checked")); const selectedGroups = selectedOptionElements.map(el => parseInt(el.value)); return selectedGroups; } } amd/build/grouppicker.min.js.map 0000644 00000005124 15215711721 0012636 0 ustar 00 {"version":3,"file":"grouppicker.min.js","sources":["../src/grouppicker.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n/**\n * @module core_group/groupPicker\n * @copyright 2022 Matthew Hilton <matthewhilton@catalyst-au.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Class used for interfacing with the group select picker.\n *\n * @class core_group/GroupPicker\n */\nexport default class GroupPicker {\n /**\n * Creates the group picker class and finds the corresponding DOM element.\n *\n * @param {String} elementId The DOM element id of the <select> input\n * @throws Error if the element was not found.\n */\n constructor(elementId = \"groups\") {\n const pickerDomElement = document.getElementById(elementId);\n\n if (!pickerDomElement) {\n throw new Error(\"Groups picker was not found.\");\n }\n\n this.element = pickerDomElement;\n }\n\n /**\n * Returns the DOM element this class is linked to.\n *\n * @returns {HTMLElement} The DOM element\n */\n getDomElement() {\n return this.element;\n }\n\n /**\n * Returns the selected group values.\n *\n * @returns {Number[]} The group IDs that are currently selected.\n */\n getSelectedValues() {\n const selectedOptionElements = Array.from(this.element.querySelectorAll(\"option:checked\"));\n const selectedGroups = selectedOptionElements.map(el => parseInt(el.value));\n\n return selectedGroups;\n }\n}\n"],"names":["constructor","elementId","pickerDomElement","document","getElementById","Error","element","getDomElement","this","getSelectedValues","Array","from","querySelectorAll","map","el","parseInt","value"],"mappings":";;;;;;MAgCIA,kBAAYC,iEAAY,eACdC,iBAAmBC,SAASC,eAAeH,eAE5CC,uBACK,IAAIG,MAAM,qCAGfC,QAAUJ,iBAQnBK,uBACWC,KAAKF,QAQhBG,2BACmCC,MAAMC,KAAKH,KAAKF,QAAQM,iBAAiB,mBAC1BC,KAAIC,IAAMC,SAASD,GAAGE"} amd/build/grouppicker.min.js 0000644 00000001505 15215711721 0012061 0 ustar 00 define("core_group/grouppicker",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default= /** * @module core_group/groupPicker * @copyright 2022 Matthew Hilton <matthewhilton@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class{constructor(){let elementId=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"groups";const pickerDomElement=document.getElementById(elementId);if(!pickerDomElement)throw new Error("Groups picker was not found.");this.element=pickerDomElement}getDomElement(){return this.element}getSelectedValues(){return Array.from(this.element.querySelectorAll("option:checked")).map((el=>parseInt(el.value)))}},_exports.default})); //# sourceMappingURL=grouppicker.min.js.map amd/build/comboboxsearch/repository.min.js.map 0000644 00000003557 15215711721 0015531 0 ustar 00 {"version":3,"file":"repository.min.js","sources":["../../src/comboboxsearch/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A repo for the comboboxsearch group type.\n *\n * @module core_group/comboboxsearch/repository\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from \"core/ajax\";\n\n/**\n * Given a course ID and optionally a module ID, we want to fetch the groups, so we may fetch their users.\n *\n * @method groupFetch\n * @param {int} courseid ID of the course to fetch the groups of.\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n * @return {object} jQuery promise\n */\nexport const groupFetch = (courseid, cmid = null) => {\n const request = {\n methodname: 'core_group_get_groups_for_selector',\n args: {\n courseid: courseid,\n cmid: cmid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","cmid","request","methodname","args","ajax","call"],"mappings":";;;;;;;gKAiC0B,SAACA,cAAUC,4DAAO,WAClCC,QAAU,CACZC,WAAY,qCACZC,KAAM,CACFJ,SAAUA,SACVC,KAAMA,cAGPI,cAAKC,KAAK,CAACJ,UAAU"} amd/build/comboboxsearch/group.min.js.map 0000644 00000027631 15215711721 0014445 0 ustar 00 {"version":3,"file":"group.min.js","sources":["../../src/comboboxsearch/group.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Allow the user to search for groups.\n *\n * @module core_group/comboboxsearch/group\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {groupFetch} from 'core_group/comboboxsearch/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport Notification from 'core/notification';\n\nexport default class GroupSearch extends search_combobox {\n\n courseID;\n cmID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n /**\n * Construct the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(cmid = null) {\n super();\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.groupsearchdropdown [data-region=\"searchplaceholder\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n // Override the instance since the body is built outside the constructor for the combobox.\n this.instance = component.querySelector(this.selectors.instance).dataset.instance;\n this.cmID = cmid;\n\n const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);\n searchValueElement.addEventListener('change', () => {\n this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);\n if (valueElement.value !== searchValueElement.value) {\n valueElement.value = searchValueElement.value;\n valueElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n\n searchValueElement.value = '';\n });\n\n this.$component.on('hide.bs.dropdown', () => {\n this.searchInput.removeAttribute('aria-activedescendant');\n\n const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role=\"listbox\"]`);\n listbox.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n listbox.scrollTop = 0;\n\n // Use setTimeout to make sure the following code is executed after the click event is handled.\n setTimeout(() => {\n if (this.searchInput.value !== '') {\n this.searchInput.value = '';\n this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));\n }\n });\n });\n\n this.renderDefault().catch(Notification.exception);\n }\n\n /**\n * Initialise an instance of the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n static init(cmid = null) {\n return new GroupSearch(cmid);\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.group-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.groupsearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {\n groups: this.getMatchedResults(),\n hasresults: this.getMatchedResults().length > 0,\n instance: this.instance,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.selectors.placeholder, html, js);\n // Remove aria-activedescendant when the available options change.\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n\n await this.renderDropdown();\n\n this.updateNodes();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n async fetchDataset() {\n return await groupFetch(this.courseID, this.cmID).then((r) => r.groups);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return filterableData;\n }\n return filterableData.filter((group) => Object.keys(group).some((key) => {\n if (group[key] === \"\" || this.bannedFilterFields.includes(key)) {\n return false;\n }\n return group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n */\n filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((group) => {\n return {\n id: group.id,\n name: group.name,\n groupimageurl: group.groupimageurl,\n };\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n if (e.target.closest(this.selectors.clearSearch)) {\n e.stopPropagation();\n // Clear the entered search query in the search bar.\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n this.searchInput.focus();\n this.clearSearchButton.classList.add('d-none');\n // Display results.\n await this.filterrenderpipe();\n }\n }\n\n /**\n * The handler for when a user changes the value of the component (selects an option from the dropdown).\n *\n * @param {Event} e The change event.\n */\n changeHandler(e) {\n window.location = this.selectOneLink(e.target.value);\n }\n\n /**\n * Override the input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n // User has given something for us to filter against.\n await this.filterrenderpipe();\n }, 300));\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n * We will call this function when a user interacts with the combobox to redirect them to show their results in the page.\n *\n * @param {Number} groupID The ID of the group selected.\n */\n selectOneLink(groupID) {\n throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GroupSearch","search_combobox","constructor","cmid","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","instance","cmID","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","$component","on","removeAttribute","listbox","getAttribute","querySelectorAll","forEach","option","classList","remove","scrollTop","setTimeout","renderDefault","catch","Notification","exception","dropdownSelector","html","js","groups","getMatchedResults","hasresults","length","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","then","r","filterableData","getPreppedSearchTerm","filter","group","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","id","name","groupimageurl","e","target","closest","clearSearch","stopPropagation","setSearchTerms","focus","clearSearchButton","add","filterrenderpipe","changeHandler","window","location","selectOneLink","registerInputHandlers","async","groupID","Error"],"mappings":"+rBA4BqBA,oBAAoBC,yBAWrCC,kBAAYC,4DAAO,mIAPE,CAAC,KAAM,OAAQ,uBAS3BC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,cAEpEQ,SAAWN,UAAUE,cAAcL,KAAKD,UAAUU,UAAUD,QAAQC,cACpEC,KAAOZ,WAENa,mBAAqBX,KAAKG,UAAUE,yBAAkBL,KAAKY,YAAYJ,QAAQK,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAehB,KAAKG,UAAUE,yBAAkBL,KAAKiB,SAAST,QAAQK,eACxEG,aAAaE,QAAUP,mBAAmBO,QAC1CF,aAAaE,MAAQP,mBAAmBO,MACxCF,aAAaG,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS,MAG7DV,mBAAmBO,MAAQ,WAG1BI,WAAWC,GAAG,oBAAoB,UAC9BX,YAAYY,gBAAgB,+BAE3BC,QAAUrB,SAASC,yBAAkBL,KAAKY,YAAYc,aAAa,sCACzED,QAAQE,iBAAiB,0BAA0BC,SAAQC,SACvDA,OAAOC,UAAUC,OAAO,aAE5BN,QAAQO,UAAY,EAGpBC,YAAW,KACwB,KAA3BjC,KAAKY,YAAYM,aACZN,YAAYM,MAAQ,QACpBN,YAAYO,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,iBAKnEa,gBAAgBC,MAAMC,sBAAaC,gCASjC,IAAI1C,mEADI,MASnBW,0BACW,gBAQXgC,yBACW,oDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,OAAQzC,KAAK0C,oBACbC,WAAY3C,KAAK0C,oBAAoBE,OAAS,EAC9CnC,SAAUT,KAAKS,SACfoC,WAAY7C,KAAK8C,qDAED9C,KAAKD,UAAUG,YAAaqC,KAAMC,SAEjD5B,YAAYY,gBAAgB,oDAO5BuB,wBAAwB/C,KAAKgD,oBAAoBhD,KAAKiD,oBACtDC,2BAEClD,KAAKmD,sBAENC,gDASQ,0BAAWpD,KAAKO,SAAUP,KAAKU,MAAM2C,MAAMC,GAAMA,EAAEb,6BAShDc,sBAEoB,KAAhCvD,KAAKwD,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,OAAe9D,KAAK+D,mBAAmBC,SAASF,MAGnDJ,MAAMI,KAAKG,WAAWC,cAAcF,SAAShE,KAAKwD,4BAOjEN,0BACSH,kBACD/C,KAAK0C,oBAAoByB,KAAKT,QACnB,CACHU,GAAIV,MAAMU,GACVC,KAAMX,MAAMW,KACZC,cAAeZ,MAAMY,sCAWlBC,GACXA,EAAEC,OAAOC,QAAQzE,KAAKD,UAAU2E,eAChCH,EAAEI,uBAEG/D,YAAYM,MAAQ,QACpB0D,eAAe5E,KAAKY,YAAYM,YAChCN,YAAYiE,aACZC,kBAAkBhD,UAAUiD,IAAI,gBAE/B/E,KAAKgF,oBASnBC,cAAcV,GACVW,OAAOC,SAAWnF,KAAKoF,cAAcb,EAAEC,OAAOtD,OAMlDmE,6BAESzE,YAAYE,iBAAiB,SAAS,oBAASwE,eAC3CV,eAAe5E,KAAKY,YAAYM,OAER,KAAzBlB,KAAK8C,qBAEAgC,kBAAkBhD,UAAUiD,IAAI,eAGhCD,kBAAkBhD,UAAUC,OAAO,gBAGtC/B,KAAKgF,qBACZ,MASPI,cAAcG,eACJ,IAAIC,8BAAuBD,4CAAmCvF,KAAKH,YAAYwE"} amd/build/comboboxsearch/repository.min.js 0000644 00000001405 15215711721 0014743 0 ustar 00 define("core_group/comboboxsearch/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj; /** * A repo for the comboboxsearch group type. * * @module core_group/comboboxsearch/repository * @copyright 2023 Mathew May <mathew.solutions> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.groupFetch=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.groupFetch=function(courseid){let cmid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const request={methodname:"core_group_get_groups_for_selector",args:{courseid:courseid,cmid:cmid}};return _ajax.default.call([request])[0]}})); //# sourceMappingURL=repository.min.js.map amd/build/comboboxsearch/group.min.js 0000644 00000010644 15215711721 0013665 0 ustar 00 define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){let cmid=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"cmID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.instance=component.querySelector(this.selectors.instance).dataset.instance,this.cmID=cmid;const searchValueElement=this.component.querySelector("#".concat(this.searchInput.dataset.inputElement));searchValueElement.addEventListener("change",(()=>{this.toggleDropdown();const valueElement=this.component.querySelector("#".concat(this.combobox.dataset.inputElement));valueElement.value!==searchValueElement.value&&(valueElement.value=searchValueElement.value,valueElement.dispatchEvent(new Event("change",{bubbles:!0}))),searchValueElement.value=""})),this.$component.on("hide.bs.dropdown",(()=>{this.searchInput.removeAttribute("aria-activedescendant");const listbox=document.querySelector("#".concat(this.searchInput.getAttribute("aria-controls"),'[role="listbox"]'));listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})),listbox.scrollTop=0,setTimeout((()=>{""!==this.searchInput.value&&(this.searchInput.value="",this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}))})),this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch(arguments.length>0&&void 0!==arguments[0]?arguments[0]:null)}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,instance:this.instance,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID,this.cmID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,groupimageurl:group.groupimageurl}))))}async clickHandler(e){e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe())}changeHandler(e){window.location=this.selectOneLink(e.target.value)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default})); //# sourceMappingURL=group.min.js.map amd/build/index.min.js 0000644 00000002440 15215711721 0010635 0 ustar 00 define("core_group/index",["exports","./grouppicker"],(function(_exports,_grouppicker){var obj; /** * @module core_group/index * @copyright 2022 Matthew Hilton <matthewhilton@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateBulkActionButtons=_exports.setElementEnabled=_exports.init=void 0;const groupPicker=new(_grouppicker=(obj=_grouppicker)&&obj.__esModule?obj:{default:obj}).default;_exports.init=()=>{groupPicker.getDomElement().addEventListener("change",updateBulkActionButtons),updateBulkActionButtons()};const updateBulkActionButtons=()=>{const aGroupIsSelected=0!==groupPicker.getSelectedValues().length,bulkActionsEnabledStatuses={enablemessaging:aGroupIsSelected,disablemessaging:aGroupIsSelected};Object.entries(bulkActionsEnabledStatuses).map((_ref=>{let[buttonId,enabled]=_ref;return setElementEnabled(buttonId,enabled)}))};_exports.updateBulkActionButtons=updateBulkActionButtons;const setElementEnabled=(domElementId,enabled)=>{const element=document.getElementById(domElementId);element&&(enabled?element.removeAttribute("disabled"):element.setAttribute("disabled","disabled"))};_exports.setElementEnabled=setElementEnabled})); //# sourceMappingURL=index.min.js.map amd/build/index.min.js.map 0000644 00000006311 15215711721 0011412 0 ustar 00 {"version":3,"file":"index.min.js","sources":["../src/index.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n/**\n * @module core_group/index\n * @copyright 2022 Matthew Hilton <matthewhilton@catalyst-au.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport GroupPicker from \"./grouppicker\";\n\nconst groupPicker = new GroupPicker();\n\n/**\n * Initialise page.\n */\nexport const init = () => {\n // Init event listeners.\n groupPicker.getDomElement().addEventListener(\"change\", updateBulkActionButtons);\n\n // Call initially to set initial button state.\n updateBulkActionButtons();\n};\n\n/**\n * Updates the bulk action buttons depending on specific conditions.\n */\nexport const updateBulkActionButtons = () => {\n const groupsSelected = groupPicker.getSelectedValues();\n const aGroupIsSelected = groupsSelected.length !== 0;\n\n // Collate the conditions where each button is enabled/disabled.\n const bulkActionsEnabledStatuses = {\n 'enablemessaging': aGroupIsSelected,\n 'disablemessaging': aGroupIsSelected\n };\n\n // Update the status of each button.\n Object.entries(bulkActionsEnabledStatuses).map(([buttonId, enabled]) => setElementEnabled(buttonId, enabled));\n};\n\n/**\n * Adds or removes the given element's disabled attribute.\n * @param {string} domElementId ID of the dom element (without the #)\n * @param {bool} enabled If false, the disable attribute is applied, else it is removed.\n */\nexport const setElementEnabled = (domElementId, enabled) => {\n const element = document.getElementById(domElementId);\n\n if (!element) {\n // If there is no element, we do nothing.\n // The element could be purposefully hidden or removed.\n return;\n }\n\n if (!enabled) {\n element.setAttribute('disabled', 'disabled');\n } else {\n element.removeAttribute('disabled');\n }\n};\n"],"names":["groupPicker","getDomElement","addEventListener","updateBulkActionButtons","aGroupIsSelected","getSelectedValues","length","bulkActionsEnabledStatuses","Object","entries","map","_ref","buttonId","enabled","setElementEnabled","domElementId","element","document","getElementById","removeAttribute","setAttribute"],"mappings":";;;;;oJAsBMA,YAAc,6FAKA,KAEhBA,YAAYC,gBAAgBC,iBAAiB,SAAUC,yBAGvDA,iCAMSA,wBAA0B,WAE7BC,iBAA6C,IAD5BJ,YAAYK,oBACKC,OAGlCC,2BAA6B,iBACZH,kCACCA,kBAIxBI,OAAOC,QAAQF,4BAA4BG,KAAIC,WAAEC,SAAUC,qBAAaC,kBAAkBF,SAAUC,4EAQ3FC,kBAAoB,CAACC,aAAcF,iBACtCG,QAAUC,SAASC,eAAeH,cAEnCC,UAMAH,QAGDG,QAAQG,gBAAgB,YAFxBH,QAAQI,aAAa,WAAY"} delete.php 0000644 00000007066 15215711721 0006532 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Delete group * * @package core_group * @copyright 2008 The Open University, s.marshall AT open.ac.uk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once('lib.php'); // Get and check parameters $courseid = required_param('courseid', PARAM_INT); $groupids = required_param('groups', PARAM_SEQUENCE); $confirm = optional_param('confirm', 0, PARAM_BOOL); $PAGE->set_url('/group/delete.php', array('courseid'=>$courseid,'groups'=>$groupids)); $PAGE->set_pagelayout('standard'); // Make sure course is OK and user has access to manage groups if (!$course = $DB->get_record('course', array('id' => $courseid))) { throw new \moodle_exception('invalidcourseid'); } require_login($course); $context = context_course::instance($course->id); require_capability('moodle/course:managegroups', $context); $changeidnumber = has_capability('moodle/course:changeidnumber', $context); // Make sure all groups are OK and belong to course $groupidarray = explode(',',$groupids); $groupnames = array(); foreach($groupidarray as $groupid) { if (!$group = $DB->get_record('groups', array('id' => $groupid))) { throw new \moodle_exception('invalidgroupid'); } if (!empty($group->idnumber) && !$changeidnumber) { throw new \moodle_exception('grouphasidnumber', '', '', $group->name); } if ($courseid != $group->courseid) { throw new \moodle_exception('groupunknown', '', '', $group->courseid); } $groupnames[] = format_string($group->name); } $returnurl='index.php?id='.$course->id; if(count($groupidarray)==0) { throw new \moodle_exception('errorselectsome', 'group', $returnurl); } if ($confirm && data_submitted()) { if (!confirm_sesskey() ) { throw new \moodle_exception('confirmsesskeybad', 'error', $returnurl); } foreach($groupidarray as $groupid) { groups_delete_group($groupid); } redirect($returnurl); } else { $PAGE->set_title(get_string('deleteselectedgroup', 'group')); $PAGE->set_heading($course->fullname . ': '. get_string('deleteselectedgroup', 'group')); echo $OUTPUT->header(); $optionsyes = array('courseid'=>$courseid, 'groups'=>$groupids, 'sesskey'=>sesskey(), 'confirm'=>1); $optionsno = array('id'=>$courseid); if(count($groupnames)==1) { $message=get_string('deletegroupconfirm', 'group', $groupnames[0]); } else { $message=get_string('deletegroupsconfirm', 'group').'<ul>'; foreach($groupnames as $groupname) { $message.='<li>'.$groupname.'</li>'; } $message.='</ul>'; } $formcontinue = new single_button(new moodle_url('delete.php', $optionsyes), get_string('yes'), 'post'); $formcancel = new single_button(new moodle_url('index.php', $optionsno), get_string('no'), 'get'); echo $OUTPUT->confirm($message, $formcontinue, $formcancel); echo $OUTPUT->footer(); } autogroup_form.php 0000644 00000025402 15215711721 0010332 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Auto group form * * @package core_group * @copyright 2007 mattc-catalyst (http://moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } require_once($CFG->dirroot.'/lib/formslib.php'); require_once($CFG->dirroot.'/cohort/lib.php'); /** * Auto group form class * * @package core_group * @copyright 2007 mattc-catalyst (http://moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class autogroup_form extends moodleform { /** * Form Definition */ function definition() { global $USER, $COURSE; $coursecontext = context_course::instance($COURSE->id); $mform =& $this->_form; $mform->addElement('header', 'autogroup', get_string('general')); $mform->addElement('text', 'namingscheme', get_string('namingscheme', 'group')); $mform->addHelpButton('namingscheme', 'namingscheme', 'group'); $mform->addRule('namingscheme', get_string('required'), 'required', null, 'client'); $mform->setType('namingscheme', PARAM_TEXT); // There must not be duplicate group names in course. $template = get_string('grouptemplate', 'group'); $gname = groups_parse_name($template, 0); if (!groups_get_group_by_name($COURSE->id, $gname)) { $mform->setDefault('namingscheme', $template); } $options = array('groups' => get_string('numgroups', 'group'), 'members' => get_string('nummembers', 'group')); $mform->addElement('select', 'groupby', get_string('groupby', 'group'), $options); $mform->addElement('text', 'number', get_string('number', 'group'),'maxlength="4" size="4"'); $mform->setType('number', PARAM_INT); $mform->addRule('number', null, 'numeric', null, 'client'); $mform->addRule('number', get_string('required'), 'required', null, 'client'); // Enable group messaging for the groups to be auto-created. if (\core_message\api::can_create_group_conversation($USER->id, $coursecontext)) { $mform->addElement('selectyesno', 'enablemessaging', get_string('enablemessaging', 'group')); $mform->addHelpButton('enablemessaging', 'enablemessaging', 'group'); } $mform->addElement('header', 'groupmembershdr', get_string('groupmembers', 'group')); $mform->setExpanded('groupmembershdr', true); $options = array(0=>get_string('all')); $options += $this->_customdata['roles']; $mform->addElement('select', 'roleid', get_string('selectfromrole', 'group'), $options); $student = get_archetype_roles('student'); $student = reset($student); if ($student and array_key_exists($student->id, $options)) { $mform->setDefault('roleid', $student->id); } $coursecontext = context_course::instance($COURSE->id); if ($cohorts = cohort_get_available_cohorts($coursecontext, COHORT_WITH_ENROLLED_MEMBERS_ONLY, 0, 0)) { $options = array(0 => get_string('anycohort', 'cohort')); foreach ($cohorts as $c) { $options[$c->id] = format_string($c->name, true, [ 'context' => context::instance_by_id($c->contextid), ]); } $mform->addElement('select', 'cohortid', get_string('selectfromcohort', 'cohort'), $options); $mform->setDefault('cohortid', '0'); } else { $mform->addElement('hidden','cohortid'); $mform->setType('cohortid', PARAM_INT); $mform->setConstant('cohortid', '0'); } if ($groupings = groups_get_all_groupings($COURSE->id)) { $options = array(); $options[0] = get_string('none'); foreach ($groupings as $grouping) { $options[$grouping->id] = format_string($grouping->name); } $mform->addElement('select', 'groupingid', get_string('selectfromgrouping', 'group'), $options); $mform->setDefault('groupingid', 0); $mform->disabledIf('groupingid', 'notingroup', 'checked'); } else { $mform->addElement('hidden', 'groupingid'); $mform->setType('groupingid', PARAM_INT); $mform->setConstant('groupingid', 0); } if ($groups = groups_get_all_groups($COURSE->id)) { $options = array(); $options[0] = get_string('none'); foreach ($groups as $group) { $options[$group->id] = format_string($group->name); } $mform->addElement('select', 'groupid', get_string('selectfromgroup', 'group'), $options); $mform->setDefault('groupid', 0); $mform->disabledIf('groupid', 'notingroup', 'checked'); } else { $mform->addElement('hidden', 'groupid'); $mform->setType('groupid', PARAM_INT); $mform->setConstant('groupid', 0); } $options = array('no' => get_string('noallocation', 'group'), 'random' => get_string('random', 'group'), 'firstname' => get_string('byfirstname', 'group'), 'lastname' => get_string('bylastname', 'group'), 'idnumber' => get_string('byidnumber', 'group')); $mform->addElement('select', 'allocateby', get_string('allocateby', 'group'), $options); $mform->setDefault('allocateby', 'random'); $mform->addElement('checkbox', 'nosmallgroups', get_string('nosmallgroups', 'group')); $mform->disabledIf('nosmallgroups', 'groupby', 'noteq', 'members'); $mform->addElement('checkbox', 'notingroup', get_string('notingroup', 'group')); $mform->disabledIf('notingroup', 'groupingid', 'neq', 0); $mform->disabledIf('notingroup', 'groupid', 'neq', 0); if (has_capability('moodle/course:viewsuspendedusers', $coursecontext)) { $mform->addElement('checkbox', 'includeonlyactiveenrol', get_string('includeonlyactiveenrol', 'group'), ''); $mform->addHelpButton('includeonlyactiveenrol', 'includeonlyactiveenrol', 'group'); $mform->setDefault('includeonlyactiveenrol', true); } $mform->addElement('header', 'groupinghdr', get_string('grouping', 'group')); $options = array('0' => get_string('nogrouping', 'group'), '-1'=> get_string('newgrouping', 'group')); if ($groupings = groups_get_all_groupings($COURSE->id)) { foreach ($groupings as $grouping) { $options[$grouping->id] = strip_tags(format_string($grouping->name)); } } $mform->addElement('select', 'grouping', get_string('createingrouping', 'group'), $options); if ($groupings) { $mform->setDefault('grouping', '-1'); } $mform->addElement('text', 'groupingname', get_string('groupingname', 'group'), $options); $mform->setType('groupingname', PARAM_TEXT); $mform->disabledIf('groupingname', 'grouping', 'noteq', '-1'); $mform->addElement('hidden','courseid'); $mform->setType('courseid', PARAM_INT); $mform->addElement('hidden','seed'); $mform->setType('seed', PARAM_INT); $buttonarray = array(); $buttonarray[] = &$mform->createElement('submit', 'preview', get_string('preview')); $buttonarray[] = &$mform->createElement('submit', 'submitbutton', get_string('submit')); $buttonarray[] = &$mform->createElement('cancel'); $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); $mform->closeHeaderBefore('buttonar'); } /** * Performs validation of the form information * * @param array $data * @param array $files * @return array $errors An array of $errors */ function validation($data, $files) { global $CFG, $COURSE; $errors = parent::validation($data, $files); if ($data['allocateby'] != 'no') { $source = array(); if ($data['cohortid']) { $source['cohortid'] = $data['cohortid']; } if ($data['groupingid']) { $source['groupingid'] = $data['groupingid']; } if ($data['groupid']) { $source['groupid'] = $data['groupid']; } if (!$users = groups_get_potential_members($data['courseid'], $data['roleid'], $source)) { $errors['roleid'] = get_string('nousersinrole', 'group'); } /// Check the number entered is sane if ($data['groupby'] == 'groups') { $usercnt = count($users); if ($data['number'] > $usercnt || $data['number'] < 1) { $errors['number'] = get_string('toomanygroups', 'group', $usercnt); } } } //try to detect group name duplicates $name = groups_parse_name(trim($data['namingscheme']), 0); if (groups_get_group_by_name($COURSE->id, $name)) { $errors['namingscheme'] = get_string('groupnameexists', 'group', $name); } // check grouping name duplicates if ( isset($data['grouping']) && $data['grouping'] == '-1') { $name = trim($data['groupingname']); if (empty($name)) { $errors['groupingname'] = get_string('required'); } else if (groups_get_grouping_by_name($COURSE->id, $name)) { $errors['groupingname'] = get_string('groupingnameexists', 'group', $name); } } /// Check the naming scheme if ($data['groupby'] == 'groups' and $data['number'] == 1) { // we can use the name as is because there will be only one group max } else { $matchcnt = preg_match_all('/[#@]{1,1}/', $data['namingscheme'], $matches); if ($matchcnt != 1) { $errors['namingscheme'] = get_string('badnamingscheme', 'group'); } } return $errors; } } group.php 0000644 00000011521 15215711721 0006413 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Create group OR edit group settings. * * @copyright 2006 The Open University, N.D.Freear AT open.ac.uk, J.White AT open.ac.uk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ require_once('../config.php'); require_once('lib.php'); require_once('group_form.php'); /// get url variables $courseid = optional_param('courseid', 0, PARAM_INT); $id = optional_param('id', 0, PARAM_INT); $delete = optional_param('delete', 0, PARAM_BOOL); $confirm = optional_param('confirm', 0, PARAM_BOOL); // This script used to support group delete, but that has been moved. In case // anyone still links to it, let's redirect to the new script. if ($delete) { debugging('Deleting a group through group/group.php is deprecated and will be removed soon. Please use group/delete.php instead'); redirect(new moodle_url('delete.php', array('courseid' => $courseid, 'groups' => $id))); } if ($id) { if (!$group = $DB->get_record('groups', array('id'=>$id))) { throw new \moodle_exception('invalidgroupid'); } if (empty($courseid)) { $courseid = $group->courseid; } else if ($courseid != $group->courseid) { throw new \moodle_exception('invalidcourseid'); } if (!$course = $DB->get_record('course', array('id'=>$courseid))) { throw new \moodle_exception('invalidcourseid'); } } else { if (!$course = $DB->get_record('course', array('id'=>$courseid))) { throw new \moodle_exception('invalidcourseid'); } $group = new stdClass(); $group->courseid = $course->id; } if ($id !== 0) { $PAGE->set_url('/group/group.php', array('id'=>$id)); } else { $PAGE->set_url('/group/group.php', array('courseid'=>$courseid)); } require_login($course); $context = context_course::instance($course->id); require_capability('moodle/course:managegroups', $context); $strgroups = get_string('groups'); $PAGE->set_title($strgroups); $PAGE->set_heading($course->fullname . ': '.$strgroups); $PAGE->set_pagelayout('admin'); navigation_node::override_active_url(new moodle_url('/group/index.php', array('id' => $course->id))); $returnurl = $CFG->wwwroot.'/group/index.php?id='.$course->id.'&group='.$id; // Prepare the description editor: We do support files for group descriptions $editoroptions = array('maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$course->maxbytes, 'trust'=>false, 'context'=>$context, 'noclean'=>true); if (!empty($group->id)) { $editoroptions['subdirs'] = file_area_contains_subdirs($context, 'group', 'description', $group->id); $group = file_prepare_standard_editor($group, 'description', $editoroptions, $context, 'group', 'description', $group->id); } else { $editoroptions['subdirs'] = false; $group = file_prepare_standard_editor($group, 'description', $editoroptions, $context, 'group', 'description', null); } /// First create the form $editform = new group_form(null, array('editoroptions' => $editoroptions, 'group' => $group)); $editform->set_data($group); if ($editform->is_cancelled()) { redirect($returnurl); } elseif ($data = $editform->get_data()) { if (!has_capability('moodle/course:changeidnumber', $context)) { // Remove the idnumber if the user doesn't have permission to modify it unset($data->idnumber); } if ($data->id) { groups_update_group($data, $editform, $editoroptions); } else { $id = groups_create_group($data, $editform, $editoroptions); $returnurl = $CFG->wwwroot.'/group/index.php?id='.$course->id.'&group='.$id; } redirect($returnurl); } $strgroups = get_string('groups'); $strparticipants = get_string('participants'); if ($id) { $strheading = get_string('editgroupsettings', 'group'); } else { $strheading = get_string('creategroup', 'group'); } $PAGE->navbar->add($strparticipants, new moodle_url('/user/index.php', array('id'=>$courseid))); $PAGE->navbar->add($strgroups, new moodle_url('/group/index.php', array('id'=>$courseid))); $PAGE->navbar->add($strheading); /// Print header echo $OUTPUT->header(); echo '<div id="grouppicture">'; if ($id) { print_group_picture($group, $course->id); } echo '</div>'; $editform->display(); echo $OUTPUT->footer(); customfield.php 0000644 00000002606 15215711721 0007601 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Manage group custom fields * * @package core_group * @author Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> * @copyright 2023 Catalyst IT Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_group\customfield\group_handler; use core_customfield\output\management; require_once('../config.php'); require_once($CFG->libdir . '/adminlib.php'); admin_externalpage_setup('group_customfield'); $output = $PAGE->get_renderer('core_customfield'); $handler = group_handler::create(); $outputpage = new management($handler); echo $output->header(), $output->heading(new lang_string('group_customfield', 'admin')), $output->render($outputpage), $output->footer(); upgrade.txt 0000644 00000004064 15215711721 0006742 0 ustar 00 === 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /group/*, information provided here is intended especially for developers. === 4.3 === * The following external methods now return group names correctly formatted: - `core_group_get_groups` - `core_group_get_course_groups` - `core_group_get_course_user_groups` - `core_group_get_activity_allowed_groups` * Groups now have access to create GeoPattern default images based upon their ID with their associated course context. This can be done by calling the following: moodle_url::make_pluginfile_url( $coursecontext->id, 'group', 'generated', $group->id, '/', 'group.svg' ); * Added group/grouping custom fields. * groups_get_members_join() now includes visibility checks for group memberships. * \core_group\visibility::sql_member_visibility_where() no longer prefixes the returned WHERE statement with AND, to give the calling code greater flexibility about how to use it. === 4.2 === * `\core_group\visibility` class added to support new `visibility` field in group records. This holds the visibility constants and helper functions for applying visibility restrictions when querying groups or group members in the database. * Changes to the group form to support visibility features: - New `visibility` field. - New `participation` field. - `participation` and `enablemessaging` fields are disabled (default: false) when `visibility` is set to `visibility::OWN` or `visibility::NONE`. * The following externallib functions now accept `visibility` and `participation` as optional parameters: - create_groups() - update_groups() * The following externallib functions now also return `visibility` and `participation` fields in their responses: - create_groups() - get_groups() - get_course_groups() === 3.11 === * The groups do not support 'hidepicture' any more, and so the column 'hidepicture' from the table {groups} has be dropped. assign.php 0000644 00000017175 15215711721 0006556 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Add/remove group from grouping. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ require_once('../config.php'); require_once('lib.php'); $groupingid = required_param('id', PARAM_INT); $PAGE->set_url('/group/assign.php', array('id'=>$groupingid)); if (!$grouping = $DB->get_record('groupings', array('id'=>$groupingid))) { throw new \moodle_exception('invalidgroupid'); } if (!$course = $DB->get_record('course', array('id'=>$grouping->courseid))) { throw new \moodle_exception('invalidcourse'); } $courseid = $course->id; require_login($course); $context = context_course::instance($courseid); require_capability('moodle/course:managegroups', $context); $returnurl = $CFG->wwwroot.'/group/groupings.php?id='.$courseid; if ($frm = data_submitted() and confirm_sesskey()) { if (isset($frm->cancel)) { redirect($returnurl); } else if (isset($frm->add) and !empty($frm->addselect)) { foreach ($frm->addselect as $groupid) { // Ask this method not to purge the cache, we'll do it ourselves afterwards. groups_assign_grouping($grouping->id, (int)$groupid, null, false); } // Invalidate the course groups cache seeing as we've changed it. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Invalidate the user_group_groupings cache, too. cache_helper::purge_by_definition('core', 'user_group_groupings'); } else if (isset($frm->remove) and !empty($frm->removeselect)) { foreach ($frm->removeselect as $groupid) { // Ask this method not to purge the cache, we'll do it ourselves afterwards. groups_unassign_grouping($grouping->id, (int)$groupid, false); } // Invalidate the course groups cache seeing as we've changed it. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Invalidate the user_group_groupings cache, too. cache_helper::purge_by_definition('core', 'user_group_groupings'); } } $currentmembers = array(); $potentialmembers = array(); if ($groups = $DB->get_records('groups', array('courseid'=>$courseid), 'name')) { if ($assignment = $DB->get_records('groupings_groups', array('groupingid'=>$grouping->id))) { foreach ($assignment as $ass) { $currentmembers[$ass->groupid] = $groups[$ass->groupid]; unset($groups[$ass->groupid]); } } $potentialmembers = $groups; } $currentmembersoptions = ''; $currentmemberscount = 0; if ($currentmembers) { foreach($currentmembers as $group) { $currentmembersoptions .= '<option value="' . $group->id . '." title="' . format_string($group->name) . '">' . format_string($group->name) . '</option>'; $currentmemberscount ++; } // Get course managers so they can be highlighted in the list if ($managerroles = get_config('', 'coursecontact')) { $coursecontactroles = explode(',', $managerroles); foreach ($coursecontactroles as $roleid) { $role = $DB->get_record('role', array('id'=>$roleid)); $managers = get_role_users($roleid, $context, true, 'u.id', 'u.id ASC'); } } } else { $currentmembersoptions .= '<option> </option>'; } $potentialmembersoptions = ''; $potentialmemberscount = 0; if ($potentialmembers) { foreach($potentialmembers as $group) { $potentialmembersoptions .= '<option value="' . $group->id . '." title="' . format_string($group->name) . '">' . format_string($group->name) . '</option>'; $potentialmemberscount ++; } } else { $potentialmembersoptions .= '<option> </option>'; } // Print the page and form $strgroups = get_string('groups'); $strparticipants = get_string('participants'); $straddgroupstogroupings = get_string('addgroupstogroupings', 'group'); $groupingname = format_string($grouping->name); navigation_node::override_active_url(new moodle_url('/group/index.php', array('id'=>$course->id))); $PAGE->set_pagelayout('admin'); $PAGE->navbar->add($strparticipants, new moodle_url('/user/index.php', array('id'=>$courseid))); $PAGE->navbar->add(get_string('groupings', 'group'), new moodle_url('/group/groupings.php', ['id' => $courseid])); $PAGE->navbar->add($straddgroupstogroupings); /// Print header $PAGE->set_title("$course->shortname: $strgroups"); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); ?> <div id="addmembersform"> <h3 class="main"><?php print_string('addgroupstogroupings', 'group'); echo ": $groupingname"; ?></h3> <form id="assignform" method="post" action=""> <div> <input type="hidden" name="sesskey" value="<?php p(sesskey()); ?>" /> <table summary="" class="generaltable generalbox groupmanagementtable boxaligncenter"> <tr> <td id="existingcell"> <label for="removeselect"><?php print_string('existingmembers', 'group', $currentmemberscount); ?></label> <div class="userselector" id="removeselect_wrapper"> <select name="removeselect[]" size="20" id="removeselect" multiple="multiple" onfocus="document.getElementById('assignform').add.disabled=true; document.getElementById('assignform').remove.disabled=false; document.getElementById('assignform').addselect.selectedIndex=-1;"> <?php echo $currentmembersoptions ?> </select></div></td> <td id="buttonscell"> <p class="arrow_button"> <input class="btn btn-secondary" name="add" id="add" type="submit" value="<?php echo $OUTPUT->larrow().' '.get_string('add'); ?>" title="<?php print_string('add'); ?>" /><br> <input class="btn btn-secondary" name="remove" id="remove" type="submit" value="<?php echo get_string('remove').' '.$OUTPUT->rarrow(); ?>" title="<?php print_string('remove'); ?>" /> </p> </td> <td id="potentialcell"> <label for="addselect"><?php print_string('potentialmembers', 'group', $potentialmemberscount); ?></label> <div class="userselector" id="addselect_wrapper"> <select name="addselect[]" size="20" id="addselect" multiple="multiple" onfocus="document.getElementById('assignform').add.disabled=false; document.getElementById('assignform').remove.disabled=true; document.getElementById('assignform').removeselect.selectedIndex=-1;"> <?php echo $potentialmembersoptions ?> </select> </div> </td> </tr> <tr><td colspan="3" id="backcell"> <input class="btn btn-secondary" type="submit" name="cancel" value="<?php print_string('backtogroupings', 'group'); ?>" /> </td></tr> </table> </div> </form> </div> <?php echo $OUTPUT->footer();
| ver. 1.4 |
Github
|
.
| PHP 8.3.30 | Generation time: 0.08 |
proxy
|
phpinfo
|
Settings