diff --git a/config/install/field.storage.group_content.group_requires_approval.yml b/config/install/field.storage.group_content.group_requires_approval.yml new file mode 100644 index 0000000..cfdc8c1 --- /dev/null +++ b/config/install/field.storage.group_content.group_requires_approval.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: + module: + - group +id: group_content.group_requires_approval +field_name: group_requires_approval +entity_type: group_content +type: boolean +settings: { } +module: core +locked: true +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: true +custom_storage: false diff --git a/config/optional/views.view.group_members.yml b/config/optional/views.view.group_members.yml index be48c75..9c4b6a6 100644 --- a/config/optional/views.view.group_members.yml +++ b/config/optional/views.view.group_members.yml @@ -625,7 +625,43 @@ display: created: '0' destination: true plugin_id: dropbutton - filters: { } + filters: + group_requires_approval_value: + id: group_requires_approval_value + table: group_content__group_requires_approval + field: group_requires_approval_value + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '0' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: boolean sorts: { } header: { } footer: { } diff --git a/config/optional/views.view.group_pending_members.yml b/config/optional/views.view.group_pending_members.yml new file mode 100644 index 0000000..14d7f66 --- /dev/null +++ b/config/optional/views.view.group_pending_members.yml @@ -0,0 +1,542 @@ +langcode: en +status: true +dependencies: + module: + - group + - user +id: group_pending_members +label: 'Pending Group members' +module: group +description: '' +tag: '' +base_table: group_content_field_data +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: group_permission + options: + group_permission: 'administer members' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 50 + offset: 0 + id: 0 + total_pages: null + tags: + previous: ‹‹ + next: ›› + first: '« First' + last: 'Last »' + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + quantity: 9 + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: true + caption: '' + summary: '' + description: '' + columns: + name: name + group_roles: group_roles + changed: changed + created: created + view_group_content: view_group_content + edit_group_content: edit_group_content + delete_group_content: delete_group_content + dropbutton: dropbutton + info: + name: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + group_roles: + align: '' + separator: '' + empty_column: false + responsive: '' + changed: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + created: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + view_group_content: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + edit_group_content: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + delete_group_content: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + dropbutton: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + default: '-1' + empty_table: true + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + name: + id: name + table: users_field_data + field: name + relationship: gc__user + group_type: group + admin_label: '' + label: User + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: user_name + settings: + link_to_entity: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: user + entity_field: name + plugin_id: field + changed: + id: changed + table: group_content_field_data + field: changed + relationship: none + group_type: group + admin_label: '' + label: Updated + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: timestamp + settings: + date_format: short + custom_date_format: '' + timezone: '' + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: group_content + entity_field: changed + plugin_id: field + created: + id: created + table: group_content_field_data + field: created + relationship: none + group_type: group + admin_label: '' + label: 'Requested Membership' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: timestamp + settings: + date_format: short + custom_date_format: '' + timezone: '' + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: group_content + entity_field: created + plugin_id: field + operations: + id: operations + table: group_content + field: operations + relationship: none + group_type: group + admin_label: '' + label: 'Operations links' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + destination: true + entity_type: group_content + plugin_id: entity_operations + filters: + group_requires_approval_value: + id: group_requires_approval_value + table: group_content__group_requires_approval + field: group_requires_approval_value + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '1' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: boolean + sorts: { } + header: { } + footer: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + empty: true + tokenize: false + content: 'No members available.' + plugin_id: text_custom + relationships: + gc__user: + id: gc__user + table: group_content_field_data + field: gc__user + relationship: none + group_type: group + admin_label: 'Member account' + required: true + group_content_plugins: + group_membership: group_membership + entity_type: group_content + plugin_id: group_content_to_entity + arguments: + gid: + id: gid + table: group_content_field_data + field: gid + relationship: none + group_type: group + admin_label: '' + default_action: 'access denied' + exception: + value: all + title_enable: false + title: All + title_enable: true + title: '{{ arguments.gid|placeholder }} pending members' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + break_phrase: false + not: false + entity_type: group_content + entity_field: gid + plugin_id: numeric + display_extenders: { } + title: 'Pending Members' + cache_metadata: + max-age: 0 + contexts: + - group_membership.roles.permissions + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: group/%group/pending-members + menu: + type: tab + title: 'Pending Members' + description: '' + expanded: false + parent: '' + weight: 20 + context: '0' + menu_name: main + cache_metadata: + max-age: 0 + contexts: + - group_membership.roles.permissions + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + tags: { } diff --git a/group.install b/group.install index 49d1dd4..bf53974 100644 --- a/group.install +++ b/group.install @@ -10,6 +10,12 @@ use Drupal\Core\Config\ExtensionInstallStorage; use Drupal\Core\Config\InstallStorage; use Drupal\Core\Entity\EntityTypeListenerInterface; use Drupal\Core\Entity\Sql\SqlContentEntityStorage; +use Drupal\Core\Config\FileStorage; +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\views\Entity\View; /** * Resave all GroupContent labels and remove orphaned entities. @@ -49,7 +55,10 @@ function group_update_8001(&$sandbox) { $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); // Show a status update for the current progress. - return t("Updated the label for @progress out of @max group content entities.", ['@progress' => $sandbox['progress'], '@max' => $sandbox['max']]); + return t("Updated the label for @progress out of @max group content entities.", [ + '@progress' => $sandbox['progress'], + '@max' => $sandbox['max'] + ]); } /** @@ -154,7 +163,8 @@ function group_update_8002() { // Now we just need to migrate the old data into the new table structure. We // read the column names from both the old and new tables and select data // from the old one into the new ones. - $temp_cols = $old_storage->getTableMapping($old_field_def)->getAllColumns($base_table); + $temp_cols = $old_storage->getTableMapping($old_field_def) + ->getAllColumns($base_table); $base_cols = $new_storage->getTableMapping()->getAllColumns($base_table); $data_cols = $new_storage->getTableMapping()->getAllColumns($data_table); @@ -163,8 +173,10 @@ function group_update_8002() { $data_shared = array_intersect($data_cols, $temp_cols); // Build subqueries for inserting old data into the new tables. - $base_query = $database->select($temp_table, 't')->fields('t', $base_shared); - $data_query = $database->select($temp_table, 't')->fields('t', $data_shared); + $base_query = $database->select($temp_table, 't') + ->fields('t', $base_shared); + $data_query = $database->select($temp_table, 't') + ->fields('t', $data_shared); // We add a default value of 1 to the 'default_langcode' field. $data_query->addExpression('1', 'default_langcode'); @@ -225,7 +237,7 @@ function group_update_8005() { foreach ($config_factory->listAll('group.type.') as $group_type_config_name) { $group_type = $config_factory->getEditable($group_type_config_name); - list(,,$group_type_id) = explode('.', $group_type_config_name); + list(, , $group_type_id) = explode('.', $group_type_config_name); // Make sure the group type ID is set in the plugin config. $plugins = $group_type->get('content'); @@ -247,7 +259,7 @@ function group_update_8006() { // Get the configuration from every group type. foreach ($config_factory->listAll('group.type.') as $group_type_config_name) { $group_type = $config_factory->getEditable($group_type_config_name); - list(,,$group_type_id) = explode('.', $group_type_config_name); + list(, , $group_type_id) = explode('.', $group_type_config_name); // Store the group type's plugin configuration in an array. $plugins = $group_type->get('content'); @@ -424,3 +436,86 @@ function group_update_8014() { $group_type->save(TRUE); } } + +/** + * Add group_requires_approval field to group membership plugin instances. + */ +function group_update_8015() { + $config_factory = \Drupal::configFactory(); + + FieldStorageConfig::create([ + 'entity_type' => 'group_content', + 'type' => 'boolean', + 'field_name' => 'group_requires_approval', + 'settings' => [], + ])->save(); + + foreach ($config_factory->listAll('group.content_type.') as $group_content_type_config_name) { + $group_content_type = $config_factory->getEditable($group_content_type_config_name); + $plugin_id = $group_content_type->get('content_plugin'); + + // Only group_membership types need the group_requires_approval field. + if ($plugin_id == 'group_membership') { + $group_content_type_id = $group_content_type->get('id'); + + // Add the group_requires_approval field to the group content type. + // The field storage for this is defined in the config/install folder. + FieldConfig::create([ + 'field_storage' => FieldStorageConfig::loadByName('group_content', 'group_requires_approval'), + 'bundle' => $group_content_type_id, + 'label' => t('Requires Approval'), + 'settings' => [ + 'on_label' => 'Requires Approval', + 'off_label' => 'Requires Approval', + ], + ])->save(); + + // Build the 'default' display ID for both the entity form and view mode. + $default_display_id = "group_content.$group_content_type_id.default"; + + // Build or retrieve the 'default' form mode. + if (!$form_display = EntityFormDisplay::load($default_display_id)) { + $form_display = EntityFormDisplay::create([ + 'targetEntityType' => 'group_content', + 'bundle' => $group_content_type_id, + 'mode' => 'default', + 'status' => TRUE, + ]); + } + + // Build or retrieve the 'default' view mode. + if (!$view_display = EntityViewDisplay::load($default_display_id)) { + $view_display = EntityViewDisplay::create([ + 'targetEntityType' => 'group_content', + 'bundle' => $group_content_type_id, + 'mode' => 'default', + 'status' => TRUE, + ]); + } + + // Assign widget settings for the 'default' form mode. + $form_display->setComponent('group_requires_approval', [ + 'type' => 'boolean_checkbox', + ])->save(); + + // Assign display settings for the 'default' view mode. + $view_display->setComponent('group_requires_approval', [ + 'label' => 'above', + 'type' => 'boolean', + ])->save(); + } + } + + // Create the new view if Views is installed. + $module_handler = \Drupal::moduleHandler(); + if ($module_handler->moduleExists('views') && !View::load('group_pending_members')) { + $optional_install_path = $module_handler->getModule('group') + ->getPath() . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY; + $storage = new FileStorage($optional_install_path); + + \Drupal::entityTypeManager() + ->getStorage('view') + ->create($storage->read('views.view.group_pending_members')) + ->save(); + } +} diff --git a/group.module b/group.module index 046b178..e980752 100644 --- a/group.module +++ b/group.module @@ -219,7 +219,7 @@ function group_entity_field_access($operation, FieldDefinitionInterface $field_d } // We only care about the group_roles field when on a form. - if ($field_definition->getName() != 'group_roles' || $operation !== 'edit') { + if (($field_definition->getName() != 'group_roles' || $operation !== 'edit') && ($field_definition->getName() != 'group_requires_approval')) { return AccessResult::neutral(); } diff --git a/group.routing.yml b/group.routing.yml index b49e5a0..3d3dba7 100644 --- a/group.routing.yml +++ b/group.routing.yml @@ -26,6 +26,39 @@ entity.group.leave: _group_permission: 'leave group' _group_member: 'TRUE' +entity.group.request_membership: + path: '/group/{group}/request-membership' + defaults: + _controller: '\Drupal\group\Controller\GroupMembershipController::requestMembership' + _title_callback: '\Drupal\group\Controller\GroupMembershipController::requestMembershipTitle' + requirements: + _group_permission: 'request group membership' + _group_member: 'FALSE' + +entity.group_content.approve_membership: + path: '/group/{group}/content/{group_content}/approve-membership' + defaults: + _controller: '\Drupal\group\Controller\GroupMembershipController::approveMembership' + _title_callback: '\Drupal\group\Controller\GroupMembershipController::approveMembershipTitle' + requirements: + _group_permission: 'administer members' + options: + parameters: + group: + type: 'entity:group' + +entity.group_content.reject_membership: + path: '/group/{group}/content/{group_content}/reject-membership' + defaults: + _controller: '\Drupal\group\Controller\GroupMembershipController::rejectMembership' + _title_callback: '\Drupal\group\Controller\GroupMembershipController::rejectMembershipTitle' + requirements: + _group_permission: 'administer members' + options: + parameters: + group: + type: 'entity:group' + # Group type entity routes. # Common entity routes are generated by \Drupal\group\Entity\Routing\GroupTypeRouteProvider. entity.group_type.permissions_form: diff --git a/src/Controller/GroupMembershipController.php b/src/Controller/GroupMembershipController.php index 75ad79f..cf455eb 100644 --- a/src/Controller/GroupMembershipController.php +++ b/src/Controller/GroupMembershipController.php @@ -23,14 +23,14 @@ class GroupMembershipController extends ControllerBase { * @var \Drupal\Core\Session\AccountInterface */ protected $currentUser; - + /** * The entity form builder. * * @var \Drupal\Core\Entity\EntityFormBuilderInterface */ protected $entityFormBuilder; - + /** * Constructs a new GroupMembershipController. * @@ -72,6 +72,7 @@ class GroupMembershipController extends ControllerBase { 'type' => $plugin->getContentTypeConfigId(), 'gid' => $group->id(), 'entity_id' => $this->currentUser->id(), + 'group_requires_approval' => 0, ]); return $this->entityFormBuilder->getForm($group_content, 'group-join'); @@ -91,6 +92,99 @@ class GroupMembershipController extends ControllerBase { } /** + * Provides the form for requesting a group membership. + * + * @param \Drupal\group\Entity\GroupInterface $group + * The group to request membership of. + * + * @return array + * A group membership request form. + */ + public function requestMembership(GroupInterface $group) { + /** @var \Drupal\group\Plugin\GroupContentEnablerInterface $plugin */ + $plugin = $group->getGroupType()->getContentPlugin('group_membership'); + + // Pre-populate a group membership with the current user. + $group_content = GroupContent::create([ + 'type' => $plugin->getContentTypeConfigId(), + 'gid' => $group->id(), + 'entity_id' => $this->currentUser->id(), + 'group_requires_approval' => 1, + ]); + + return $this->entityFormBuilder()->getForm($group_content, 'group-request-membership'); + } + + /** + * The _title_callback for the request membership form route. + * + * @param \Drupal\group\Entity\GroupInterface $group + * The group to request membership of. + * + * @return string + * The page title. + */ + public function requestMembershipTitle(GroupInterface $group) { + return $this->t('Request membership group %label', ['%label' => $group->label()]); + } + + /** + * Provides the form for approving a requested group membership. + * + * @param \Drupal\group\Entity\GroupInterface $group + * The group containing the requested membership. + * @param \Drupal\group\Entity\GroupContent $group_content + * The requested group membership. + * + * @return array + * A group membership approval form. + */ + public function approveMembership(GroupInterface $group, GroupContent $group_content) { + return $this->entityFormBuilder()->getForm($group_content, 'group-approve-membership'); + } + + /** + * The _title_callback for the approve requested membership form route. + * + * @param \Drupal\group\Entity\GroupInterface $group + * The group containing the requested membership. + * + * @return string + * The page title. + */ + public function approveMembershipTitle(GroupInterface $group) { + return $this->t('Approve membership request for group %label', ['%label' => $group->label()]); + } + + /** + * Provides the form for rejecting a requested group membership. + * + * @param \Drupal\group\Entity\GroupInterface $group + * The group containing the requested membership. + * @param \Drupal\group\Entity\GroupContent $group_content + * The requested group membership. + * + * @return array + * A group membership rejection form. + */ + public function rejectMembership(GroupInterface $group, GroupContent $group_content) { + return $this->entityFormBuilder()->getForm($group_content, 'group-reject-membership'); + } + + /** + * The _title_callback for the reject requested membership form route. + * + * @param \Drupal\group\Entity\GroupInterface $group + * The group containing the requested membership. + * + * @return string + * The page title. + */ + public function rejectMembershipTitle(GroupInterface $group) { + return $this->t('Reject membership request for group %label', ['%label' => $group->label()]); + } + + /** * Provides the form for leaving a group. * * @param \Drupal\group\Entity\GroupInterface $group diff --git a/src/Entity/Controller/GroupContentListBuilder.php b/src/Entity/Controller/GroupContentListBuilder.php index 4a788df..0d80536 100644 --- a/src/Entity/Controller/GroupContentListBuilder.php +++ b/src/Entity/Controller/GroupContentListBuilder.php @@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Routing\RedirectDestinationInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -134,6 +135,34 @@ class GroupContentListBuilder extends EntityListBuilder { /** @var \Drupal\group\Entity\GroupContentInterface $entity */ $operations = parent::getDefaultOperations($entity); + // Display membership approve / reject operation links for group content + // with a populated 'group_requires_approval' field. + $group_requires_approval = ($entity->hasField('group_requires_approval')) ? $entity->get('group_requires_approval') : NULL; + + if (!empty($group_requires_approval) && !empty($group_requires_approval->getString())) { + if ($entity->access('update') && $entity->hasLinkTemplate('edit-form')) { + $operations['approve'] = array( + 'title' => $this->t('Approve Membership'), + 'weight' => 0, + 'url' => Url::fromRoute('entity.group_content.approve_membership', array( + 'group' => $entity->getGroup()->id(), + 'group_content' => $entity->id(), + )), + ); + } + + if ($entity->access('delete') && $entity->hasLinkTemplate('delete-form')) { + $operations['reject'] = array( + 'title' => $this->t('Reject Membership'), + 'weight' => 1, + 'url' => Url::fromRoute('entity.group_content.reject_membership', array( + 'group' => $entity->getGroup()->id(), + 'group_content' => $entity->id(), + )), + ); + } + } + // Improve the edit and delete operation labels. if (isset($operations['edit'])) { $operations['edit']['title'] = $this->t('Edit relation'); diff --git a/src/Entity/Form/GroupMembershipApproveForm.php b/src/Entity/Form/GroupMembershipApproveForm.php new file mode 100644 index 0000000..80f6722 --- /dev/null +++ b/src/Entity/Form/GroupMembershipApproveForm.php @@ -0,0 +1,72 @@ +getEntity(); + return $group_content->getContentPlugin(); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to approve %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelURL() { + /** @var \Drupal\group\Entity\GroupContent $group_content */ + $group_content = $this->getEntity(); + $group = $group_content->getGroup(); + $route_params = [ + 'group' => $group->id(), + 'group_content' => $group_content->id(), + ]; + return new Url('entity.group_content.canonical', $route_params); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Approve'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\group\Entity\GroupContent $group_content */ + $group_content = $this->getEntity(); + $group_content->group_requires_approval = 0; + $group_content->save(); + + \Drupal::logger('group_content')->notice('@type: approved %title.', [ + '@type' => $group_content->bundle(), + '%title' => $group_content->label(), + ]); + + $route_params = ['group' => $group_content->getGroup()->id(), 'group_content' => $group_content->id()]; + $form_state->setRedirect('entity.group_content.canonical', $route_params); + } + +} diff --git a/src/Entity/Form/GroupMembershipRejectForm.php b/src/Entity/Form/GroupMembershipRejectForm.php new file mode 100644 index 0000000..247ef94 --- /dev/null +++ b/src/Entity/Form/GroupMembershipRejectForm.php @@ -0,0 +1,69 @@ +getEntity(); + return $group_content->getContentPlugin(); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to reject %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelURL() { + /** @var \Drupal\group\Entity\GroupContent $group_content */ + $group_content = $this->getEntity(); + $group = $group_content->getGroup(); + $route_params = [ + 'group' => $group->id(), + 'group_content' => $group_content->id(), + ]; + return new Url('entity.group_content.canonical', $route_params); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Reject'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $group_content = $this->getEntity(); + $group_content->delete(); + + \Drupal::logger('group_content')->notice('@type: rejected %title.', [ + '@type' => $this->entity->bundle(), + '%title' => $this->entity->label(), + ]); + + $form_state->setRedirect('entity.group.canonical', ['group' => $group_content->getGroup()->id()]); + } + +} diff --git a/src/Entity/Group.php b/src/Entity/Group.php index 0bbea96..2f9d35e 100644 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -187,6 +187,11 @@ class Group extends ContentEntityBase implements GroupInterface { */ public function addMember(UserInterface $account, $values = []) { if (!$this->getMember($account)) { + // Default to no membership approval required if value is missing. + if (!isset($values['group_requires_approval'])) { + $values['group_requires_approval'] = 0; + } + $this->addContent($account, 'group_membership', $values); } } diff --git a/src/Entity/GroupContent.php b/src/Entity/GroupContent.php index e94d533..c27c2e5 100644 --- a/src/Entity/GroupContent.php +++ b/src/Entity/GroupContent.php @@ -39,6 +39,9 @@ use Drupal\user\UserInterface; * "delete" = "Drupal\group\Entity\Form\GroupContentDeleteForm", * "group-join" = "Drupal\group\Form\GroupJoinForm", * "group-leave" = "Drupal\group\Form\GroupLeaveForm", + * "group-request-membership" = "Drupal\group\Form\GroupRequestMembershipForm", + * "group-approve-membership" = "Drupal\group\Entity\Form\GroupMembershipApproveForm", + * "group-reject-membership" = "Drupal\group\Entity\Form\GroupMembershipRejectForm" * }, * "access" = "Drupal\group\Entity\Access\GroupContentAccessControlHandler", * }, diff --git a/src/Entity/Storage/GroupRoleStorage.php b/src/Entity/Storage/GroupRoleStorage.php index 4bd9cbc..822f932 100644 --- a/src/Entity/Storage/GroupRoleStorage.php +++ b/src/Entity/Storage/GroupRoleStorage.php @@ -101,8 +101,9 @@ class GroupRoleStorage extends ConfigEntityStorage implements GroupRoleStorageIn if ($include_implied) { $group_type = $group->getGroupType(); - if ($membership !== FALSE) { - $ids[] = $group_type->getMemberRoleId(); + // Members who require approval do not receive the member role. + if (($membership !== FALSE) && !$membership->requiresApproval()) { + $ids[] = $group->getGroupType()->getMemberRoleId(); } elseif ($account->isAnonymous()) { $ids[] = $group_type->getAnonymousRoleId(); diff --git a/src/Form/GroupJoinForm.php b/src/Form/GroupJoinForm.php index 8cf1a11..898c7f1 100644 --- a/src/Form/GroupJoinForm.php +++ b/src/Form/GroupJoinForm.php @@ -17,6 +17,9 @@ class GroupJoinForm extends GroupContentForm { $form = parent::form($form, $form_state); $form['entity_id']['#access'] = FALSE; $form['group_roles']['#access'] = FALSE; + // Remove group_requires_approval so default value isn't overwritten. + // @see Drupal\group\Controller\GroupMembershipController::join() + unset($form['group_requires_approval']); return $form; } diff --git a/src/Form/GroupRequestMembershipForm.php b/src/Form/GroupRequestMembershipForm.php new file mode 100644 index 0000000..e17ed33 --- /dev/null +++ b/src/Form/GroupRequestMembershipForm.php @@ -0,0 +1,51 @@ +t('Request membership'); + return $actions; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $return = parent::save($form, $form_state); + + /** @var \Drupal\group\Entity\GroupContent $group_content */ + $group_content = $this->getEntity(); + + // Redirect back to the group because we've not yet been added. + $route_params = ['group' => $group_content->getGroup()->id()]; + $form_state->setRedirect('entity.group.canonical', $route_params); + + drupal_set_message(t('You membership request for @group has been sent', ['@group' => $group_content->getGroup()->label()]), 'status'); + + return $return; + } + +} diff --git a/src/GroupMembership.php b/src/GroupMembership.php index 08c8f26..5e41674 100644 --- a/src/GroupMembership.php +++ b/src/GroupMembership.php @@ -79,6 +79,16 @@ class GroupMembership implements CacheableDependencyInterface { } /** + * Returns the approval required status for the membership. + * + * @return bool + * Whether the member requires approval to join the group. + */ + public function requiresApproval() { + return (!empty($this->getGroupContent()->group_requires_approval->value)); + } + + /** * Checks whether the member has a permission. * * @param string $permission diff --git a/src/Plugin/GroupContentEnabler/GroupMembership.php b/src/Plugin/GroupContentEnabler/GroupMembership.php index d8a5673..3cbe0aa 100644 --- a/src/Plugin/GroupContentEnabler/GroupMembership.php +++ b/src/Plugin/GroupContentEnabler/GroupMembership.php @@ -37,7 +37,9 @@ class GroupMembership extends GroupContentEnablerBase { $account = \Drupal::currentUser(); $operations = []; - if ($group->getMember($account)) { + $member = $group->getMember($account); + + if (!empty($member)) { if ($group->hasPermission('leave group', $account)) { $operations['group-leave'] = [ 'title' => $this->t('Leave group'), @@ -46,12 +48,22 @@ class GroupMembership extends GroupContentEnablerBase { ]; } } - elseif ($group->hasPermission('join group', $account)) { - $operations['group-join'] = [ - 'title' => $this->t('Join group'), - 'url' => new Url('entity.group.join', ['group' => $group->id()]), - 'weight' => 0, - ]; + else { + if ($group->hasPermission('join group', $account)) { + $operations['group-join'] = [ + 'title' => $this->t('Join group'), + 'url' => new Url('entity.group.join', ['group' => $group->id()]), + 'weight' => 0, + ]; + } + + if ($group->hasPermission('request group membership', $account)) { + $operations['group-request-membership'] = [ + 'title' => $this->t('Request group membership'), + 'url' => new Url('entity.group.request_membership', ['group' => $group->id()]), + 'weight' => 0, + ]; + } } return $operations; @@ -80,6 +92,11 @@ class GroupMembership extends GroupContentEnablerBase { 'allowed for' => ['member'], ] + $defaults; + $permissions['request group membership'] = [ + 'title' => 'Request group membership', + 'allowed for' => ['outsider'], + ]; + // Update the labels of the default permissions. $permissions['view group_membership content']['title'] = '%plugin_name: View individual group members'; $permissions['update own group_membership content']['title'] = '%plugin_name: Edit own membership'; @@ -202,6 +219,52 @@ class GroupMembership extends GroupContentEnablerBase { 'link' => 0, ], ])->save(); + + // Add the group_requires_approval field to the new group content type. + // The field storage for this is defined in the config/install folder. + FieldConfig::create([ + 'field_storage' => FieldStorageConfig::loadByName('group_content', 'group_requires_approval'), + 'bundle' => $group_content_type_id, + 'label' => $this->t('Requires Approval'), + 'settings' => [ + 'on_label' => 'Requires Approval', + 'off_label' => 'Requires Approval', + ], + ])->save(); + + // Build the 'default' display ID for both the entity form and view mode. + $default_display_id = "group_content.$group_content_type_id.default"; + + // Build or retrieve the 'default' form mode. + if (!$form_display = EntityFormDisplay::load($default_display_id)) { + $form_display = EntityFormDisplay::create([ + 'targetEntityType' => 'group_content', + 'bundle' => $group_content_type_id, + 'mode' => 'default', + 'status' => TRUE, + ]); + } + + // Build or retrieve the 'default' view mode. + if (!$view_display = EntityViewDisplay::load($default_display_id)) { + $view_display = EntityViewDisplay::create([ + 'targetEntityType' => 'group_content', + 'bundle' => $group_content_type_id, + 'mode' => 'default', + 'status' => TRUE, + ]); + } + + // Assign widget settings for the 'default' form mode. + $form_display->setComponent('group_requires_approval', [ + 'type' => 'boolean_checkbox', + ])->save(); + + // Assign display settings for the 'default' view mode. + $view_display->setComponent('group_requires_approval', [ + 'label' => 'above', + 'type' => 'boolean', + ])->save(); } /**