diff --git a/admin/settings/courses.php b/admin/settings/courses.php index ac91d531d5c0b..0bce68aa8049d 100644 --- a/admin/settings/courses.php +++ b/admin/settings/courses.php @@ -263,6 +263,27 @@ $CFG->wwwroot . '/course/pending.php', array('moodle/site:approvecourse'))); } + // Add a category for the Groups. + $ADMIN->add('courses', new admin_category('groups', new lang_string('groups'))); + $ADMIN->add( + 'groups', + new admin_externalpage( + 'group_customfield', + new lang_string('group_customfield', 'admin'), + $CFG->wwwroot . '/group/customfield.php', + ['moodle/group:configurecustomfields'] + ) + ); + $ADMIN->add( + 'groups', + new admin_externalpage( + 'grouping_customfield', + new lang_string('grouping_customfield', 'admin'), + $CFG->wwwroot . '/group/grouping_customfield.php', + ['moodle/group:configurecustomfields'] + ) + ); + // Add a category for the Activity Chooser. $ADMIN->add('courses', new admin_category('activitychooser', new lang_string('activitychoosercategory', 'course'))); $temp = new admin_settingpage('activitychoosersettings', new lang_string('activitychoosersettings', 'course')); diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index 00f6e79726f90..b718434c084b9 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -1354,6 +1354,10 @@ protected function define_structure() { 'name', 'idnumber', 'description', 'descriptionformat', 'enrolmentkey', 'picture', 'visibility', 'participation', 'timecreated', 'timemodified')); + $groupcustomfields = new backup_nested_element('groupcustomfields'); + $groupcustomfield = new backup_nested_element('groupcustomfield', ['id'], [ + 'shortname', 'type', 'value', 'valueformat', 'groupid']); + $members = new backup_nested_element('group_members'); $member = new backup_nested_element('group_member', array('id'), array( @@ -1365,6 +1369,10 @@ protected function define_structure() { 'name', 'idnumber', 'description', 'descriptionformat', 'configdata', 'timecreated', 'timemodified')); + $groupingcustomfields = new backup_nested_element('groupingcustomfields'); + $groupingcustomfield = new backup_nested_element('groupingcustomfield', ['id'], [ + 'shortname', 'type', 'value', 'valueformat', 'groupingid']); + $groupinggroups = new backup_nested_element('grouping_groups'); $groupinggroup = new backup_nested_element('grouping_group', array('id'), array( @@ -1373,12 +1381,16 @@ protected function define_structure() { // Build the tree $groups->add_child($group); + $groups->add_child($groupcustomfields); + $groupcustomfields->add_child($groupcustomfield); $groups->add_child($groupings); $group->add_child($members); $members->add_child($member); $groupings->add_child($grouping); + $groupings->add_child($groupingcustomfields); + $groupingcustomfields->add_child($groupingcustomfield); $grouping->add_child($groupinggroups); $groupinggroups->add_child($groupinggroup); @@ -1405,6 +1417,10 @@ protected function define_structure() { if ($userinfo) { $member->set_source_table('groups_members', array('groupid' => backup::VAR_PARENTID)); } + + $courseid = $this->task->get_courseid(); + $groupcustomfield->set_source_array($this->get_group_custom_fields_for_backup($courseid)); + $groupingcustomfield->set_source_array($this->get_grouping_custom_fields_for_backup($courseid)); } // Define id annotations (as final) @@ -1420,6 +1436,40 @@ protected function define_structure() { // Return the root element (groups) return $groups; } + + /** + * Get custom fields array for group + * @param int $courseid + * @return array + */ + protected function get_group_custom_fields_for_backup(int $courseid): array { + global $DB; + $handler = \core_group\customfield\group_handler::create(); + $fieldsforbackup = []; + if ($groups = $DB->get_records('groups', ['courseid' => $courseid], '', 'id')) { + foreach ($groups as $group) { + $fieldsforbackup = array_merge($fieldsforbackup, $handler->get_instance_data_for_backup($group->id)); + } + } + return $fieldsforbackup; + } + + /** + * Get custom fields array for grouping + * @param int $courseid + * @return array + */ + protected function get_grouping_custom_fields_for_backup(int $courseid): array { + global $DB; + $handler = \core_group\customfield\grouping_handler::create(); + $fieldsforbackup = []; + if ($groupings = $DB->get_records('groupings', ['courseid' => $courseid], '', 'id')) { + foreach ($groupings as $grouping) { + $fieldsforbackup = array_merge($fieldsforbackup, $handler->get_instance_data_for_backup($grouping->id)); + } + } + return $fieldsforbackup; + } } /** diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 9cbd36d7167ab..daf0382bca436 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -1159,7 +1159,10 @@ protected function define_structure() { $groupinfo = $this->get_setting_value('groups'); if ($groupinfo) { $paths[] = new restore_path_element('group', '/groups/group'); + $paths[] = new restore_path_element('groupcustomfield', '/groups/groupcustomfields/groupcustomfield'); $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping'); + $paths[] = new restore_path_element('groupingcustomfield', + '/groups/groupings/groupingcustomfields/groupingcustomfield'); $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group'); } return $paths; @@ -1225,6 +1228,18 @@ public function process_group($data) { cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid)); } + /** + * Restore group custom field values. + * @param array $data data for group custom field + * @return void + */ + public function process_groupcustomfield($data) { + $newgroup = $this->get_mapping('group', $data['groupid']); + $data['groupid'] = $newgroup->newitemid; + $handler = \core_group\customfield\group_handler::create(); + $handler->restore_instance_data_from_backup($this->task, $data); + } + public function process_grouping($data) { global $DB; @@ -1270,6 +1285,18 @@ public function process_grouping($data) { cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid)); } + /** + * Restore grouping custom field values. + * @param array $data data for grouping custom field + * @return void + */ + public function process_groupingcustomfield($data) { + $newgroup = $this->get_mapping('grouping', $data['groupingid']); + $data['groupingid'] = $newgroup->newitemid; + $handler = \core_group\customfield\grouping_handler::create(); + $handler->restore_instance_data_from_backup($this->task, $data); + } + public function process_grouping_group($data) { global $CFG; diff --git a/backup/tests/backup_restore_group_test.php b/backup/tests/backup_restore_group_test.php new file mode 100644 index 0000000000000..9814c5b0810d8 --- /dev/null +++ b/backup/tests/backup_restore_group_test.php @@ -0,0 +1,109 @@ +. + +namespace core_backup; + +use core_backup_backup_restore_base_testcase; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once('backup_restore_base_testcase.php'); +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + +/** + * Backup restore permission tests. + * + * @package core_backup + * @author Tomo Tsuyuki + * @copyright 2023 Catalyst IT Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_restore_group_test extends core_backup_backup_restore_base_testcase { + + /** + * Test for backup/restore with customfields. + * @covers \backup_groups_structure_step + * @covers \restore_groups_structure_step + */ + public function test_backup_restore_group_with_customfields(): void { + + $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, + 'name' => 'Test group 1', + 'customfield_testgroupcustomfield1' => 'Custom input for group1', + ]); + $grouping1 = self::getDataGenerator()->create_grouping([ + 'courseid' => $course1->id, + 'name' => 'Test grouping 1', + 'customfield_testgroupingcustomfield1' => 'Custom input for grouping1', + ]); + + // Perform backup and restore. + $backupid = $this->perform_backup($course1); + $this->perform_restore($backupid, $course2); + + // Test group. + $groups = groups_get_all_groups($course2->id); + $this->assertCount(1, $groups); + $group = reset($groups); + + // Confirm the group is not same group as original one. + $this->assertNotEquals($group1->id, $group->id); + $this->assertEquals($group1->name, $group->name); + + // Confirm custom field is restored in the new group. + $grouphandler = \core_group\customfield\group_handler::create(); + $data = $grouphandler->export_instance_data_object($group->id); + $this->assertSame('Custom input for group1', $data->testgroupcustomfield1); + + // Test grouping. + $groupings = groups_get_all_groupings($course2->id); + $this->assertCount(1, $groupings); + $grouping = reset($groupings); + + // Confirm this is not same grouping as original one. + $this->assertNotEquals($grouping1->id, $grouping->id); + + // Confirm custom field is restored in the new grouping. + $groupinghandler = \core_group\customfield\grouping_handler::create(); + $data = $groupinghandler->export_instance_data_object($grouping->id); + $this->assertSame('Custom input for grouping1', $data->testgroupingcustomfield1); + } +} diff --git a/group/classes/customfield/group_handler.php b/group/classes/customfield/group_handler.php new file mode 100644 index 0000000000000..af55ac1d3f694 --- /dev/null +++ b/group/classes/customfield/group_handler.php @@ -0,0 +1,185 @@ +. + +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 + * @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'), + 'groupid' => $data->get('instanceid'), + ]; + } + } + return $finalfields; + } + + /** + * Creates or updates custom field data. + * + * @param restore_task $task + * @param array $data + */ + 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('contextid', $context->id); + $d->save(); + } + return; + } + } + } +} diff --git a/group/classes/customfield/grouping_handler.php b/group/classes/customfield/grouping_handler.php new file mode 100644 index 0000000000000..58f16c360bc68 --- /dev/null +++ b/group/classes/customfield/grouping_handler.php @@ -0,0 +1,186 @@ +. + +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 + * @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'), + '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 + */ + 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('contextid', $context->id); + $d->save(); + } + return; + } + } + } +} diff --git a/group/customfield.php b/group/customfield.php new file mode 100644 index 0000000000000..f54ed3e1801d7 --- /dev/null +++ b/group/customfield.php @@ -0,0 +1,41 @@ +. + +/** + * Manage group custom fields + * + * @package core_group + * @author Tomo Tsuyuki + * @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(); diff --git a/group/externallib.php b/group/externallib.php index f94a8f30699cf..0d2d2713f74f7 100644 --- a/group/externallib.php +++ b/group/externallib.php @@ -24,6 +24,10 @@ use core_external\util; use core_group\visibility; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/group/lib.php'); + /** * Group external functions * @@ -80,6 +84,7 @@ public static function create_groups_parameters() { '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.' ) @@ -132,6 +137,14 @@ public static function create_groups($groups) { // 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)) { @@ -170,6 +183,7 @@ public static function create_groups_returns() { '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.' ); @@ -201,6 +215,7 @@ 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, ' @@ -223,6 +238,7 @@ public static function get_groups($groupids) { \core_external\util::format_text($group->description, $group->descriptionformat, $context, 'group', 'description', $group->id); + $group->customfields = $customfieldsdata[$group->id] ?? []; $groups[] = (array)$group; } @@ -250,6 +266,7 @@ public static function get_groups_returns() { '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(), ) ) ); @@ -633,7 +650,8 @@ public static function create_groupings_parameters() { '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) + '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.' ) @@ -682,6 +700,14 @@ public static function create_groupings($groupings) { $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; @@ -707,7 +733,8 @@ public static function create_groupings_returns() { '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') + '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.' ); @@ -729,7 +756,8 @@ public static function update_groupings_parameters() { '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) + '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.' ) @@ -786,6 +814,14 @@ public static function update_groupings($groupings) { // 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); } @@ -839,6 +875,7 @@ public static function get_groupings($groupingids, $returngroups = false) { '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); @@ -859,6 +896,7 @@ public static function get_groupings($groupingids, $returngroups = false) { \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']) { @@ -867,6 +905,7 @@ public static function get_groupings($groupingids, $returngroups = false) { "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, @@ -879,6 +918,11 @@ public static function get_groupings($groupingids, $returngroups = false) { '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; } @@ -905,6 +949,7 @@ public static function get_groupings_returns() { '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( @@ -914,7 +959,8 @@ public static function get_groupings_returns() { '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') + 'idnumber' => new external_value(PARAM_RAW, 'id number'), + 'customfields' => self::build_custom_fields_returns_structure(), ) ), 'optional groups', VALUE_OPTIONAL) @@ -1554,6 +1600,7 @@ public static function update_groups_parameters() { . '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.' ) @@ -1629,6 +1676,14 @@ public static function update_groups($groups) { $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); } @@ -1646,4 +1701,47 @@ public static function update_groups($groups) { 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; + } } diff --git a/group/group.php b/group/group.php index 311e746a2d31a..7eec451883744 100644 --- a/group/group.php +++ b/group/group.php @@ -93,7 +93,7 @@ } /// First create the form -$editform = new group_form(null, array('editoroptions'=>$editoroptions)); +$editform = new group_form(null, array('editoroptions' => $editoroptions, 'group' => $group)); $editform->set_data($group); if ($editform->is_cancelled()) { diff --git a/group/group_form.php b/group/group_form.php index 9e239c226600f..900bc822ebe59 100644 --- a/group/group_form.php +++ b/group/group_form.php @@ -47,6 +47,7 @@ function definition () { $mform =& $this->_form; $editoroptions = $this->_customdata['editoroptions']; + $group = $this->_customdata['group']; $mform->addElement('header', 'general', get_string('general', 'form')); @@ -99,6 +100,10 @@ function definition () { $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); @@ -153,6 +158,8 @@ public function definition_after_data() { $participation->freeze(); } + $handler = core_group\customfield\group_handler::create(); + $handler->instance_form_definition_after_data($this->_form, empty($groupid) ? 0 : $groupid); } /** @@ -216,6 +223,9 @@ function validation($data, $files) { } } + $handler = \core_group\customfield\group_handler::create(); + $errors = array_merge($errors, $handler->instance_form_validation($data, $files)); + return $errors; } diff --git a/group/grouping.php b/group/grouping.php index ec4d139862965..2b28780b05ede 100644 --- a/group/grouping.php +++ b/group/grouping.php @@ -108,7 +108,7 @@ } /// First create the form -$editform = new grouping_form(null, compact('editoroptions')); +$editform = new grouping_form(null, compact('editoroptions', 'grouping')); $editform->set_data($grouping); if ($editform->is_cancelled()) { diff --git a/group/grouping_customfield.php b/group/grouping_customfield.php new file mode 100644 index 0000000000000..883e281ef3daa --- /dev/null +++ b/group/grouping_customfield.php @@ -0,0 +1,41 @@ +. + +/** + * Manage grouping custom fields + * + * @package core_group + * @author Tomo Tsuyuki + * @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(); diff --git a/group/grouping_form.php b/group/grouping_form.php index eb597bb533888..8ffd98db58d8e 100644 --- a/group/grouping_form.php +++ b/group/grouping_form.php @@ -47,6 +47,7 @@ function definition () { $mform =& $this->_form; $editoroptions = $this->_customdata['editoroptions']; + $grouping = $this->_customdata['grouping']; $mform->addElement('header', 'general', get_string('general', 'form')); @@ -64,6 +65,10 @@ function definition () { $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); @@ -109,7 +114,18 @@ function validation($data, $files) { $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); + } } diff --git a/group/import.php b/group/import.php index b6c027d95a86e..26771039a62e8 100644 --- a/group/import.php +++ b/group/import.php @@ -82,7 +82,7 @@ // make arrays of valid fields for error checking $required = array("groupname" => 1); - $optionalDefaults = array("lang" => 1); + $optionaldefaults = array("lang" => 1); $optional = array("coursename" => 1, "idnumber" => 1, "groupidnumber" => 1, @@ -91,17 +91,34 @@ "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 + // Check for valid field names. foreach ($header as $i => $h) { - $h = trim($h); $header[$i] = $h; // remove whitespace - if (!(isset($required[$h]) or isset($optionalDefaults[$h]) or isset($optional[$h]))) { - throw new \moodle_exception('invalidfieldname', 'error', $PAGE->url, $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; } @@ -117,7 +134,7 @@ while ($line = $csvimport->next()) { $newgroup = new stdClass();//to make Martin happy - foreach ($optionalDefaults as $key => $value) { + foreach ($optionaldefaults as $key => $value) { $newgroup->$key = current_language(); //defaults to current language } foreach ($line as $key => $value) { @@ -211,6 +228,12 @@ $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 { diff --git a/group/lib.php b/group/lib.php index 55f8f070d67ba..ed44961f48a49 100644 --- a/group/lib.php +++ b/group/lib.php @@ -275,6 +275,9 @@ function groups_create_group($data, $editform = false, $editoroptions = false) { $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); @@ -352,6 +355,9 @@ function groups_create_grouping($data, $editoroptions=null) { $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; @@ -445,6 +451,9 @@ function groups_update_group($data, $editform = false, $editoroptions = false) { $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. @@ -529,6 +538,9 @@ function groups_update_grouping($data, $editoroptions=null) { } $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)); @@ -1222,3 +1234,61 @@ function set_groups_messaging(array $groupids, bool $enabled): void { 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; +} diff --git a/group/tests/behat/group_customfields.feature b/group/tests/behat/group_customfields.feature new file mode 100644 index 0000000000000..1cba8a5fb05b4 --- /dev/null +++ b/group/tests/behat/group_customfields.feature @@ -0,0 +1,73 @@ +@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" diff --git a/group/tests/behat/groups_import.feature b/group/tests/behat/groups_import.feature index 5f2fdc2c6ecef..8d807943622a5 100644 --- a/group/tests/behat/groups_import.feature +++ b/group/tests/behat/groups_import.feature @@ -146,3 +146,35 @@ Feature: Importing of groups and groupings 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" diff --git a/group/tests/customfield/group_handler_test.php b/group/tests/customfield/group_handler_test.php new file mode 100644 index 0000000000000..11b3f9b978070 --- /dev/null +++ b/group/tests/customfield/group_handler_test.php @@ -0,0 +1,186 @@ +. + +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 + * @copyright 2023 Catalyst IT Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group_handler_test extends advanced_testcase { + /** + * Test custom field handler. + * @var group_handler + */ + protected $handler; + + /** + * Setup. + */ + public function setUp(): void { + $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() { + $this->assertInstanceOf(context_system::class, $this->handler->get_configuration_context()); + } + + /** + * Test getting config URL. + */ + public function test_get_configuration_url() { + $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() { + 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() { + $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() { + $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() { + $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)); + } +} diff --git a/group/tests/customfield/grouping_handler_test.php b/group/tests/customfield/grouping_handler_test.php new file mode 100644 index 0000000000000..152d2ccfa7d45 --- /dev/null +++ b/group/tests/customfield/grouping_handler_test.php @@ -0,0 +1,186 @@ +. + +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 + * @copyright 2023 Catalyst IT Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class grouping_handler_test extends advanced_testcase { + /** + * Test custom field handler. + * @var \core_customfield\handler + */ + protected $handler; + + /** + * Setup. + */ + public function setUp(): void { + $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() { + $this->assertInstanceOf(context_system::class, $this->handler->get_configuration_context()); + } + + /** + * Test getting config URL. + */ + public function test_get_configuration_url() { + $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() { + 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() { + $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() { + $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() { + $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)); + } +} diff --git a/group/tests/externallib_test.php b/group/tests/externallib_test.php index b19ebc0d752d9..e185e4cc635d3 100644 --- a/group/tests/externallib_test.php +++ b/group/tests/externallib_test.php @@ -16,7 +16,10 @@ 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; @@ -36,9 +39,45 @@ * @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 */ 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 */ @@ -129,6 +168,41 @@ public function test_create_groups() { $froups = core_group_external::create_groups(array($group4)); } + /** + * Test create_groups with custom fields. + */ + public function test_create_groups_with_customfields() { + 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. * @@ -240,6 +314,35 @@ public function test_update_groups() { $groups = core_group_external::update_groups(array($group1data)); } + /** + * Test update_groups with custom fields. + */ + public function test_update_groups_with_customfields() { + $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. * @@ -411,6 +514,33 @@ public function test_get_groups() { $groups = core_group_external::get_groups(array($group1->id, $group2->id)); } + /** + * Test get_groups with customfields. + */ + public function test_get_groups_with_customfields() { + $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 */ @@ -519,6 +649,73 @@ public function test_create_update_groupings() { } } + /** + * Test create_groupings with custom fields. + */ + public function test_create_groupings_with_customfields() { + 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() { + $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 */ @@ -596,6 +793,56 @@ public function test_get_groupings() { } } + /** + * Test get_groupings with customfields. + */ + public function test_get_groupings_with_customfields() { + $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. */ diff --git a/group/tests/fixtures/groups_import_with_customfield.csv b/group/tests/fixtures/groups_import_with_customfield.csv new file mode 100644 index 0000000000000..c0e7196bf6eaf --- /dev/null +++ b/group/tests/fixtures/groups_import_with_customfield.csv @@ -0,0 +1,3 @@ +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 diff --git a/group/tests/lib_test.php b/group/tests/lib_test.php index 81642cc514873..6ee7623d00ae9 100644 --- a/group/tests/lib_test.php +++ b/group/tests/lib_test.php @@ -27,6 +27,7 @@ global $CFG; require_once($CFG->dirroot . '/group/lib.php'); +require_once($CFG->dirroot . '/lib/grouplib.php'); /** * Group lib testcase. @@ -439,6 +440,95 @@ public function test_groups_delete_groupings() { $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() { + $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 () { global $DB; $this->resetAfterTest(); diff --git a/group/upgrade.txt b/group/upgrade.txt index 10cc5c1167f28..32ca37a7409bb 100644 --- a/group/upgrade.txt +++ b/group/upgrade.txt @@ -17,6 +17,7 @@ information provided here is intended especially for developers. '/', 'group.svg' ); +* Added group/grouping custom fields. === 4.2 === * `\core_group\visibility` class added to support new `visibility` field in group records. This holds the visibility constants diff --git a/lang/en/admin.php b/lang/en/admin.php index b1fa8955e19df..6ede2d0f9cf58 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -697,6 +697,8 @@ $string['globalsearchmanage'] = 'Manage global search'; $string['groupenrolmentkeypolicy'] = 'Group enrolment key policy'; $string['groupenrolmentkeypolicy_desc'] = 'If enabled, group enrolment keys will be checked against the password policy as specified in the settings above.'; +$string['grouping_customfield'] = 'Grouping custom fields'; +$string['group_customfield'] = 'Group custom fields'; $string['googlemapkey3'] = 'Google Maps API V3 key'; $string['googlemapkey3_help'] = 'You need to enter a special key to use Google Maps for IP address lookup visualization. You can obtain the key free of charge at https://developers.google.com/maps/documentation/javascript/tutorial#api_key'; $string['gotofirst'] = 'Go to first missing string'; diff --git a/lang/en/role.php b/lang/en/role.php index 27ab547709653..596d4f44b4d5b 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -280,6 +280,7 @@ $string['grade:view'] = 'View own grades'; $string['grade:viewall'] = 'View grades of other users'; $string['grade:viewhidden'] = 'View hidden grades for owner'; +$string['group:configurecustomfields'] = 'Configure group/grouping custom fields'; $string['h5p:deploy'] = 'Deploy H5P content'; $string['h5p:updatelibraries'] = 'Manage H5P content types'; $string['h5p:setdisplayoptions'] = 'Set H5P display options'; diff --git a/lib/db/access.php b/lib/db/access.php index 0ec36384501e8..5bd73780e7979 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -788,6 +788,13 @@ 'clonepermissionsfrom' => 'moodle/site:config' ), + 'moodle/group:configurecustomfields' => array( + 'riskbitmask' => RISK_SPAM, + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/course:create' => array( 'riskbitmask' => RISK_XSS, diff --git a/lib/grouplib.php b/lib/grouplib.php index a6f9388fd1aad..181b508fb4368 100644 --- a/lib/grouplib.php +++ b/lib/grouplib.php @@ -211,9 +211,14 @@ function groups_get_grouping_by_idnumber($courseid, $idnumber) { * @return bool|stdClass group object or false if not found * @throws dml_exception */ -function groups_get_group($groupid, $fields='*', $strictness=IGNORE_MISSING) { +function groups_get_group($groupid, $fields = '*', $strictness = IGNORE_MISSING, $withcustomfields = false) { global $DB; - return $DB->get_record('groups', array('id'=>$groupid), $fields, $strictness); + $group = $DB->get_record('groups', ['id' => $groupid], $fields, $strictness); + if ($withcustomfields) { + $customfieldsdata = get_group_custom_fields_data([$groupid]); + $group->customfields = $customfieldsdata[$groupid] ?? []; + } + return $group; } /** @@ -225,9 +230,14 @@ function groups_get_group($groupid, $fields='*', $strictness=IGNORE_MISSING) { * @param int $strictness (IGNORE_MISSING - default) * @return stdClass group object */ -function groups_get_grouping($groupingid, $fields='*', $strictness=IGNORE_MISSING) { +function groups_get_grouping($groupingid, $fields='*', $strictness=IGNORE_MISSING, $withcustomfields = false) { global $DB; - return $DB->get_record('groupings', array('id'=>$groupingid), $fields, $strictness); + $grouping = $DB->get_record('groupings', ['id' => $groupingid], $fields, $strictness); + if ($withcustomfields) { + $customfieldsdata = get_grouping_custom_fields_data([$groupingid]); + $grouping->customfields = $customfieldsdata[$groupingid] ?? []; + } + return $grouping; } /** diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 5be1b2604b748..59b92725a267a 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -261,6 +261,12 @@ public static function reset_all_data($detectchanges = false) { if (class_exists('\core_cohort\customfield\cohort_handler')) { \core_cohort\customfield\cohort_handler::reset_caches(); } + if (class_exists('\core_group\customfield\group_handler')) { + \core_group\customfield\group_handler::reset_caches(); + } + if (class_exists('\core_group\customfield\grouping_handler')) { + \core_group\customfield\grouping_handler::reset_caches(); + } // Clear static cache within restore. if (class_exists('restore_section_structure_step')) { diff --git a/version.php b/version.php index 268826e82e4ca..31b908930c300 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2023080400.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2023080400.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.3dev (Build: 20230804)'; // Human-friendly version name