diff --git a/app/assets/javascripts/discourse/app/lib/transform-post.js b/app/assets/javascripts/discourse/app/lib/transform-post.js index efc1ac2882..e9c24b5708 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 1ccb12f462..8905829ba7 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 ce74f3af14..e9190d024d 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 20be48a6ba..583d7ebeed 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 e996722538..51c99d9905 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 be5378e26d..7e565b3f49 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 60a7c37102..1cd93e4a83 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 83c0ae9e00..3f59df457d 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) @@ -185,6 +187,59 @@ RSpec.describe TopicsController do end end + describe "moving to a new topic as a group moderator" do + fab!(:group_user) { Fabricate(:group_user) } + fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) } + fab!(:topic) { Fabricate(:topic, category: category) } + fab!(:p1) { Fabricate(:post, user: group_user.user, post_number: 1, topic: topic) } + fab!(:p2) { Fabricate(:post, user: group_user.user, post_number: 2, topic: topic) } + let(:user) { group_user.user } + + before do + sign_in(user) + SiteSetting.enable_category_group_moderation = true + end + + it "moves the posts" do + expect do + post "/t/#{topic.id}/move-posts.json", params: { + title: 'Logan is a good movie', + post_ids: [p2.id], + category_id: category.id + } + end.to change { Topic.count }.by(1) + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result['success']).to eq(true) + expect(result['url']).to eq(Topic.last.relative_url) + end + + it "does not allow posts to be moved to a private category" do + staff_category = Fabricate(:category) + staff_category.set_permissions(staff: :full) + staff_category.save! + + post "/t/#{topic.id}/move-posts.json", params: { + title: 'Logan is a good movie', + post_ids: [p2.id], + category_id: staff_category.id + } + + expect(response).to be_forbidden + end + + it "does not allow posts outside of the category to be moved" do + topic.update!(category: nil) + + post "/t/#{topic.id}/move-posts.json", params: { + title: 'blah', post_ids: [p1.post_number, p2.post_number] + } + + expect(response).to be_forbidden + end + end + describe 'moving to an existing topic' do let!(:user) { sign_in(moderator) } let(:p1) { Fabricate(:post, user: user) } @@ -246,6 +301,59 @@ RSpec.describe TopicsController do end end + describe "moving to an existing topic as a group moderator" do + fab!(:group_user) { Fabricate(:group_user) } + fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) } + fab!(:topic) { Fabricate(:topic, category: category) } + fab!(:p1) { Fabricate(:post, user: group_user.user, post_number: 1, topic: topic) } + fab!(:p2) { Fabricate(:post, user: group_user.user, post_number: 2, topic: topic) } + fab!(:dest_topic) { Fabricate(:topic) } + + let(:user) { group_user.user } + + before do + sign_in(user) + SiteSetting.enable_category_group_moderation = true + end + + it "moves the posts" do + post "/t/#{topic.id}/move-posts.json", params: { + post_ids: [p2.id], + destination_topic_id: dest_topic.id + } + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result['success']).to eq(true) + expect(result['url']).to be_present + end + + it "does not allow posts to be moved to a private category" do + staff_category = Fabricate(:category) + staff_category.set_permissions(staff: :full) + staff_category.save! + dest_topic.update!(category: staff_category) + + post "/t/#{topic.id}/move-posts.json", params: { + post_ids: [p2.id], + destination_topic_id: dest_topic.id + } + + expect(response).to be_forbidden + end + + it "does not allow posts outside of the category to be moved" do + topic.update!(category: nil) + + post "/t/#{topic.id}/move-posts.json", params: { + post_ids: [p1.post_number, p2.post_number], + destination_topic_id: dest_topic.id + } + + expect(response).to be_forbidden + end + end + describe 'moving to a new message' do let!(:message) { Fabricate(:private_message_topic) } let!(:p1) { Fabricate(:post, user: user, post_number: 1, topic: message) } @@ -417,6 +525,55 @@ RSpec.describe TopicsController do end end + describe "merging into another topic as a group moderator" do + fab!(:group_user) { Fabricate(:group_user) } + fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) } + fab!(:topic) { Fabricate(:topic, category: category) } + fab!(:p1) { Fabricate(:post, post_number: 1, topic: topic) } + fab!(:p2) { Fabricate(:post, post_number: 2, topic: topic) } + fab!(:dest_topic) { Fabricate(:topic) } + let(:user) { group_user.user } + + before do + sign_in(user) + SiteSetting.enable_category_group_moderation = true + end + + it "moves the posts" do + post "/t/#{topic.id}/merge-topic.json", params: { + destination_topic_id: dest_topic.id + } + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result['success']).to eq(true) + expect(result['url']).to be_present + end + + it "does not allow posts to be moved to a private category" do + staff_category = Fabricate(:category) + staff_category.set_permissions(staff: :full) + staff_category.save! + dest_topic.update!(category: staff_category) + + post "/t/#{topic.id}/merge-topic.json", params: { + destination_topic_id: dest_topic.id + } + + expect(response).to be_forbidden + end + + it "does not allow posts outside of the category to be moved" do + topic.update!(category: nil) + + post "/t/#{topic.id}/merge-topic.json", params: { + destination_topic_id: dest_topic.id + } + + expect(response).to be_forbidden + end + end + describe 'merging into another message' do let(:message) { Fabricate(:private_message_topic, user: user) } let!(:p1) { Fabricate(:post, topic: message, user: trust_level_4) } @@ -708,9 +865,7 @@ RSpec.describe TopicsController do fab!(:group_user) { Fabricate(:group_user) } fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) } fab!(:topic) { Fabricate(:topic, category: category) } - let(:user) { group_user.user } - let(:group) { group_user.group } before do sign_in(user) @@ -743,7 +898,6 @@ RSpec.describe TopicsController do expect(response.status).to eq(403) expect(topic.reload.pinned_at).to eq(nil) end - end end diff --git a/test/javascripts/acceptance/topic-move-posts-test.js b/test/javascripts/acceptance/topic-move-posts-test.js index 8d9124f440..d867c2a063 100644 --- a/test/javascripts/acceptance/topic-move-posts-test.js +++ b/test/javascripts/acceptance/topic-move-posts-test.js @@ -120,3 +120,27 @@ QUnit.test("moving posts from personal message", async assert => { "it shows an option to move to existing message" ); }); + +QUnit.test("group moderator moving posts", async assert => { + await visit("/t/topic-for-group-moderators/2480"); + await click(".toggle-admin-menu"); + await click(".topic-admin-multi-select .btn"); + await click("#post_2 .select-below"); + + assert.equal( + find(".selected-posts .move-to-topic") + .text() + .trim(), + I18n.t("topic.move_to.action"), + "it should show the move to button" + ); + + await click(".selected-posts .move-to-topic"); + + assert.ok( + find(".choose-topic-modal .title") + .html() + .includes(I18n.t("topic.move_to.title")), + "it opens move to modal" + ); +}); diff --git a/test/javascripts/fixtures/topic.js b/test/javascripts/fixtures/topic.js index 6b91b712fa..0a39bae2de 100644 --- a/test/javascripts/fixtures/topic.js +++ b/test/javascripts/fixtures/topic.js @@ -5077,340 +5077,342 @@ export default { pm_with_non_human_user: false }, "/t/2480/1.json": { - "post_stream": { - "posts": [ + post_stream: { + posts: [ { - "id": 41, - "name": "", - "username": "group_moderator", - "avatar_template": "/images/avatar.png", - "created_at": "2020-07-24T17:48:55.419Z", - "cooked": "
Here is my new topic. I am a group category moderator!
", - "post_number": 1, - "post_type": 1, - "updated_at": "2020-07-24T17:48:55.419Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0, - "yours": true, - "topic_id": 2480, - "topic_slug": "a-topic-with-group-category-moderators", - "display_username": "", - "primary_group_name": "group_moderators", - "primary_group_flair_url": "cheese", - "primary_group_flair_bg_color": "ff0", - "primary_group_flair_color": "", - "version": 1, - "can_edit": true, - "can_delete": false, - "can_recover": false, - "can_wiki": false, - "read": true, - "user_title": "a title", - "title_is_group": false, - "bookmarked": false, - "actions_summary": [ + id: 41, + name: "", + username: "group_moderator", + avatar_template: "/images/avatar.png", + created_at: "2020-07-24T17:48:55.419Z", + cooked: "Here is my new topic. I am a group category moderator!
", + post_number: 1, + post_type: 1, + updated_at: "2020-07-24T17:48:55.419Z", + reply_count: 0, + reply_to_post_number: null, + quote_count: 0, + incoming_link_count: 0, + reads: 2, + readers_count: 1, + score: 0, + yours: true, + topic_id: 2480, + topic_slug: "a-topic-with-group-category-moderators", + display_username: "", + primary_group_name: "group_moderators", + primary_group_flair_url: "cheese", + primary_group_flair_bg_color: "ff0", + primary_group_flair_color: "", + version: 1, + can_edit: true, + can_delete: false, + can_recover: false, + can_wiki: false, + read: true, + user_title: "a title", + title_is_group: false, + bookmarked: false, + actions_summary: [ { - "id": 3, - "can_act": true + id: 3, + can_act: true }, { - "id": 4, - "can_act": true + id: 4, + can_act: true }, { - "id": 8, - "can_act": true + id: 8, + can_act: true }, { - "id": 7, - "can_act": true + id: 7, + can_act: true } ], - "moderator": false, - "admin": false, - "staff": false, - "group_moderator": true, - "user_id": 3, - "hidden": false, - "trust_level": 1, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0 + moderator: false, + admin: false, + staff: false, + group_moderator: true, + user_id: 3, + hidden: false, + trust_level: 1, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false, + reviewable_id: 0, + reviewable_score_count: 0, + reviewable_score_pending_count: 0 }, { - "id": 42, - "name": "", - "username": "normal_user", - "avatar_template": "/images/avatar.png", - "created_at": "2020-07-24T17:50:01.263Z", - "cooked": "A fascinating topic worthy of discussion.
", - "post_number": 2, - "post_type": 1, - "updated_at": "2020-07-24T17:50:01.263Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0, - "yours": false, - "topic_id": 2480, - "topic_slug": "a-topic-with-group-category-moderators", - "display_username": "", - "primary_group_name": null, - "primary_group_flair_url": null, - "primary_group_flair_bg_color": null, - "primary_group_flair_color": null, - "version": 1, - "can_edit": false, - "can_delete": false, - "can_recover": false, - "can_wiki": false, - "read": true, - "user_title": null, - "bookmarked": false, - "actions_summary": [ + id: 42, + name: "", + username: "normal_user", + avatar_template: "/images/avatar.png", + created_at: "2020-07-24T17:50:01.263Z", + cooked: "A fascinating topic worthy of discussion.
", + post_number: 2, + post_type: 1, + updated_at: "2020-07-24T17:50:01.263Z", + reply_count: 0, + reply_to_post_number: null, + quote_count: 0, + incoming_link_count: 0, + reads: 2, + readers_count: 1, + score: 0, + yours: false, + topic_id: 2480, + topic_slug: "a-topic-with-group-category-moderators", + display_username: "", + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_bg_color: null, + primary_group_flair_color: null, + version: 1, + can_edit: false, + can_delete: false, + can_recover: false, + can_wiki: false, + read: true, + user_title: null, + bookmarked: false, + actions_summary: [ { - "id": 2, - "can_act": true + id: 2, + can_act: true }, { - "id": 3, - "can_act": true + id: 3, + can_act: true }, { - "id": 4, - "can_act": true + id: 4, + can_act: true }, { - "id": 8, - "can_act": true + id: 8, + can_act: true }, { - "id": 6, - "can_act": true + id: 6, + can_act: true }, { - "id": 7, - "can_act": true + id: 7, + can_act: true } ], - "moderator": false, - "admin": false, - "staff": false, - "user_id": 2, - "hidden": false, - "trust_level": 1, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0 + moderator: false, + admin: false, + staff: false, + user_id: 2, + hidden: false, + trust_level: 1, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false, + reviewable_id: 0, + reviewable_score_count: 0, + reviewable_score_pending_count: 0 }, { - "id": 43, - "name": "", - "username": "group_moderator", - "avatar_template": "/images/avatar.png", - "created_at": "2020-07-24T17:50:17.274Z", - "cooked": "Thank you for your reply!
", - "post_number": 3, - "post_type": 1, - "updated_at": "2020-07-24T17:50:17.274Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0, - "yours": true, - "topic_id": 2480, - "topic_slug": "a-topic-with-group-category-moderators", - "display_username": "", - "primary_group_name": "group_moderators", - "primary_group_flair_url": "cheese", - "primary_group_flair_bg_color": "ff0", - "primary_group_flair_color": "", - "version": 1, - "can_edit": true, - "can_delete": true, - "can_recover": false, - "can_wiki": false, - "read": true, - "user_title": "a title", - "title_is_group": false, - "bookmarked": false, - "actions_summary": [ + id: 43, + name: "", + username: "group_moderator", + avatar_template: "/images/avatar.png", + created_at: "2020-07-24T17:50:17.274Z", + cooked: "Thank you for your reply!
", + post_number: 3, + post_type: 1, + updated_at: "2020-07-24T17:50:17.274Z", + reply_count: 0, + reply_to_post_number: null, + quote_count: 0, + incoming_link_count: 0, + reads: 2, + readers_count: 1, + score: 0, + yours: true, + topic_id: 2480, + topic_slug: "a-topic-with-group-category-moderators", + display_username: "", + primary_group_name: "group_moderators", + primary_group_flair_url: "cheese", + primary_group_flair_bg_color: "ff0", + primary_group_flair_color: "", + version: 1, + can_edit: true, + can_delete: true, + can_recover: false, + can_wiki: false, + read: true, + user_title: "a title", + title_is_group: false, + bookmarked: false, + actions_summary: [ { - "id": 3, - "can_act": true + id: 3, + can_act: true }, { - "id": 4, - "can_act": true + id: 4, + can_act: true }, { - "id": 8, - "can_act": true + id: 8, + can_act: true }, { - "id": 7, - "can_act": true + id: 7, + can_act: true } ], - "moderator": false, - "admin": false, - "staff": false, - "group_moderator": true, - "user_id": 3, - "hidden": false, - "trust_level": 1, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0 + moderator: false, + admin: false, + staff: false, + group_moderator: true, + user_id: 3, + hidden: false, + trust_level: 1, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false, + reviewable_id: 0, + reviewable_score_count: 0, + reviewable_score_pending_count: 0 } ], - "stream": [ + stream: [ 41, 42, 43 ] }, - "timeline_lookup": [ + timeline_lookup: [ [ 1, 0 ] ], - "id": 2480, - "title": "A Topic with Group Category Moderators", - "fancy_title": "A Topic with Group Category Moderators", - "posts_count": 3, - "created_at": "2020-07-24T17:48:54.986Z", - "views": 2, - "reply_count": 0, - "like_count": 0, - "last_posted_at": "2020-07-24T17:50:17.274Z", - "visible": true, - "closed": false, - "archived": false, - "has_summary": false, - "archetype": "regular", - "slug": "a-topic-with-group-category-moderators", - "category_id": 5, - "word_count": 22, - "deleted_at": null, - "user_id": 3, - "featured_link": null, - "pinned_globally": false, - "pinned_at": null, - "pinned_until": null, - "image_url": null, - "draft": null, - "draft_key": "topic_2480", - "draft_sequence": 1, - "posted": true, - "unpinned": null, - "pinned": false, - "current_post_number": 3, - "highest_post_number": 3, - "last_read_post_number": 3, - "last_read_post_id": 43, - "deleted_by": null, - "actions_summary": [ + id: 2480, + title: "A Topic with Group Category Moderators", + fancy_title: "A Topic with Group Category Moderators", + posts_count: 3, + created_at: "2020-07-24T17:48:54.986Z", + views: 2, + reply_count: 0, + like_count: 0, + last_posted_at: "2020-07-24T17:50:17.274Z", + visible: true, + closed: false, + archived: false, + has_summary: false, + archetype: "regular", + slug: "a-topic-with-group-category-moderators", + category_id: 5, + word_count: 22, + deleted_at: null, + user_id: 3, + featured_link: null, + pinned_globally: false, + pinned_at: null, + pinned_until: null, + image_url: null, + draft: null, + draft_key: "topic_2480", + draft_sequence: 1, + posted: true, + unpinned: null, + pinned: false, + current_post_number: 3, + highest_post_number: 3, + last_read_post_number: 3, + last_read_post_id: 43, + deleted_by: null, + actions_summary: [ { - "id": 4, - "count": 0, - "hidden": false, - "can_act": true + id: 4, + count: 0, + hidden: false, + can_act: true }, { - "id": 8, - "count": 0, - "hidden": false, - "can_act": true + id: 8, + count: 0, + hidden: false, + can_act: true }, { - "id": 7, - "count": 0, - "hidden": false, - "can_act": true + id: 7, + count: 0, + hidden: false, + can_act: true } ], - "chunk_size": 20, - "bookmarked": false, - "topic_timer": null, - "private_topic_timer": null, - "message_bus_last_id": 4, - "participant_count": 2, - "show_read_indicator": false, - "thumbnails": null, - "details": { - "notification_level": 3, - "notifications_reason_id": 1, - "can_edit": true, - "can_create_post": true, - "can_reply_as_new_topic": true, - "can_flag_topic": true, - "can_review_topic": true, - "can_close_topic": true, - "can_archive_topic": true, - "can_edit_staff_notes": true, - "participants": [ + chunk_size: 20, + bookmarked: false, + topic_timer: null, + private_topic_timer: null, + message_bus_last_id: 4, + participant_count: 2, + show_read_indicator: false, + thumbnails: null, + details: { + notification_level: 3, + notifications_reason_id: 1, + can_edit: true, + can_create_post: true, + can_move_posts: true, + can_reply_as_new_topic: true, + can_flag_topic: true, + can_review_topic: true, + can_close_topic: true, + can_archive_topic: true, + can_split_merge_topic: true, + can_edit_staff_notes: true, + participants: [ { - "id": 3, - "username": "group_moderator", - "name": "", - "avatar_template": "/images/avatar.png", - "post_count": 2, - "primary_group_name": "group_moderators", - "primary_group_flair_url": "cheese", - "primary_group_flair_color": "", - "primary_group_flair_bg_color": "ff0" + id: 3, + username: "group_moderator", + name: "", + avatar_template: "/images/avatar.png", + post_count: 2, + primary_group_name: "group_moderators", + primary_group_flair_url: "cheese", + primary_group_flair_color: "", + primary_group_flair_bg_color: "ff0" }, { - "id": 2, - "username": "normal_user", - "name": "", - "avatar_template": "/images/avatar.png", - "post_count": 1, - "primary_group_name": null, - "primary_group_flair_url": null, - "primary_group_flair_color": null, - "primary_group_flair_bg_color": null + id: 2, + username: "normal_user", + name: "", + avatar_template: "/images/avatar.png", + post_count: 1, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_color: null, + primary_group_flair_bg_color: null } ], - "created_by": { - "id": 3, - "username": "group_moderator", - "name": "", - "avatar_template": "/images/avatar.png" + created_by: { + id: 3, + username: "group_moderator", + name: "", + avatar_template: "/images/avatar.png" }, - "last_poster": { - "id": 3, - "username": "group_moderator", - "name": "", - "avatar_template": "/images/avatar.png" + last_poster: { + id: 3, + username: "group_moderator", + name: "", + avatar_template: "/images/avatar.png" } } }