FEATURE - allow category group moderators to split/merge topics (#10351)

FEATURE - allow category group moderators to split/merge topics (#10351)

diff --git a/app/assets/javascripts/discourse/app/lib/transform-post.js b/app/assets/javascripts/discourse/app/lib/transform-post.js
index efc1ac2..e9c24b5 100644
--- a/app/assets/javascripts/discourse/app/lib/transform-post.js
+++ b/app/assets/javascripts/discourse/app/lib/transform-post.js
@@ -122,6 +122,7 @@ export default function transformPost(
     currentUser && (currentUser.id === post.user_id || currentUser.staff);
   postAtts.canArchiveTopic = !!details.can_archive_topic;
   postAtts.canCloseTopic = !!details.can_close_topic;
+  postAtts.canSplitMergeTopic = !!details.can_split_merge_topic;
   postAtts.canEditStaffNotes = !!details.can_edit_staff_notes;
   postAtts.canReplyAsNewTopic = !!details.can_reply_as_new_topic;
   postAtts.canReviewTopic = !!details.can_review_topic;
diff --git a/app/assets/javascripts/discourse/app/widgets/post-stream.js b/app/assets/javascripts/discourse/app/widgets/post-stream.js
index 1ccb12f..8905829 100644
--- a/app/assets/javascripts/discourse/app/widgets/post-stream.js
+++ b/app/assets/javascripts/discourse/app/widgets/post-stream.js
@@ -94,7 +94,7 @@ export default createWidget("post-stream", {
       transformed.canCreatePost = attrs.canCreatePost;
       transformed.mobileView = mobileView;
 
-      if (transformed.canManage) {
+      if (transformed.canManage || transformed.canSplitMergeTopic) {
         transformed.multiSelect = attrs.multiSelect;
 
         if (attrs.multiSelect) {
diff --git a/app/assets/javascripts/discourse/app/widgets/topic-admin-menu.js b/app/assets/javascripts/discourse/app/widgets/topic-admin-menu.js
index ce74f3a..e9190d0 100644
--- a/app/assets/javascripts/discourse/app/widgets/topic-admin-menu.js
+++ b/app/assets/javascripts/discourse/app/widgets/topic-admin-menu.js
@@ -130,7 +130,10 @@ export default createWidget("topic-admin-menu", {
     const visible = topic.get("visible");
 
     // Admin actions
-    if (this.currentUser && this.currentUser.get("canManageTopic")) {
+    if (
+      this.get("currentUser.canManageTopic") ||
+      details.can_split_merge_topic
+    ) {
       this.addActionButton({
         className: "topic-admin-multi-select",
         buttonClass: "popup-menu-btn",
@@ -138,7 +141,9 @@ export default createWidget("topic-admin-menu", {
         icon: "tasks",
         label: "actions.multi_select"
       });
+    }
 
+    if (this.get("currentUser.canManageTopic")) {
       if (details.get("can_delete")) {
         this.addActionButton({
           className: "topic-admin-delete",
@@ -180,7 +185,7 @@ export default createWidget("topic-admin-menu", {
       }
     }
 
-    if (this.currentUser && this.currentUser.get("canManageTopic")) {
+    if (this.get("currentUser.canManageTopic")) {
       this.addActionButton({
         className: "topic-admin-status-update",
         buttonClass: "popup-menu-btn",
@@ -230,7 +235,7 @@ export default createWidget("topic-admin-menu", {
       }
     }
 
-    if (this.currentUser && this.currentUser.get("canManageTopic")) {
+    if (this.get("currentUser.canManageTopic")) {
       this.addActionButton({
         className: "topic-admin-visible",
         buttonClass: "popup-menu-btn",
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index 20be48a..583d7eb 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -711,6 +711,9 @@ class TopicsController < ApplicationController
     topic = Topic.find_by(id: topic_id)
     guardian.ensure_can_move_posts!(topic)
 
+    destination_topic = Topic.find_by(id: destination_topic_id)
+    guardian.ensure_can_create_post_on_topic!(destination_topic)
+
     args = {}
     args[:destination_topic_id] = destination_topic_id.to_i
 
@@ -736,9 +739,15 @@ class TopicsController < ApplicationController
     topic = Topic.with_deleted.find_by(id: topic_id)
     guardian.ensure_can_move_posts!(topic)
 
-    # when creating a new topic, ensure the 1st post is a regular post
-    if params[:title].present? && Post.where(topic: topic, id: post_ids).order(:post_number).pluck_first(:post_type) != Post.types[:regular]
-      return render_json_error("When moving posts to a new topic, the first post must be a regular post.")
+    if params[:title].present?
+      # when creating a new topic, ensure the 1st post is a regular post
+      if Post.where(topic: topic, id: post_ids).order(:post_number).pluck_first(:post_type) != Post.types[:regular]
+        return render_json_error("When moving posts to a new topic, the first post must be a regular post.")
+      end
+
+      if params[:category_id].present?
+        guardian.ensure_can_create_topic_on_category!(params[:category_id])
+      end
     end
 
     destination_topic = move_posts_to_destination(topic)
diff --git a/app/serializers/topic_view_details_serializer.rb b/app/serializers/topic_view_details_serializer.rb
index e996722..51c99d9 100644
--- a/app/serializers/topic_view_details_serializer.rb
+++ b/app/serializers/topic_view_details_serializer.rb
@@ -19,6 +19,7 @@ class TopicViewDetailsSerializer < ApplicationSerializer
      :can_publish_page,
      :can_close_topic,
      :can_archive_topic,
+     :can_split_merge_topic,
      :can_edit_staff_notes]
   end
 
@@ -142,6 +143,7 @@ class TopicViewDetailsSerializer < ApplicationSerializer
   end
   alias :include_can_close_topic? :can_perform_action_available_to_group_moderators?
   alias :include_can_archive_topic? :can_perform_action_available_to_group_moderators?
+  alias :include_can_split_merge_topic? :can_perform_action_available_to_group_moderators?
   alias :include_can_edit_staff_notes? :can_perform_action_available_to_group_moderators?
 
   def include_can_publish_page?
diff --git a/lib/guardian.rb b/lib/guardian.rb
index be5378e..7e565b3 100644
--- a/lib/guardian.rb
+++ b/lib/guardian.rb
@@ -172,7 +172,6 @@ class Guardian
   def can_moderate?(obj)
     obj && authenticated? && !is_silenced? && (is_staff? || (obj.is_a?(Topic) && @user.has_trust_level?(TrustLevel[4])))
   end
-  alias :can_move_posts? :can_moderate?
   alias :can_see_flags? :can_moderate?
 
   def can_tag?(topic)
diff --git a/lib/guardian/topic_guardian.rb b/lib/guardian/topic_guardian.rb
index 60a7c37..1cd93e4 100644
--- a/lib/guardian/topic_guardian.rb
+++ b/lib/guardian/topic_guardian.rb
@@ -210,6 +210,12 @@ module TopicGuardian
   end
   alias :can_archive_topic? :can_perform_action_available_to_group_moderators?
   alias :can_close_topic? :can_perform_action_available_to_group_moderators?
+  alias :can_split_merge_topic? :can_perform_action_available_to_group_moderators?
   alias :can_edit_staff_notes? :can_perform_action_available_to_group_moderators?
 
+  def can_move_posts?(topic)
+    return false if is_silenced?
+    can_perform_action_available_to_group_moderators?(topic)
+  end
+
 end
diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb
index 83c0ae9..3f59df4 100644
--- a/spec/requests/topics_controller_spec.rb
+++ b/spec/requests/topics_controller_spec.rb
@@ -99,6 +99,8 @@ RSpec.describe TopicsController do
       end
 
       context 'success' do
+        fab!(:category) { Fabricate(:category) }
+
         before { sign_in(admin) }
 
         it "returns success" do
@@ -106,7 +108,7 @@ RSpec.describe TopicsController do
             post "/t/#{topic.id}/move-posts.json", params: {
               title: 'Logan is a good movie',
               post_ids: [p2.id],
-              category_id: 123,
+              category_id: category.id,
               tags: ["tag1", "tag2"]
             }
           end.to change { Topic.count }.by(1)
@@ -130,7 +132,7 @@ RSpec.describe TopicsController do
               post "/t/#{topic.id}/move-posts.json", params: {
                 title: 'Logan is a good movie',
                 post_ids: [p2.id],
-                category_id: 123
+                category_id: category.id
               }
             end.to change { Topic.count }.by(1)
 

[... diff too long, it was truncated ...]

GitHub sha: 67e8bc53

This commit appears in #10351 which was approved by eviltrout. It was merged by jbrw.