mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-03 08:18:42 +08:00
Adds two new staff-only bulk actions in the topic-list dropdown — "Make Public Topic…" when every selected topic is a PM, and "Make Personal Message" when none of them are. Both are silent by default: no bump, no post revision, no edit notifications to watchers, no small-action post in the topic, and no forced re-watch. Admins who do want to notify participants can tick the existing "Notify" checkbox in the bulk-actions modal. Today, converting PMs to public topics is a one-at-a-time operation that fans out noisy edit notifications to everyone watching the PM, creates a post revision on the first post, and leaves a small-action post behind. The Support team hits this whenever they move a customer's history out of a group inbox and into an enterprise category — the existing runbook (Ref - t/68360) works around it with a custom console script, and doing it by hand spams participants. A proper bulk action in the UI was the missing piece. The plumbing starts at TopicConverter, which now takes a `silent:` keyword argument. When true, it passes `bypass_bump`, `skip_revision`, and `silent` through to PostRevisor (suppressing the bump, the revision row, and the edit notifications), skips `add_small_action`, and skips the forced `watch_topic` call at the end. While in there, `convert_to_private_message` now fails loudly with a base error when the final recipient count would exceed `max_allowed_message_recipients`, rather than silently collapsing to a self-only PM and losing all context — this applies to both the single-topic and bulk paths, since the check lives in the converter itself. TopicsBulkAction registers two new operations, `convert_to_public_topic` and `convert_to_private_message`, both driven by a small `bulk_convert(from_pm:)` helper. Per-topic success is detected from the archetype flip rather than from `.valid?`, which would re-run validations and wipe the `errors.add(:base, ...)` added by the cap guard. Validation errors bubble into the modal's failure list through the existing `@errors` channel. On the frontend, `bulk-select-topics-dropdown` gets two new entries with matching visibility rules, and `bulk-topic-actions` gains a couple of cases that reuse the existing CategoryChooser and `allowSilent`-backed "Notify" checkbox. `isCategoryAction` is extended so the destination-category picker renders for the PM→public flow. Ref - t/181539
327 lines
12 KiB
Ruby
327 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe TopicConverter do
|
|
describe "convert_to_public_topic" do
|
|
fab!(:admin)
|
|
fab!(:author, :user)
|
|
fab!(:category) { Fabricate(:category, topic_count: 1) }
|
|
fab!(:private_message) { Fabricate(:private_message_topic, user: author) } # creates a topic without a first post
|
|
let(:first_post) do
|
|
create_post(user: author, topic: private_message, allow_uncategorized_topics: false)
|
|
end
|
|
let(:other_user) { private_message.topic_allowed_users.find { |u| u.user != author }.user }
|
|
|
|
let(:uncategorized_category) { Category.find(SiteSetting.uncategorized_category_id) }
|
|
|
|
context "with success" do
|
|
it "converts private message to regular topic" do
|
|
SiteSetting.allow_uncategorized_topics = true
|
|
topic = nil
|
|
|
|
_pm_post_2 = Fabricate(:post, topic: private_message, user: author)
|
|
_pm_post_3 = Fabricate(:post, topic: private_message, user: author)
|
|
|
|
other_pm = Fabricate(:private_message_post).topic
|
|
other_pm_post = Fabricate(:private_message_post, topic: other_pm)
|
|
other_pm_post_2 =
|
|
Fabricate(:private_message_post, topic: other_pm, user: other_pm_post.user)
|
|
|
|
expect do
|
|
topic = TopicConverter.new(first_post.topic, admin).convert_to_public_topic
|
|
topic.reload
|
|
end.to change { uncategorized_category.reload.topic_count }.by(1).and change {
|
|
author.reload.topic_count
|
|
}.from(0).to(1).and change { author.reload.post_count }.from(0).to(2)
|
|
|
|
# Ensure query does not affect users from other topics or posts as DB query to update count is quite complex.
|
|
expect(other_pm.user.topic_count).to eq(0)
|
|
expect(other_pm.user.post_count).to eq(0)
|
|
expect(other_pm_post.user.topic_count).to eq(0)
|
|
expect(other_pm_post.user.post_count).to eq(0)
|
|
|
|
expect(topic).to be_valid
|
|
expect(topic.archetype).to eq("regular")
|
|
expect(topic.category_id).to eq(SiteSetting.uncategorized_category_id)
|
|
end
|
|
|
|
context "when uncategorized category is not allowed" do
|
|
before do
|
|
SiteSetting.allow_uncategorized_topics = false
|
|
category.update!(read_restricted: false)
|
|
end
|
|
|
|
it "should convert private message into the right category" do
|
|
topic = TopicConverter.new(first_post.topic, admin).convert_to_public_topic
|
|
topic.reload
|
|
|
|
expect(topic).to be_valid
|
|
expect(topic.archetype).to eq("regular")
|
|
|
|
first_category =
|
|
Category
|
|
.where.not(id: SiteSetting.uncategorized_category_id)
|
|
.where(read_restricted: false)
|
|
.order("id asc")
|
|
.first
|
|
|
|
expect(topic.category_id).to eq(first_category.id)
|
|
expect(topic.category.topic_count).to eq(2)
|
|
end
|
|
end
|
|
|
|
context "when a custom category_id is given" do
|
|
it "should convert private message into the right category" do
|
|
topic = TopicConverter.new(first_post.topic, admin).convert_to_public_topic(category.id)
|
|
|
|
expect(topic.reload.category).to eq(category)
|
|
expect(topic.category.topic_count).to eq(2)
|
|
end
|
|
end
|
|
|
|
it "updates user stats" do
|
|
first_post
|
|
topic_user = TopicUser.find_by(user_id: author.id, topic_id: private_message.id)
|
|
expect(private_message.user.user_stat.topic_count).to eq(0)
|
|
expect(private_message.user.user_stat.post_count).to eq(0)
|
|
private_message.convert_to_public_topic(admin)
|
|
expect(private_message.reload.user.user_stat.topic_count).to eq(1)
|
|
expect(private_message.user.user_stat.post_count).to eq(0)
|
|
expect(topic_user.reload.notification_level).to eq(TopicUser.notification_levels[:watching])
|
|
end
|
|
|
|
context "with a reply" do
|
|
before do
|
|
Jobs.run_immediately!
|
|
UserActionManager.enable
|
|
first_post
|
|
create_post(topic: private_message, user: other_user)
|
|
private_message.reload
|
|
end
|
|
|
|
it "updates UserActions" do
|
|
TopicConverter.new(private_message, admin).convert_to_public_topic
|
|
expect(
|
|
author.user_actions.where(action_type: UserAction::NEW_PRIVATE_MESSAGE).count,
|
|
).to eq(0)
|
|
expect(author.user_actions.where(action_type: UserAction::NEW_TOPIC).count).to eq(1)
|
|
expect(
|
|
other_user.user_actions.where(action_type: UserAction::NEW_PRIVATE_MESSAGE).count,
|
|
).to eq(0)
|
|
expect(
|
|
other_user.user_actions.where(action_type: UserAction::GOT_PRIVATE_MESSAGE).count,
|
|
).to eq(0)
|
|
expect(other_user.user_actions.where(action_type: UserAction::REPLY).count).to eq(1)
|
|
end
|
|
end
|
|
|
|
it "creates small action and revision when not silent" do
|
|
Jobs.run_immediately!
|
|
first_post
|
|
|
|
expect do
|
|
TopicConverter.new(private_message, admin).convert_to_public_topic(category.id)
|
|
end.to change { PostRevision.count }.by(1)
|
|
|
|
expect(
|
|
private_message.posts.where(
|
|
post_type: Post.types[:small_action],
|
|
action_code: "public_topic",
|
|
),
|
|
).to be_present
|
|
end
|
|
|
|
it "skips small action, revision, and bump when silent" do
|
|
Jobs.run_immediately!
|
|
first_post
|
|
bumped_at = private_message.reload.bumped_at
|
|
|
|
expect do
|
|
TopicConverter.new(private_message, admin, silent: true).convert_to_public_topic(
|
|
category.id,
|
|
)
|
|
end.to not_change { PostRevision.count }
|
|
|
|
expect(private_message.posts.where(post_type: Post.types[:small_action])).to be_empty
|
|
expect(private_message.reload.bumped_at).to be_within(1.second).of(bumped_at)
|
|
end
|
|
|
|
it "deletes notifications for users not allowed to see the topic" do
|
|
staff_category = Fabricate(:private_category, group: Group[:staff])
|
|
user_notification =
|
|
Fabricate(:mentioned_notification, post: first_post, user: Fabricate(:user))
|
|
admin_notification =
|
|
Fabricate(:mentioned_notification, post: first_post, user: Fabricate(:admin))
|
|
|
|
Jobs.run_immediately!
|
|
TopicConverter.new(first_post.topic, admin).convert_to_public_topic(staff_category.id)
|
|
|
|
expect(Notification.exists?(user_notification.id)).to eq(false)
|
|
expect(Notification.exists?(admin_notification.id)).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "convert_to_private_message" do
|
|
fab!(:admin)
|
|
fab!(:author, :user)
|
|
fab!(:category)
|
|
fab!(:topic) { Fabricate(:topic, user: author, category_id: category.id) }
|
|
fab!(:post) { Fabricate(:post, topic: topic, user: topic.user) }
|
|
|
|
context "with success" do
|
|
it "converts regular topic to private message" do
|
|
private_message = topic.convert_to_private_message(post.user)
|
|
expect(private_message).to be_valid
|
|
expect(topic.archetype).to eq("private_message")
|
|
expect(topic.category_id).to eq(nil)
|
|
expect(category.reload.topic_count).to eq(0)
|
|
end
|
|
|
|
it "converts unlisted topic to private message" do
|
|
topic.update_status("visible", false, admin)
|
|
private_message = topic.convert_to_private_message(post.user)
|
|
|
|
expect(private_message).to be_valid
|
|
expect(topic.archetype).to eq("private_message")
|
|
expect(topic.category_id).to eq(nil)
|
|
expect(topic.user.post_count).to eq(0)
|
|
expect(topic.user.topic_count).to eq(0)
|
|
expect(category.reload.topic_count).to eq(0)
|
|
end
|
|
|
|
it "updates user stats when converting topic to private message" do
|
|
_post_2 = Fabricate(:post, topic: topic, user: author)
|
|
_post_3 = Fabricate(:post, topic: topic, user: author)
|
|
|
|
other_topic = Fabricate(:post).topic
|
|
other_post = Fabricate(:post, topic: other_topic)
|
|
|
|
topic_user = TopicUser.create!(user_id: author.id, topic_id: topic.id, posted: true)
|
|
|
|
expect do topic.convert_to_private_message(admin) end.to change {
|
|
author.reload.post_count
|
|
}.from(2).to(0).and change { author.reload.topic_count }.from(1).to(0)
|
|
|
|
# Ensure query does not affect users from other topics or posts as DB query to update count is quite complex.
|
|
expect(other_topic.user.post_count).to eq(0)
|
|
expect(other_topic.user.topic_count).to eq(1)
|
|
expect(other_post.user.post_count).to eq(1)
|
|
expect(other_post.user.topic_count).to eq(0)
|
|
|
|
expect(topic.reload.topic_allowed_users.where(user_id: author.id).count).to eq(1)
|
|
expect(topic_user.reload.notification_level).to eq(TopicUser.notification_levels[:watching])
|
|
end
|
|
|
|
it "invites only users with regular posts" do
|
|
post2 = Fabricate(:post, topic: topic)
|
|
Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
|
|
Fabricate(:post, topic: topic, post_type: Post.types[:small_action])
|
|
|
|
topic.convert_to_private_message(admin)
|
|
|
|
expect(topic.reload.topic_allowed_users.pluck(:user_id)).to contain_exactly(
|
|
admin.id,
|
|
post.user_id,
|
|
post2.user_id,
|
|
)
|
|
end
|
|
|
|
it "changes user_action type" do
|
|
Jobs.run_immediately!
|
|
UserActionManager.enable
|
|
topic.convert_to_private_message(admin)
|
|
expect(author.user_actions.where(action_type: UserAction::NEW_TOPIC).count).to eq(0)
|
|
expect(author.user_actions.where(action_type: UserAction::NEW_PRIVATE_MESSAGE).count).to eq(
|
|
1,
|
|
)
|
|
end
|
|
|
|
it "deletes notifications for users not allowed to see the message" do
|
|
user_notification = Fabricate(:mentioned_notification, post: post, user: Fabricate(:user))
|
|
admin_notification = Fabricate(:mentioned_notification, post: post, user: Fabricate(:admin))
|
|
|
|
Jobs.run_immediately!
|
|
topic.convert_to_private_message(admin)
|
|
|
|
expect(Notification.exists?(user_notification.id)).to eq(false)
|
|
expect(Notification.exists?(admin_notification.id)).to eq(true)
|
|
end
|
|
|
|
it "fails with an error when posters exceed max_allowed_message_recipients" do
|
|
SiteSetting.max_allowed_message_recipients = 2
|
|
Fabricate(:post, topic: topic)
|
|
Fabricate(:post, topic: topic)
|
|
|
|
result = TopicConverter.new(topic, admin).convert_to_private_message
|
|
|
|
expect(result.errors[:base]).to be_present
|
|
expect(topic.reload.archetype).to eq(Archetype.default)
|
|
end
|
|
|
|
it "creates small action and revision when not silent" do
|
|
Jobs.run_immediately!
|
|
|
|
expect do TopicConverter.new(topic, admin).convert_to_private_message end.to change {
|
|
PostRevision.count
|
|
}.by(1)
|
|
|
|
expect(
|
|
topic.posts.where(post_type: Post.types[:small_action], action_code: "private_topic"),
|
|
).to be_present
|
|
end
|
|
|
|
it "skips small action, revision, and bump when silent" do
|
|
Jobs.run_immediately!
|
|
bumped_at = topic.bumped_at
|
|
|
|
expect do
|
|
TopicConverter.new(topic, admin, silent: true).convert_to_private_message
|
|
end.to not_change { PostRevision.count }
|
|
|
|
expect(topic.posts.where(post_type: Post.types[:small_action])).to be_empty
|
|
expect(topic.reload.bumped_at).to be_within(1.second).of(bumped_at)
|
|
end
|
|
|
|
it "includes the poster of a single-post topic" do
|
|
moderator = Fabricate(:moderator)
|
|
private_message = topic.convert_to_private_message(moderator)
|
|
expect(private_message.allowed_users).to match_array([topic.user, moderator])
|
|
end
|
|
end
|
|
|
|
context "when topic has replies" do
|
|
let(:replied_user) { Fabricate(:coding_horror) }
|
|
|
|
before do
|
|
create_post(topic: topic, user: replied_user)
|
|
topic.reload
|
|
end
|
|
|
|
it "adds users who replied to topic in Private Message" do
|
|
topic.convert_to_private_message(admin)
|
|
|
|
expect(topic.reload.topic_allowed_users.where(user_id: replied_user.id).count).to eq(1)
|
|
expect(topic.reload.user.user_stat.post_count).to eq(0)
|
|
end
|
|
end
|
|
|
|
context "when user already exists in topic_allowed_users table" do
|
|
before { topic.topic_allowed_users.create!(user_id: admin.id) }
|
|
|
|
it "works" do
|
|
topic.convert_to_private_message(admin)
|
|
|
|
expect(topic.reload.archetype).to eq("private_message")
|
|
end
|
|
end
|
|
|
|
context "with user_profiles with newly converted PM as featured topic" do
|
|
it "sets all matching user_profile featured topic ids to nil" do
|
|
author.user_profile.update(featured_topic: topic)
|
|
topic.convert_to_private_message(admin)
|
|
|
|
expect(author.user_profile.reload.featured_topic).to eq(nil)
|
|
end
|
|
end
|
|
end
|
|
end
|