From f859fd6bde348dd725c736d5a0b6a1ee091364ae Mon Sep 17 00:00:00 2001
From: Krzysztof Kotlarek <kotlarek.krzysztof@gmail.com>
Date: Mon, 6 Sep 2021 10:18:51 +1000
Subject: [PATCH] FEATURE: allow plugins to extend Groups (#14216)

* add_permitted_group_param API for plugins
* add groups-interaction-custom-options outlet
* custom search can use custom group scope
---
 .../discourse/app/lib/user-search.js          |  6 +++++
 .../groups-form-interaction-fields.hbs        |  2 ++
 .../addon/components/user-chooser.js          |  1 +
 app/controllers/admin/groups_controller.rb    |  2 ++
 app/controllers/groups_controller.rb          |  2 ++
 app/controllers/users_controller.rb           |  7 +++++-
 app/models/group.rb                           | 10 ++++++--
 lib/discourse_plugin_registry.rb              |  2 ++
 lib/plugin/instance.rb                        | 11 ++++++++
 spec/models/group_spec.rb                     | 13 ++++++++++
 spec/requests/admin/groups_controller_spec.rb | 25 +++++++++++++++++++
 11 files changed, 78 insertions(+), 3 deletions(-)

diff --git a/app/assets/javascripts/discourse/app/lib/user-search.js b/app/assets/javascripts/discourse/app/lib/user-search.js
index 7cde342f3fb..c16e74bda4c 100644
--- a/app/assets/javascripts/discourse/app/lib/user-search.js
+++ b/app/assets/javascripts/discourse/app/lib/user-search.js
@@ -25,6 +25,7 @@ function performSearch(
   topicId,
   categoryId,
   includeGroups,
+  customGroupsScope,
   includeMentionableGroups,
   includeMessageableGroups,
   allowedUsers,
@@ -56,6 +57,7 @@ function performSearch(
       topic_id: topicId,
       category_id: categoryId,
       include_groups: includeGroups,
+      custom_groups_scope: customGroupsScope,
       include_mentionable_groups: includeMentionableGroups,
       include_messageable_groups: includeMessageableGroups,
       groups: groupMembersOf,
@@ -100,6 +102,7 @@ let debouncedSearch = function (
   topicId,
   categoryId,
   includeGroups,
+  customGroupsScope,
   includeMentionableGroups,
   includeMessageableGroups,
   allowedUsers,
@@ -116,6 +119,7 @@ let debouncedSearch = function (
     topicId,
     categoryId,
     includeGroups,
+    customGroupsScope,
     includeMentionableGroups,
     includeMessageableGroups,
     allowedUsers,
@@ -207,6 +211,7 @@ export default function userSearch(options) {
 
   let term = options.term || "",
     includeGroups = options.includeGroups,
+    customGroupsScope = options.customGroupsScope,
     includeMentionableGroups = options.includeMentionableGroups,
     includeMessageableGroups = options.includeMessageableGroups,
     allowedUsers = options.allowedUsers,
@@ -248,6 +253,7 @@ export default function userSearch(options) {
       topicId,
       categoryId,
       includeGroups,
+      customGroupsScope,
       includeMentionableGroups,
       includeMessageableGroups,
       allowedUsers,
diff --git a/app/assets/javascripts/discourse/app/templates/components/groups-form-interaction-fields.hbs b/app/assets/javascripts/discourse/app/templates/components/groups-form-interaction-fields.hbs
index df6d0ac8e2a..7681b30a854 100644
--- a/app/assets/javascripts/discourse/app/templates/components/groups-form-interaction-fields.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/groups-form-interaction-fields.hbs
@@ -104,3 +104,5 @@
     onChange=(action (mut model.default_notification_level))
   }}
 </div>
+
+{{plugin-outlet name="groups-interaction-custom-options" args=(hash model=model)}}
diff --git a/app/assets/javascripts/select-kit/addon/components/user-chooser.js b/app/assets/javascripts/select-kit/addon/components/user-chooser.js
index a6b1db09ba9..9d3f90c00ca 100644
--- a/app/assets/javascripts/select-kit/addon/components/user-chooser.js
+++ b/app/assets/javascripts/select-kit/addon/components/user-chooser.js
@@ -70,6 +70,7 @@ export default MultiSelectComponent.extend({
       categoryId: options.categoryId,
       exclude: this.excludedUsers,
       includeGroups: options.includeGroups,
+      customGroupsScope: options.customGroupsScope,
       allowedUsers: options.allowedUsers,
       includeMentionableGroups: options.includeMentionableGroups,
       includeMessageableGroups: options.includeMessageableGroups,
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 3c84ff92981..f80dfe27c5e 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -180,6 +180,8 @@ class Admin::GroupsController < Admin::AdminController
     custom_fields = DiscoursePluginRegistry.editable_group_custom_fields
     permitted << { custom_fields: custom_fields } unless custom_fields.blank?
 
+    permitted = permitted | DiscoursePluginRegistry.group_params
+
     params.require(:group).permit(permitted)
   end
 end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 828bb11a5f5..8bc89054b8c 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -736,6 +736,8 @@ class GroupsController < ApplicationController
       end
     end
 
+    permitted_params = permitted_params | DiscoursePluginRegistry.group_params
+
     params.require(:group).permit(*permitted_params)
   end
 
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 98b8d28e90b..0835db62eef 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1123,7 +1123,12 @@ class UsersController < ApplicationController
       end
 
     if groups
-      groups = Group.search_groups(term, groups: groups)
+      groups = Group.search_groups(term,
+                                   groups: groups,
+                                   custom_scope: {
+                                     name: params["custom_groups_scope"]&.to_sym,
+                                     arguments: [current_user]
+                                   })
       groups = groups.order('groups.name asc')
 
       to_render[:groups] = groups.map do |m|
diff --git a/app/models/group.rb b/app/models/group.rb
index 41ca0a3e6e9..d863b43d86b 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -557,8 +557,14 @@ class Group < ActiveRecord::Base
     lookup_group(name) || refresh_automatic_group!(name)
   end
 
-  def self.search_groups(name, groups: nil)
-    (groups || Group).where(
+  def self.search_groups(name, groups: nil, custom_scope: {})
+    groups ||= Group
+
+    if custom_scope.present? && DiscoursePluginRegistry.group_scope_for_search.include?(custom_scope[:name])
+      groups = groups.send(custom_scope[:name], *custom_scope[:arguments])
+    end
+
+    groups.where(
       "name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "%#{name}%"
     )
   end
diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb
index de86a251bbc..625e7fe798b 100644
--- a/lib/discourse_plugin_registry.rb
+++ b/lib/discourse_plugin_registry.rb
@@ -76,6 +76,8 @@ class DiscoursePluginRegistry
   define_filtered_register :staff_editable_user_custom_fields
 
   define_filtered_register :editable_group_custom_fields
+  define_filtered_register :group_params
+  define_filtered_register :group_scope_for_search
 
   define_filtered_register :topic_thumbnail_sizes
 
diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb
index 6e9f59c906b..28fac3753ff 100644
--- a/lib/plugin/instance.rb
+++ b/lib/plugin/instance.rb
@@ -369,6 +369,17 @@ class Plugin::Instance
     end
   end
 
+  # Add a permitted_param to Group, respecting if the plugin is enabled
+  # Used in GroupsController#update and Admin::GroupsController#create
+  def register_group_param(param)
+    DiscoursePluginRegistry.register_group_param(param, self)
+  end
+
+  # Add a custom scopes for search to Group, respecting if the plugin is enabled
+  def register_group_scope_for_search(scope_name)
+    DiscoursePluginRegistry.register_group_scope_for_search(scope_name, self)
+  end
+
   # Add validation method but check that the plugin is enabled
   def validate(klass, name, &block)
     klass = klass.to_s.classify.constantize
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 4a839950e74..9e3dc1b869d 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -920,6 +920,7 @@ describe Group do
 
   describe '.search_groups' do
     fab!(:group) { Fabricate(:group, name: 'tEsT_more_things', full_name: 'Abc something awesome') }
+    let(:messageable_group) { Fabricate(:group, name: "MessageableGroup", messageable_level: Group::ALIAS_LEVELS[:everyone]) }
 
     it 'should return the right groups' do
       group
@@ -934,6 +935,18 @@ describe Group do
       expect(Group.search_groups('sOmEthi')).to eq([group])
       expect(Group.search_groups('test2')).to eq([])
     end
+
+    it 'allows to filter with additional scope' do
+      messageable_group
+
+      expect(Group.search_groups('es', custom_scope: { name: :messageable, arguments: [user] }).sort).to eq([messageable_group, group].sort)
+
+      plugin = Plugin::Instance.new
+      plugin.register_group_scope_for_search(:messageable)
+      expect(Group.search_groups('es', custom_scope: { name: :messageable, arguments: [user] }).sort).to eq([messageable_group].sort)
+
+      DiscoursePluginRegistry.reset!
+    end
   end
 
   describe '#bulk_add' do
diff --git a/spec/requests/admin/groups_controller_spec.rb b/spec/requests/admin/groups_controller_spec.rb
index c0053113e47..29a5c5a566d 100644
--- a/spec/requests/admin/groups_controller_spec.rb
+++ b/spec/requests/admin/groups_controller_spec.rb
@@ -76,6 +76,31 @@ RSpec.describe Admin::GroupsController do
         expect(group.custom_fields['test2']).to be_blank
       end
     end
+
+    context 'with Group.plugin_permitted_params' do
+      after do
+        DiscoursePluginRegistry.reset!
+      end
+
+      it 'filter unpermitted params' do
+        params = group_params
+        params[:group].merge!(allow_unknown_sender_topic_replies: true)
+
+        post "/admin/groups.json", params: params
+        expect(Group.last.allow_unknown_sender_topic_replies).to eq(false)
+      end
+
+      it 'allows plugin to allow custom params' do
+        params = group_params
+        params[:group].merge!(allow_unknown_sender_topic_replies: true)
+
+        plugin = Plugin::Instance.new
+        plugin.register_group_param :allow_unknown_sender_topic_replies
+
+        post "/admin/groups.json", params: params
+        expect(Group.last.allow_unknown_sender_topic_replies).to eq(true)
+      end
+    end
   end
 
   describe '#add_owners' do