From 39f0245804fb4fd3318ab95eacd213b4015814d3 Mon Sep 17 00:00:00 2001 From: Nazariy Velychenko Date: Fri, 26 Jun 2020 10:37:52 +0300 Subject: [PATCH 1/8] OI-74: #comment Added the ability to restrict access to "unfollow" the idea to author of idea. --- modules/openideal_idea/openideal_idea.module | 38 +++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/modules/openideal_idea/openideal_idea.module b/modules/openideal_idea/openideal_idea.module index d69780faf..b7cea07c9 100644 --- a/modules/openideal_idea/openideal_idea.module +++ b/modules/openideal_idea/openideal_idea.module @@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; +use Drupal\flag\FlagInterface; use Drupal\node\NodeInterface; use Drupal\openideal_idea\Form\OpenidealBaseRatingForm; @@ -60,7 +61,7 @@ function openideal_idea_node_presave(NodeInterface $node) { /** * Implements hook_ENTITY_TYPE_insert(). */ -function openideal_idea_node_insert(EntityInterface $entity) { +function openideal_idea_node_insert(NodeInterface $entity) { if ($entity->bundle() == 'idea') { try { // Create the group for the node. @@ -76,6 +77,12 @@ function openideal_idea_node_insert(EntityInterface $entity) { $plugin_id = 'group_node:' . $entity->bundle(); // Add the entity to the group. $group->addContent($entity, $plugin_id); + + // Author follow the node after its creating. + /** @var \Drupal\flag\FlagService $flag_service*/ + $flag_service = Drupal::service('flag'); + $flag = $flag_service->getFlagById('follow'); + $flag_service->flag($flag, $entity, $entity->getOwner()); } catch (Exception $e) { Drupal::logger('openideal_idea')->error($e->getMessage()); @@ -216,3 +223,32 @@ function openideal_idea_entity_type_build(array &$entity_types) { // To add "un-like" ability to the Idea bundle. $entity_types['vote']->setFormClass('votingapi_openideal_useful', OpenidealBaseRatingForm::class); } + +/** + * Implements hook_flag_action_access(). + * + * If user is Idea creator then restrict "unfollow" access. + */ +function openideal_idea_flag_action_access($action, FlagInterface $flag, AccountInterface $account, EntityInterface $flaggable = NULL) { + if ($flaggable && $flaggable instanceof NodeInterface && $action == 'unflag' && $flaggable->bundle() == 'idea') { + /** @var \Drupal\group\GroupMembershipLoaderInterface $membership_loader */ + $membership_loader = Drupal::service('group.membership_loader'); + + /** @var \Drupal\group\Entity\Storage\GroupContentStorage $storage */ + $storage = Drupal::entityTypeManager()->getStorage('group_content'); + + /** @var \Drupal\group\Entity\GroupContent $group_content */ + $group_contents = $storage->loadByEntity($flaggable); + + // Don't need to check all of group contents, + // such as they all from one group. + $group_content = reset($group_contents); + $group = $group_content->getGroup(); + + /** @var \Drupal\group\GroupMembership $member */ + $member = $membership_loader->load($group, $account); + + return AccessResult::forbiddenIf(array_key_exists('idea-author', $member->getRoles())); + } + return AccessResult::neutral(); +} From 22535b8351a0f03306c7be17de4d7d9fcfbe171f Mon Sep 17 00:00:00 2001 From: Nazariy Velychenko Date: Fri, 26 Jun 2020 23:30:01 +0300 Subject: [PATCH 2/8] OI-74: Removed default media option to add the creator to group as a membership, and added this functionality during group creating in openideal_idea_node_insert, to be able fetch the idea that belongs to group, because before member was added before node. --- ...e.entity_view_display.node.faq.default.yml | 3 +- ...re.entity_view_display.node.faq.teaser.yml | 1 + ....entity_view_display.node.page.default.yml | 5 ++- .../core.entity_view_mode.vote.token.yml | 9 +++++ ...ore.entity_view_mode.vote_result.token.yml | 9 +++++ config/install/group.type.idea.yml | 4 +- config/install/image.style.mentions_icon.yml | 14 +++++++ ..._hole.behavior_settings.node_type_idea.yml | 4 +- ...xonomy_vocabulary_idea_lifecycle_phase.yml | 4 +- config/install/user.role.authenticated.yml | 2 +- ...ser_registrationpassword.mail_original.yml | 4 +- modules/openideal_idea/openideal_idea.module | 38 ++++--------------- .../src/Form/ScoreConfigForm.php | 3 -- 13 files changed, 53 insertions(+), 47 deletions(-) create mode 100644 config/install/core.entity_view_mode.vote.token.yml create mode 100644 config/install/core.entity_view_mode.vote_result.token.yml create mode 100644 config/install/image.style.mentions_icon.yml diff --git a/config/install/core.entity_view_display.node.faq.default.yml b/config/install/core.entity_view_display.node.faq.default.yml index 29d358604..ed2fe40f0 100644 --- a/config/install/core.entity_view_display.node.faq.default.yml +++ b/config/install/core.entity_view_display.node.faq.default.yml @@ -26,4 +26,5 @@ content: region: content settings: { } third_party_settings: { } -hidden: { } +hidden: + addtoany: true diff --git a/config/install/core.entity_view_display.node.faq.teaser.yml b/config/install/core.entity_view_display.node.faq.teaser.yml index 97fe524da..daa085081 100644 --- a/config/install/core.entity_view_display.node.faq.teaser.yml +++ b/config/install/core.entity_view_display.node.faq.teaser.yml @@ -18,4 +18,5 @@ content: third_party_settings: { } region: content hidden: + addtoany: true field_faq_items: true diff --git a/config/install/core.entity_view_display.node.page.default.yml b/config/install/core.entity_view_display.node.page.default.yml index c2aeff36b..9f6461c55 100644 --- a/config/install/core.entity_view_display.node.page.default.yml +++ b/config/install/core.entity_view_display.node.page.default.yml @@ -92,8 +92,6 @@ content: settings: { } third_party_settings: { } region: content - field_paragraphs: - type: entity_reference_revisions_entity_view field_attached_docs: weight: 104 label: above @@ -118,6 +116,9 @@ content: third_party_settings: { } type: image region: content + field_paragraphs: + type: entity_reference_revisions_entity_view + region: content links: weight: 101 region: content diff --git a/config/install/core.entity_view_mode.vote.token.yml b/config/install/core.entity_view_mode.vote.token.yml new file mode 100644 index 000000000..971f5dbc6 --- /dev/null +++ b/config/install/core.entity_view_mode.vote.token.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +dependencies: + module: + - votingapi +id: vote.token +label: Token +targetEntityType: vote +cache: true diff --git a/config/install/core.entity_view_mode.vote_result.token.yml b/config/install/core.entity_view_mode.vote_result.token.yml new file mode 100644 index 000000000..3ce58cd08 --- /dev/null +++ b/config/install/core.entity_view_mode.vote_result.token.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +dependencies: + module: + - votingapi +id: vote_result.token +label: Token +targetEntityType: vote_result +cache: true diff --git a/config/install/group.type.idea.yml b/config/install/group.type.idea.yml index 8f96568f8..c6e0757bf 100644 --- a/config/install/group.type.idea.yml +++ b/config/install/group.type.idea.yml @@ -4,7 +4,7 @@ dependencies: { } id: idea label: Idea description: 'It provides an idea group type.' -creator_membership: true -creator_wizard: true +creator_membership: false +creator_wizard: false creator_roles: - idea-author diff --git a/config/install/image.style.mentions_icon.yml b/config/install/image.style.mentions_icon.yml new file mode 100644 index 000000000..df4a3f4bb --- /dev/null +++ b/config/install/image.style.mentions_icon.yml @@ -0,0 +1,14 @@ +langcode: en +status: true +dependencies: { } +name: mentions_icon +label: 'CKEditor Mentions Icon' +effects: + fa871714-f0f7-48fb-9ae7-e0b9bea3318f: + uuid: fa871714-f0f7-48fb-9ae7-e0b9bea3318f + id: image_scale_and_crop + weight: 0 + data: + width: 20 + height: 20 + anchor: center-center diff --git a/config/install/rabbit_hole.behavior_settings.node_type_idea.yml b/config/install/rabbit_hole.behavior_settings.node_type_idea.yml index 8abb5e881..2eea2b80c 100644 --- a/config/install/rabbit_hole.behavior_settings.node_type_idea.yml +++ b/config/install/rabbit_hole.behavior_settings.node_type_idea.yml @@ -1,8 +1,6 @@ langcode: en status: true -dependencies: - config: - - node.type.idea +dependencies: { } id: node_type_idea action: display_page allow_override: 1 diff --git a/config/install/rabbit_hole.behavior_settings.taxonomy_vocabulary_idea_lifecycle_phase.yml b/config/install/rabbit_hole.behavior_settings.taxonomy_vocabulary_idea_lifecycle_phase.yml index a19248e8c..c13b25f9c 100644 --- a/config/install/rabbit_hole.behavior_settings.taxonomy_vocabulary_idea_lifecycle_phase.yml +++ b/config/install/rabbit_hole.behavior_settings.taxonomy_vocabulary_idea_lifecycle_phase.yml @@ -1,8 +1,6 @@ langcode: en status: true -dependencies: - config: - - taxonomy.vocabulary.idea_lifecycle_phase +dependencies: { } id: taxonomy_vocabulary_idea_lifecycle_phase action: access_denied allow_override: 0 diff --git a/config/install/user.role.authenticated.yml b/config/install/user.role.authenticated.yml index 6d2dd3311..616879925 100644 --- a/config/install/user.role.authenticated.yml +++ b/config/install/user.role.authenticated.yml @@ -18,8 +18,8 @@ permissions: - 'post comments' - 'search content' - 'skip comment approval' + - 'use inline mentions' - 'use life_cycle_phases transition create_new_draft' - 'use life_cycle_phases transition publish' - - 'use inline mentions' - 'use text format basic_html' - 'use text format comments' diff --git a/config/install/user_registrationpassword.mail_original.yml b/config/install/user_registrationpassword.mail_original.yml index 51770bca1..139e50924 100644 --- a/config/install/user_registrationpassword.mail_original.yml +++ b/config/install/user_registrationpassword.mail_original.yml @@ -1,4 +1,4 @@ status_activated: - subject: 'Account details for [user:display-name] at [site:name] (approved)' - body: "[user:display-name],\r\n\r\nYour account at [site:name] has been activated.\r\n\r\nYou may now log in by clicking this link or copying and pasting it into your browser:\r\n\r\n[user:one-time-login-url]\r\n\r\nThis link can only be used once to log in and will lead you to a page where you can set your password.\r\n\r\nAfter setting your password, you will be able to log in at [site:login-url] in the future using:\r\n\r\nusername: [user:account-name]\r\npassword: Your password\r\n\r\n-- [site:name] team" + subject: 'Account details for [user:display-name] at [site:name]' + body: "[user:display-name],\n\nYour account at [site:name] has been activated.\n\nYou will be able to log in to [site:login-url] in the future using:\n\nusername: [user:name]\npassword: your password.\n\n-- [site:name] team" langcode: en diff --git a/modules/openideal_idea/openideal_idea.module b/modules/openideal_idea/openideal_idea.module index 950af95da..5a6fcd6b7 100644 --- a/modules/openideal_idea/openideal_idea.module +++ b/modules/openideal_idea/openideal_idea.module @@ -13,7 +13,6 @@ use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; -use Drupal\flag\FlagInterface; use Drupal\node\NodeInterface; use Drupal\openideal_idea\ComputedNumberList; use Drupal\openideal_idea\Form\OpenidealBaseRatingForm; @@ -76,11 +75,19 @@ function openideal_idea_node_insert(NodeInterface $entity) { 'langcode' => 'en', ]); $group->save(); + + // Need to add node to group before the member will be added + // to be able fetch the node. + // // Define the plugin id. $plugin_id = 'group_node:' . $entity->bundle(); // Add the entity to the group. $group->addContent($entity, $plugin_id); + // The group creator automatically becomes a member. + $values = ['group_roles' => ['idea-author']]; + $group->addMember($group->getOwner(), $values); + // Author follow the node after its creating. /** @var \Drupal\flag\FlagService $flag_service*/ $flag_service = Drupal::service('flag'); @@ -254,32 +261,3 @@ function openideal_idea_entity_type_build(array &$entity_types) { // To add "un-like" ability to the Idea bundle. $entity_types['vote']->setFormClass('votingapi_openideal_useful', OpenidealBaseRatingForm::class); } - -/** - * Implements hook_flag_action_access(). - * - * If user is Idea creator then restrict "unfollow" access. - */ -function openideal_idea_flag_action_access($action, FlagInterface $flag, AccountInterface $account, EntityInterface $flaggable = NULL) { - if ($flaggable && $flaggable instanceof NodeInterface && $action == 'unflag' && $flaggable->bundle() == 'idea') { - /** @var \Drupal\group\GroupMembershipLoaderInterface $membership_loader */ - $membership_loader = Drupal::service('group.membership_loader'); - - /** @var \Drupal\group\Entity\Storage\GroupContentStorage $storage */ - $storage = Drupal::entityTypeManager()->getStorage('group_content'); - - /** @var \Drupal\group\Entity\GroupContent $group_content */ - $group_contents = $storage->loadByEntity($flaggable); - - // Don't need to check all of group contents, - // such as they all from one group. - $group_content = reset($group_contents); - $group = $group_content->getGroup(); - - /** @var \Drupal\group\GroupMembership $member */ - $member = $membership_loader->load($group, $account); - - return AccessResult::forbiddenIf(array_key_exists('idea-author', $member->getRoles())); - } - return AccessResult::neutral(); -} diff --git a/modules/openideal_idea/src/Form/ScoreConfigForm.php b/modules/openideal_idea/src/Form/ScoreConfigForm.php index 220a34cc5..0e33d840a 100644 --- a/modules/openideal_idea/src/Form/ScoreConfigForm.php +++ b/modules/openideal_idea/src/Form/ScoreConfigForm.php @@ -4,9 +4,6 @@ use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Config\ConfigFactoryInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; /** * Class ScoreConfigForm. From 113484274d3758b9f9327aed1b8405d384f3591e Mon Sep 17 00:00:00 2001 From: Nazariy Velychenko Date: Fri, 26 Jun 2020 23:34:26 +0300 Subject: [PATCH 3/8] OI-74: Created a Rules "reaction rules" to set the user to follow the idea after joining the group. Created a rules "action" to follow/unfollow and an entity. --- ...llow_the_idea_after_joining_the_group_.yml | 56 ++++++++++ .../src/Plugin/RulesAction/FlagAction.php | 101 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 config/install/rules.reaction.set_the_user_to_follow_the_idea_after_joining_the_group_.yml create mode 100644 modules/openideal_user/src/Plugin/RulesAction/FlagAction.php diff --git a/config/install/rules.reaction.set_the_user_to_follow_the_idea_after_joining_the_group_.yml b/config/install/rules.reaction.set_the_user_to_follow_the_idea_after_joining_the_group_.yml new file mode 100644 index 000000000..debf48190 --- /dev/null +++ b/config/install/rules.reaction.set_the_user_to_follow_the_idea_after_joining_the_group_.yml @@ -0,0 +1,56 @@ +langcode: en +status: true +dependencies: { } +id: set_the_user_to_follow_the_idea_after_joining_the_group_ +label: 'Set the user to follow the idea after joining the group.' +events: + - + event_name: openideal_user.user_joined_group +description: '' +tags: { } +config_version: '3' +expression: + id: rules_rule + uuid: 4da5c6e8-b04c-4008-ab13-e3451492b751 + weight: 0 + conditions: + id: rules_and + uuid: 0f2245d4-056b-41b8-a54d-bbfac43c0efb + weight: 0 + conditions: + - + id: rules_condition + uuid: 566fa3aa-9839-4a88-804e-86bce6911194 + weight: 0 + context_values: + operation: '==' + value: idea + context_mapping: + data: node.type.target_id + context_processors: + operation: + rules_tokens: { } + value: + rules_tokens: { } + provides_mapping: { } + condition_id: rules_data_comparison + negate: false + actions: + id: rules_action_set + uuid: 7db1f1f7-b94d-4422-a7e1-f46ee3654d33 + weight: 0 + actions: + - + id: rules_action + uuid: ac82e6de-b6a8-4712-ac71-87101858a0ba + weight: 0 + context_values: + operation: flag + context_mapping: + entity: node + user: user + context_processors: + operation: + rules_tokens: { } + provides_mapping: { } + action_id: openideal_user_flag_action diff --git a/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php b/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php new file mode 100644 index 000000000..5075f7d71 --- /dev/null +++ b/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php @@ -0,0 +1,101 @@ +flagService = $flag_service; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('flag') + ); + } + + /** + * Flag the Entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be flagged. + * @param \Drupal\user\UserInterface $user + * User to flag. + * @param string $operation + * Operation (flag or unflag) + */ + protected function doExecute(EntityInterface $entity, UserInterface $user, string $operation) { + if ($this->validateOperation($operation)) { + $flag = $this->flagService->getFlagById('follow'); + $this->flagService->{$operation}($flag, $entity, $user); + } + } + + /** + * Validate operation. + * + * @param string $operation + * Operation. + * + * @return bool + * TRUE if validate, FALSE otherwise. + */ + private function validateOperation(string $operation) { + return $operation == 'flag' || $operation == 'unflag'; + } + +} From fbd6db0dcf2f53f09d695065f8442c7f3b91c8f8 Mon Sep 17 00:00:00 2001 From: Nazariy Velychenko Date: Fri, 26 Jun 2020 23:39:23 +0300 Subject: [PATCH 4/8] OI-74: Created a logic to restrict "unfollow" acces when the user is creator of the Idea. Reworked OpenidealUserGroupEvent.php event. --- modules/openideal_user/openideal_user.module | 52 +++++++++++++++---- .../openideal_user.rules.events.yml | 14 +++-- .../src/Event/OpenidealUserGroupEvent.php | 36 ++++++++++--- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/modules/openideal_user/openideal_user.module b/modules/openideal_user/openideal_user.module index 003a9eceb..706d060a0 100644 --- a/modules/openideal_user/openideal_user.module +++ b/modules/openideal_user/openideal_user.module @@ -5,10 +5,15 @@ * Contains openideal_idea.module. */ +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatch; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; +use Drupal\flag\FlagInterface; use Drupal\group\Entity\GroupContent; +use Drupal\node\NodeInterface; use Drupal\openideal_user\Event\OpenidealUserEvents; use Drupal\openideal_user\Event\OpenidealUserGroupEvent; use Drupal\openideal_user\Event\OpenidealUserJoinedSiteEvent; @@ -71,15 +76,15 @@ function openideal_user_entity_type_alter(array &$entity_types) { * Implements hook_preprocess_HOOK(). */ function openideal_user_preprocess_page(&$variables) { - if (\Drupal::currentUser()->isAuthenticated()) { - $user_id = \Drupal::currentUser()->id(); - $user = \Drupal::entityTypeManager()->getStorage('user')->load($user_id); - $current_route = \Drupal::service('current_route_match')->getRouteName(); + if (Drupal::currentUser()->isAuthenticated()) { + $user_id = Drupal::currentUser()->id(); + $user = Drupal::entityTypeManager()->getStorage('user')->load($user_id); + $current_route = Drupal::service('current_route_match')->getRouteName(); // Check if any of user field empty, if so set a remind message. if (($user->get('field_age_group')->isEmpty() || $user->get('field_gender')->isEmpty()) && ($current_route !== 'entity.user.edit_form' && $current_route !== 'openideal_user.register.user.more_about_you') && !$user->hasRole('administrator')) { - \Drupal::messenger()->addMessage(t('Please fill your profile', + Drupal::messenger()->addMessage(t('Please fill your profile', ['@link' => Url::fromRoute('entity.user.edit_form', ['user' => $user_id])->toString()] )); } @@ -90,14 +95,43 @@ function openideal_user_preprocess_page(&$variables) { * Implements hook_ENTITY_TYPE_insert(). */ function openideal_user_group_content_insert(GroupContent $entity) { - $event = new OpenidealUserGroupEvent($entity); - Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_JOINED_GROUP, $event); + if ($entity->getGroupContentType()->id() == 'idea-group_membership') { + $event = new OpenidealUserGroupEvent($entity); + Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_JOINED_GROUP, $event); + } } /** * Implements hook_ENTITY_TYPE_delete(). */ function openideal_user_group_content_delete(GroupContent $entity) { - $event = new OpenidealUserGroupEvent($entity); - Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_LEFT_GROUP, $event); + if ($entity->getGroupContentType()->id() == 'idea-group_membership') { + $event = new OpenidealUserGroupEvent($entity); + Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_LEFT_GROUP, $event); + } +} + +/** + * Implements hook_flag_action_access(). + * + * If user is Idea creator then restrict "unfollow" access. + */ +function openideal_idea_flag_action_access($action, FlagInterface $flag, AccountInterface $account, EntityInterface $flaggable = NULL) { + if ($flaggable && $flaggable instanceof NodeInterface && $action == 'unflag' && $flaggable->bundle() == 'idea') { + /** @var \Drupal\group\Entity\Storage\GroupContentStorage $storage */ + $storage = Drupal::entityTypeManager()->getStorage('group_content'); + + /** @var \Drupal\group\Entity\GroupContent $group_content */ + $group_contents = $storage->loadByEntity($flaggable); + + // As one node can be a part of one group, + // get the first element. + $group_content = reset($group_contents); + $member = $group_content->getGroup()->getMember($account); + + if ($member && array_key_exists('idea-author', $member->getRoles())) { + return AccessResult::forbidden(); + } + } + return AccessResult::allowed(); } diff --git a/modules/openideal_user/openideal_user.rules.events.yml b/modules/openideal_user/openideal_user.rules.events.yml index c8a6fbc2b..014584f8b 100644 --- a/modules/openideal_user/openideal_user.rules.events.yml +++ b/modules/openideal_user/openideal_user.rules.events.yml @@ -13,9 +13,12 @@ openideal_user.user_joined_group: label: 'After user joined the group' category: 'User' context_definitions: - group: + groupContent: type: 'entity:group_content' - label: 'Group that contains the user.' + label: 'Group content entity that contains the user.' + node: + type: 'entity:node' + label: 'Node that unite the group.' user: type: 'entity:user' label: 'Group owner user.' @@ -24,9 +27,12 @@ openideal_user.user_left_group: label: 'After user left the group' category: 'User' context_definitions: - group: + groupContent: type: 'entity:group_content' - label: 'Group that contains the user.' + label: 'Group content entity that contains the user.' + node: + type: 'entity:node' + label: 'Node that unite the group.' user: type: 'entity:user' label: 'Group owner user.' diff --git a/modules/openideal_user/src/Event/OpenidealUserGroupEvent.php b/modules/openideal_user/src/Event/OpenidealUserGroupEvent.php index 60759d6a6..23d653d02 100644 --- a/modules/openideal_user/src/Event/OpenidealUserGroupEvent.php +++ b/modules/openideal_user/src/Event/OpenidealUserGroupEvent.php @@ -22,17 +22,29 @@ class OpenidealUserGroupEvent extends Event { * * @var \Drupal\group\Entity\GroupContent */ - public $group; + public $groupContent; + + /** + * Node that unite the group. + * + * @var \Drupal\node\NodeInterface + */ + public $node; /** * OpenideaLUserMentionEvent construct. * - * @param \Drupal\group\Entity\GroupContent $group + * @param \Drupal\group\Entity\GroupContent $group_content * Group content entity. */ - public function __construct(GroupContent $group) { - $this->group = $group; - $this->user = $group->getEntity(); + public function __construct(GroupContent $group_content) { + $this->groupContent = $group_content; + // As one node can have be part of one group get first element. + // @Todo: check if node can be part more then for one group, + // and restrict it, if so. + $content = $group_content->getGroup()->getContent('group_node:idea'); + $this->node = reset($content)->getEntity(); + $this->user = $group_content->getEntity(); } /** @@ -51,8 +63,18 @@ public function getUser() { * @return \Drupal\group\Entity\GroupContent * Group content. */ - public function getGroup() { - return $this->group; + public function getGroupContent() { + return $this->groupContent; + } + + /** + * Get node that unite the group. + * + * @return \Drupal\node\NodeInterface + * Node. + */ + public function getNode() { + return $this->node; } } From 6631ad967d0d6ba9dd60fdad0c7db868cb7cb523 Mon Sep 17 00:00:00 2001 From: Nazariy Velychenko Date: Tue, 30 Jun 2020 00:57:31 +0300 Subject: [PATCH 5/8] OI-74: Added the "\" to all Drupal class executions. Reworked FlagAction to be more flexible. --- ...ction.follow_idea_after_joining_group.yml} | 5 +- .../openideal_challenge.module | 4 +- modules/openideal_idea/openideal_idea.module | 20 +++---- .../openideal_idea/src/ComputedNumberList.php | 11 ++-- modules/openideal_user/openideal_user.module | 26 ++++----- .../src/Plugin/RulesAction/FlagAction.php | 53 ++++++++++++++----- 6 files changed, 75 insertions(+), 44 deletions(-) rename config/install/{rules.reaction.set_the_user_to_follow_the_idea_after_joining_the_group_.yml => rules.reaction.follow_idea_after_joining_group.yml} (92%) diff --git a/config/install/rules.reaction.set_the_user_to_follow_the_idea_after_joining_the_group_.yml b/config/install/rules.reaction.follow_idea_after_joining_group.yml similarity index 92% rename from config/install/rules.reaction.set_the_user_to_follow_the_idea_after_joining_the_group_.yml rename to config/install/rules.reaction.follow_idea_after_joining_group.yml index debf48190..230e9223a 100644 --- a/config/install/rules.reaction.set_the_user_to_follow_the_idea_after_joining_the_group_.yml +++ b/config/install/rules.reaction.follow_idea_after_joining_group.yml @@ -1,7 +1,7 @@ langcode: en status: true dependencies: { } -id: set_the_user_to_follow_the_idea_after_joining_the_group_ +id: follow_idea_after_joining_group label: 'Set the user to follow the idea after joining the group.' events: - @@ -46,11 +46,14 @@ expression: weight: 0 context_values: operation: flag + flag_id: follow context_mapping: entity: node user: user context_processors: operation: rules_tokens: { } + flag_id: + rules_tokens: { } provides_mapping: { } action_id: openideal_user_flag_action diff --git a/modules/openideal_challenge/openideal_challenge.module b/modules/openideal_challenge/openideal_challenge.module index 7b26a09ae..d523f0ad2 100644 --- a/modules/openideal_challenge/openideal_challenge.module +++ b/modules/openideal_challenge/openideal_challenge.module @@ -17,7 +17,7 @@ use Drupal\views\ViewExecutable; function openideal_challenge_cron() { // Processing open/close scheduling challenge nodes via cron. /** @var \Drupal\openideal_challenge\Service\OpenidealChallengeService $challenge_service */ - $challenge_service = Drupal::service('openideal_challenge.challenge_service'); + $challenge_service = \Drupal::service('openideal_challenge.challenge_service'); $challenge_service->openChallenges(); $challenge_service->closeChallenges(); } @@ -82,7 +82,7 @@ function openideal_challenge_views_query_alter(ViewExecutable $view, QueryPlugin */ function openideal_challenge_node_presave(EntityInterface $entity) { if ($entity->bundle() == 'challenge') { - $event_dispatcher = Drupal::service('event_dispatcher'); + $event_dispatcher = \Drupal::service('event_dispatcher'); // If challenge was created react only on if it was opened // make no sense to react on close. if ($entity->isNew() && $entity->get('field_is_open')->value) { diff --git a/modules/openideal_idea/openideal_idea.module b/modules/openideal_idea/openideal_idea.module index 5a6fcd6b7..1cdc2ff50 100644 --- a/modules/openideal_idea/openideal_idea.module +++ b/modules/openideal_idea/openideal_idea.module @@ -23,9 +23,9 @@ use Drupal\openideal_idea\Form\OpenidealBaseRatingForm; function openideal_idea_form_alter(&$form, FormStateInterface $form_state, $form_id) { if ($form_id == 'node_idea_form') { // Check challenge query parameter. - if ($challenge_id = Drupal::request()->get('challenge')) { + if ($challenge_id = \Drupal::request()->get('challenge')) { // Load predefined challenge node. - $predefined_challenge = Drupal::entityTypeManager() + $predefined_challenge = \Drupal::entityTypeManager() ->getStorage('node') ->load($challenge_id); if (!empty($predefined_challenge)) { @@ -68,7 +68,7 @@ function openideal_idea_node_insert(NodeInterface $entity) { try { // Create the group for the node. /** @var \Drupal\group\Entity\Group $group */ - $group = Drupal::entityTypeManager()->getStorage('group') + $group = \Drupal::entityTypeManager()->getStorage('group') ->create([ 'label' => $entity->label(), 'type' => 'idea', @@ -90,12 +90,12 @@ function openideal_idea_node_insert(NodeInterface $entity) { // Author follow the node after its creating. /** @var \Drupal\flag\FlagService $flag_service*/ - $flag_service = Drupal::service('flag'); + $flag_service = \Drupal::service('flag'); $flag = $flag_service->getFlagById('follow'); $flag_service->flag($flag, $entity, $entity->getOwner()); } catch (Exception $e) { - Drupal::logger('openideal_idea')->error($e->getMessage()); + \Drupal::logger('openideal_idea')->error($e->getMessage()); } } } @@ -129,7 +129,7 @@ function openideal_idea_entity_access(EntityInterface $entity, $operation, Accou */ function openideal_idea_comment_create_access(AccountInterface $account, array $context, $entity_bundle) { /** @var \Drupal\node\NodeInterface $node */ - $node = Drupal::routeMatch()->getParameter('node'); + $node = \Drupal::routeMatch()->getParameter('node'); if ($node instanceof NodeInterface && $node->bundle() === 'idea') { return AccessResult::forbiddenIf(!$node->get('field_duplicate_of')->isEmpty()); } @@ -143,7 +143,7 @@ function openideal_idea_menu_local_tasks_alter(&$data, $route_name, RefinableCac return; } - $node = Drupal::routeMatch()->getParameter('node'); + $node = \Drupal::routeMatch()->getParameter('node'); if ($node->bundle() === 'idea' && $group = _openideal_idea_get_group_by_entity($node)) { $data['tabs'][0]['group.members'] = [ '#theme' => 'menu_local_task', @@ -151,7 +151,7 @@ function openideal_idea_menu_local_tasks_alter(&$data, $route_name, RefinableCac 'title' => t('Group Members'), 'url' => Url::fromRoute('view.group_members.page_1', ['group' => $group->id()]), ], - '#access' => $group->hasPermission('administer members', Drupal::currentUser()), + '#access' => $group->hasPermission('administer members', \Drupal::currentUser()), ]; // The tab we're adding is dependent on a user's access to add content. @@ -171,7 +171,7 @@ function openideal_idea_menu_local_tasks_alter(&$data, $route_name, RefinableCac function _openideal_idea_get_group_by_entity($entity) { // In our case we will have one node per group. // We get all group ids but return just the first one. - $group_contents = Drupal::entityTypeManager() + $group_contents = \Drupal::entityTypeManager() ->getStorage('group_content') ->loadByEntity($entity); foreach ($group_contents as $group_content) { @@ -197,7 +197,7 @@ function openideal_idea_form_views_exposed_form_alter(&$form, FormStateInterface unset($form['phase']['#options']['Life Cycle Phases']); } // Get the list of published and opened challenges. - $node_storage = Drupal::entityTypeManager()->getStorage('node'); + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); $query = $node_storage->getQuery() ->condition('type', 'challenge', '=') ->condition('field_is_open', TRUE, '=') diff --git a/modules/openideal_idea/src/ComputedNumberList.php b/modules/openideal_idea/src/ComputedNumberList.php index ecd4bba07..84410995b 100644 --- a/modules/openideal_idea/src/ComputedNumberList.php +++ b/modules/openideal_idea/src/ComputedNumberList.php @@ -2,7 +2,6 @@ namespace Drupal\openideal_idea; -use Drupal; use Drupal\Core\Field\FieldItemList; use Drupal\Core\TypedData\ComputedItemListTrait; use InvalidArgumentException; @@ -28,19 +27,19 @@ protected function computeValue() { * Get overall score. */ protected function getOverallScore() { - $configuration = Drupal::configFactory()->get('openideal_idea.scoreconfig'); + $configuration = \Drupal::configFactory()->get('openideal_idea.scoreconfig'); // Node id. $id = $this->getEntity()->id(); // Get node comments. - $comments = Drupal::entityQuery('comment') + $comments = \Drupal::entityQuery('comment') ->condition('entity_id', $id) ->condition('entity_type', 'node') ->count() ->execute(); // Get node votes. - $votes = Drupal::entityQuery('vote') + $votes = \Drupal::entityQuery('vote') ->condition('entity_id', $id) ->condition('entity_type', 'node') ->count() @@ -50,9 +49,9 @@ protected function getOverallScore() { $node_counter_value = 0; // If statistics module is enabled then add node view count to score. - if (Drupal::moduleHandler()->moduleExists('statistics')) { + if (\Drupal::moduleHandler()->moduleExists('statistics')) { /** @var \Drupal\statistics\StatisticsViewsResult $statistics_result */ - $statistics_result = Drupal::service('statistics.storage.node')->fetchView($id); + $statistics_result = \Drupal::service('statistics.storage.node')->fetchView($id); if ($statistics_result) { $node_counter_value = $statistics_result->getTotalCount() * ($configuration->get('node_value') ?? 0.2); } diff --git a/modules/openideal_user/openideal_user.module b/modules/openideal_user/openideal_user.module index 706d060a0..31323c853 100644 --- a/modules/openideal_user/openideal_user.module +++ b/modules/openideal_user/openideal_user.module @@ -26,8 +26,8 @@ use Symfony\Component\HttpFoundation\RedirectResponse; */ function openideal_user_form_user_login_form_alter(&$form, FormStateInterface $form_state, $form_id) { /** @var \Drupal\social_api\Plugin\NetworkManager $network_manager */ - $network_manager = Drupal::service('plugin.network.manager'); - $social_plugins = Drupal::config('social_auth.settings')->get('auth'); + $network_manager = \Drupal::service('plugin.network.manager'); + $social_plugins = \Drupal::config('social_auth.settings')->get('auth'); // Remove socials that aren't configured properly. foreach ($social_plugins as $plugin_id => $data) { @@ -46,9 +46,9 @@ function openideal_user_form_user_login_form_alter(&$form, FormStateInterface $f * Implements hook_user_login(). */ function openideal_user_user_login(UserInterface $account) { - if (RouteMatch::createFromRequest(Drupal::request())->getRouteName() == 'user_registrationpassword.confirm') { + if (RouteMatch::createFromRequest(\Drupal::request())->getRouteName() == 'user_registrationpassword.confirm') { $event = new OpenidealUserJoinedSiteEvent($account); - Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_JOINED_THE_SITE, $event); + \Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_JOINED_THE_SITE, $event); // Redirect user if joined the site for the first // time via the user_registrationpassword one time link. @@ -60,7 +60,7 @@ function openideal_user_user_login(UserInterface $account) { // OpenidealUserEvents. if (!$account->getLastAccessedTime()) { $event = new OpenidealUserJoinedSiteEvent($account); - Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_JOINED_THE_SITE, $event); + \Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_JOINED_THE_SITE, $event); } } @@ -76,15 +76,15 @@ function openideal_user_entity_type_alter(array &$entity_types) { * Implements hook_preprocess_HOOK(). */ function openideal_user_preprocess_page(&$variables) { - if (Drupal::currentUser()->isAuthenticated()) { - $user_id = Drupal::currentUser()->id(); - $user = Drupal::entityTypeManager()->getStorage('user')->load($user_id); - $current_route = Drupal::service('current_route_match')->getRouteName(); + if (\Drupal::currentUser()->isAuthenticated()) { + $user_id = \Drupal::currentUser()->id(); + $user = \Drupal::entityTypeManager()->getStorage('user')->load($user_id); + $current_route = \Drupal::service('current_route_match')->getRouteName(); // Check if any of user field empty, if so set a remind message. if (($user->get('field_age_group')->isEmpty() || $user->get('field_gender')->isEmpty()) && ($current_route !== 'entity.user.edit_form' && $current_route !== 'openideal_user.register.user.more_about_you') && !$user->hasRole('administrator')) { - Drupal::messenger()->addMessage(t('Please fill your profile', + \Drupal::messenger()->addMessage(t('Please fill your profile', ['@link' => Url::fromRoute('entity.user.edit_form', ['user' => $user_id])->toString()] )); } @@ -97,7 +97,7 @@ function openideal_user_preprocess_page(&$variables) { function openideal_user_group_content_insert(GroupContent $entity) { if ($entity->getGroupContentType()->id() == 'idea-group_membership') { $event = new OpenidealUserGroupEvent($entity); - Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_JOINED_GROUP, $event); + \Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_JOINED_GROUP, $event); } } @@ -107,7 +107,7 @@ function openideal_user_group_content_insert(GroupContent $entity) { function openideal_user_group_content_delete(GroupContent $entity) { if ($entity->getGroupContentType()->id() == 'idea-group_membership') { $event = new OpenidealUserGroupEvent($entity); - Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_LEFT_GROUP, $event); + \Drupal::service('event_dispatcher')->dispatch(OpenidealUserEvents::OPENIDEA_USER_LEFT_GROUP, $event); } } @@ -119,7 +119,7 @@ function openideal_user_group_content_delete(GroupContent $entity) { function openideal_idea_flag_action_access($action, FlagInterface $flag, AccountInterface $account, EntityInterface $flaggable = NULL) { if ($flaggable && $flaggable instanceof NodeInterface && $action == 'unflag' && $flaggable->bundle() == 'idea') { /** @var \Drupal\group\Entity\Storage\GroupContentStorage $storage */ - $storage = Drupal::entityTypeManager()->getStorage('group_content'); + $storage = \Drupal::entityTypeManager()->getStorage('group_content'); /** @var \Drupal\group\Entity\GroupContent $group_content */ $group_contents = $storage->loadByEntity($flaggable); diff --git a/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php b/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php index 5075f7d71..70eb4b40b 100644 --- a/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php +++ b/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php @@ -3,7 +3,9 @@ namespace Drupal\openideal_user\Plugin\RulesAction; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\flag\FlagServiceInterface; use Drupal\rules\Core\RulesActionBase; use Drupal\user\UserInterface; @@ -19,11 +21,8 @@ * context_definitions = { * "entity" = @ContextDefinition("entity", * label = @Translation("Entity to follow"), - * assignment_restriction = "selector" - * ), - * "user" = @ContextDefinition("entity:user", - * label = @Translation("User that will follow the entity"), - * assignment_restriction = "selector" + * assignment_restriction = "selector", + * required = TRUE * ), * "operation" = @ContextDefinition("string", * label = @Translation("The flag operation."), @@ -31,11 +30,24 @@ * assignment_restriction = "input", * required = TRUE * ), + * "flag_id" = @ContextDefinition("string", + * label = @Translation("The flag id."), + * description = @Translation("The identifier of the flag to load."), + * assignment_restriction = "input", + * required = TRUE + * ), + * "user" = @ContextDefinition("entity:user", + * label = @Translation("User that will follow the entity"), + * assignment_restriction = "selector", + * required = FALSE + * ), * } * ) */ class FlagAction extends RulesActionBase implements ContainerFactoryPluginInterface { + use StringTranslationTrait; + /** * The flag service. * @@ -43,6 +55,13 @@ class FlagAction extends RulesActionBase implements ContainerFactoryPluginInterf */ protected $flagService; + /** + * A logger instance. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + /** * {@inheritDoc} */ @@ -50,10 +69,12 @@ public function __construct( array $configuration, $plugin_id, $plugin_definition, - FlagServiceInterface $flag_service + FlagServiceInterface $flag_service, + LoggerChannelInterface $logger ) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->flagService = $flag_service; + $this->logger = $logger; } /** @@ -64,7 +85,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('flag') + $container->get('flag'), + $container->get('logger.factory')->get('rules') ); } @@ -73,15 +95,22 @@ public static function create(ContainerInterface $container, array $configuratio * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to be flagged. - * @param \Drupal\user\UserInterface $user - * User to flag. * @param string $operation * Operation (flag or unflag) + * @param string $flag_id + * The identifier of the flag to load. + * @param \Drupal\user\UserInterface $user + * User to flag. */ - protected function doExecute(EntityInterface $entity, UserInterface $user, string $operation) { + protected function doExecute(EntityInterface $entity, string $operation, string $flag_id, UserInterface $user = NULL) { if ($this->validateOperation($operation)) { - $flag = $this->flagService->getFlagById('follow'); - $this->flagService->{$operation}($flag, $entity, $user); + $flag = $this->flagService->getFlagById($flag_id); + if ($flag) { + $this->flagService->{$operation}($flag, $entity, $user); + } + else { + $this->logger->warning($this->t("Provided flag id doesn't exists")); + } } } From 54869b8c321461c466f2e6d5bb9902b58b819c61 Mon Sep 17 00:00:00 2001 From: Nazariy Velychenko Date: Tue, 30 Jun 2020 16:16:56 +0300 Subject: [PATCH 6/8] OI-74: Removed idea-author role from group idea membership edit form. --- modules/openideal_idea/openideal_idea.module | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/openideal_idea/openideal_idea.module b/modules/openideal_idea/openideal_idea.module index 1cdc2ff50..4169c3c6f 100644 --- a/modules/openideal_idea/openideal_idea.module +++ b/modules/openideal_idea/openideal_idea.module @@ -46,6 +46,12 @@ function openideal_idea_form_alter(&$form, FormStateInterface $form_state, $form unset($form['group_roles']['widget']['#options']['idea-author']); unset($form['group_roles']['widget']['#options']['idea-expert']); } + + // Unset useless group roles for group idea membership edit form. + if ($form_id == 'group_content_idea-group_membership_edit_form') { + unset($form['group_roles']['widget']['#options']['idea-author']); + } + } /** From 95477e39c292bff3e2ed8f9f0fcf79a49b66048f Mon Sep 17 00:00:00 2001 From: Nazariy Velychenko Date: Tue, 30 Jun 2020 16:40:02 +0300 Subject: [PATCH 7/8] OI-74: Reworked flag action access logic. Added additional logic to flag action plugin. --- modules/openideal_user/openideal_user.module | 21 ++++++------------- .../src/Plugin/RulesAction/FlagAction.php | 9 ++++++-- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/modules/openideal_user/openideal_user.module b/modules/openideal_user/openideal_user.module index 31323c853..4017c61f4 100644 --- a/modules/openideal_user/openideal_user.module +++ b/modules/openideal_user/openideal_user.module @@ -117,21 +117,12 @@ function openideal_user_group_content_delete(GroupContent $entity) { * If user is Idea creator then restrict "unfollow" access. */ function openideal_idea_flag_action_access($action, FlagInterface $flag, AccountInterface $account, EntityInterface $flaggable = NULL) { - if ($flaggable && $flaggable instanceof NodeInterface && $action == 'unflag' && $flaggable->bundle() == 'idea') { - /** @var \Drupal\group\Entity\Storage\GroupContentStorage $storage */ - $storage = \Drupal::entityTypeManager()->getStorage('group_content'); - - /** @var \Drupal\group\Entity\GroupContent $group_content */ - $group_contents = $storage->loadByEntity($flaggable); - - // As one node can be a part of one group, - // get the first element. - $group_content = reset($group_contents); - $member = $group_content->getGroup()->getMember($account); - - if ($member && array_key_exists('idea-author', $member->getRoles())) { - return AccessResult::forbidden(); - } + if ($flaggable + && $flaggable instanceof NodeInterface + && $action == 'unflag' + && $flaggable->bundle() == 'idea' + && $flaggable->uid->target_id == $account->id()) { + return AccessResult::forbidden(); } return AccessResult::allowed(); } diff --git a/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php b/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php index 70eb4b40b..bb4060e05 100644 --- a/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php +++ b/modules/openideal_user/src/Plugin/RulesAction/FlagAction.php @@ -12,7 +12,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Provides a 'Save entity' action. + * Provides a Flag action. * * @RulesAction( * id = "openideal_user_flag_action", @@ -106,7 +106,12 @@ protected function doExecute(EntityInterface $entity, string $operation, string if ($this->validateOperation($operation)) { $flag = $this->flagService->getFlagById($flag_id); if ($flag) { - $this->flagService->{$operation}($flag, $entity, $user); + try { + $this->flagService->{$operation}($flag, $entity, $user); + } + catch (\LogicException $exception) { + $this->logger->warning($exception->getMessage()); + } } else { $this->logger->warning($this->t("Provided flag id doesn't exists")); From 9428ae4da4318fd81aa966d9e1a4e7b7273f3222 Mon Sep 17 00:00:00 2001 From: Nazariy Velychenko Date: Tue, 30 Jun 2020 21:24:58 +0300 Subject: [PATCH 8/8] OI-74: Replace allowed access result with neutral --- modules/openideal_user/openideal_user.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openideal_user/openideal_user.module b/modules/openideal_user/openideal_user.module index 4017c61f4..af7e7be43 100644 --- a/modules/openideal_user/openideal_user.module +++ b/modules/openideal_user/openideal_user.module @@ -124,5 +124,5 @@ function openideal_idea_flag_action_access($action, FlagInterface $flag, Account && $flaggable->uid->target_id == $account->id()) { return AccessResult::forbidden(); } - return AccessResult::allowed(); + return AccessResult::neutral(); }