mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-02 04:42:19 +08:00
Previously, the bulk-select dropdown exposed three separate entries for
tag operations ("Add Tags", "Replace Tags", "Remove Tags"), each opening
its own modal with a single tag chooser.
This commit replaces them with a single "Manage tags" item. The modal
combines Remove, Add, and Replace sections so any mix of the three can
be applied in one submission. The Remove section also has a "Remove all
tags" toggle which clears each topic first, then applies whatever is in
the Add and Replace sections on top.
Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com>
1077 lines
34 KiB
Ruby
1077 lines
34 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe TopicsBulkAction do
|
|
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
|
fab!(:topic) { Fabricate(:topic, user: user) }
|
|
|
|
describe "dismiss_topics" do
|
|
fab!(:user) { Fabricate(:user, created_at: 1.day.ago, refresh_auto_groups: true) }
|
|
fab!(:category)
|
|
fab!(:topic2) { Fabricate(:topic, category: category, created_at: 60.minutes.ago) }
|
|
fab!(:topic3) { Fabricate(:topic, category: category, created_at: 120.minutes.ago) }
|
|
|
|
before { topic.destroy! }
|
|
|
|
it "dismisses private messages" do
|
|
pm = Fabricate(:private_message_topic, recipient: user)
|
|
|
|
TopicsBulkAction.new(user, [pm.id], type: "dismiss_topics").perform!
|
|
|
|
expect(DismissedTopicUser.exists?(topic: pm)).to eq(true)
|
|
end
|
|
|
|
it "dismisses two topics" do
|
|
expect {
|
|
TopicsBulkAction.new(user, [Topic.all.pluck(:id)], type: "dismiss_topics").perform!
|
|
}.to change { DismissedTopicUser.count }.by(2)
|
|
end
|
|
|
|
it "returns dismissed topic ids" do
|
|
expect(
|
|
TopicsBulkAction.new(user, [Topic.all.pluck(:id)], type: "dismiss_topics").perform!.sort,
|
|
).to match_array([topic2.id, topic3.id])
|
|
end
|
|
|
|
it "respects max_new_topics limit" do
|
|
SiteSetting.max_new_topics = 1
|
|
expect do
|
|
TopicsBulkAction.new(user, [Topic.all.pluck(:id)], type: "dismiss_topics").perform!
|
|
end.to change { DismissedTopicUser.count }.by(1)
|
|
|
|
dismissed_topic_user = DismissedTopicUser.last
|
|
|
|
expect(dismissed_topic_user.user_id).to eq(user.id)
|
|
expect(dismissed_topic_user.topic_id).to eq(topic2.id)
|
|
expect(dismissed_topic_user.created_at).not_to be_nil
|
|
end
|
|
|
|
it "respects seen topics" do
|
|
Fabricate(:topic_user, user: user, topic: topic2, last_read_post_number: 1)
|
|
Fabricate(:topic_user, user: user, topic: topic3, last_read_post_number: 1)
|
|
expect do
|
|
TopicsBulkAction.new(user, [Topic.all.pluck(:id)], type: "dismiss_topics").perform!
|
|
end.not_to change { DismissedTopicUser.count }
|
|
end
|
|
|
|
it "dismisses when topic user without last_read_post_number" do
|
|
Fabricate(:topic_user, user: user, topic: topic2, last_read_post_number: nil)
|
|
Fabricate(:topic_user, user: user, topic: topic3, last_read_post_number: nil)
|
|
expect do
|
|
TopicsBulkAction.new(user, [Topic.all.pluck(:id)], type: "dismiss_topics").perform!
|
|
end.to change { DismissedTopicUser.count }.by(2)
|
|
end
|
|
|
|
it "respects new_topic_duration_minutes" do
|
|
user.user_option.update!(new_topic_duration_minutes: 70)
|
|
|
|
expect do
|
|
TopicsBulkAction.new(user, [Topic.all.pluck(:id)], type: "dismiss_topics").perform!
|
|
end.to change { DismissedTopicUser.count }.by(1)
|
|
|
|
dismissed_topic_user = DismissedTopicUser.last
|
|
|
|
expect(dismissed_topic_user.user_id).to eq(user.id)
|
|
expect(dismissed_topic_user.topic_id).to eq(topic2.id)
|
|
expect(dismissed_topic_user.created_at).not_to be_nil
|
|
end
|
|
|
|
it "doesn't dismiss topics the user can't see" do
|
|
group = Fabricate(:group)
|
|
private_category = Fabricate(:private_category, group: group)
|
|
topic2.update!(category_id: private_category.id)
|
|
|
|
expect do
|
|
TopicsBulkAction.new(user, [topic2.id, topic3.id], type: "dismiss_topics").perform!
|
|
end.to change { DismissedTopicUser.count }.by(1)
|
|
|
|
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq([topic3.id])
|
|
|
|
group.add(user)
|
|
|
|
expect do
|
|
TopicsBulkAction.new(user, [topic2.id, topic3.id], type: "dismiss_topics").perform!
|
|
end.to change { DismissedTopicUser.count }.by(1)
|
|
|
|
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to contain_exactly(
|
|
topic2.id,
|
|
topic3.id,
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "dismiss_posts" do
|
|
it "dismisses posts" do
|
|
post1 = create_post
|
|
post2 = create_post(topic_id: post1.topic_id)
|
|
create_post(topic_id: post1.topic_id)
|
|
|
|
PostDestroyer.new(Fabricate(:admin), post2).destroy
|
|
|
|
TopicTrackingState.expects(:publish_dismiss_new_posts).with(
|
|
post1.user_id,
|
|
topic_ids: [post1.topic_id],
|
|
)
|
|
|
|
TopicsBulkAction.new(post1.user, [post1.topic_id], type: "dismiss_posts").perform!
|
|
|
|
tu = TopicUser.find_by(user_id: post1.user_id, topic_id: post1.topic_id)
|
|
|
|
expect(tu.last_read_post_number).to eq(3)
|
|
end
|
|
|
|
context "when the user is staff" do
|
|
fab!(:user, :admin)
|
|
|
|
context "when the highest_staff_post_number is > highest_post_number for a topic (e.g. whisper is last post)" do
|
|
it "dismisses posts" do
|
|
SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}"
|
|
post1 = create_post(user: user)
|
|
create_post(topic_id: post1.topic_id)
|
|
create_post(topic_id: post1.topic_id)
|
|
|
|
PostCreator.new(
|
|
user,
|
|
topic_id: post1.topic.id,
|
|
post_type: Post.types[:whisper],
|
|
raw: "this is a whispered reply",
|
|
).create
|
|
|
|
TopicsBulkAction.new(user, [post1.topic_id], type: "dismiss_posts").perform!
|
|
|
|
tu = TopicUser.find_by(user_id: user.id, topic_id: post1.topic_id)
|
|
|
|
expect(tu.last_read_post_number).to eq(4)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "invalid operation" do
|
|
let(:user) { Fabricate.build(:user) }
|
|
|
|
it "raises an error with an invalid operation" do
|
|
tba = TopicsBulkAction.new(user, [1], type: "rm_root")
|
|
expect { tba.perform! }.to raise_error(Discourse::InvalidParameters)
|
|
end
|
|
end
|
|
|
|
describe "change_category" do
|
|
fab!(:category)
|
|
fab!(:first_post) { Fabricate(:post, topic: topic) }
|
|
|
|
describe "silent option" do
|
|
fab!(:admin)
|
|
|
|
before do
|
|
Jobs.run_immediately!
|
|
PostActionNotifier.enable
|
|
SiteSetting.create_revision_on_bulk_topic_moves = true
|
|
TopicUser.change(
|
|
Fabricate(:user),
|
|
topic.id,
|
|
notification_level: TopicUser.notification_levels[:watching],
|
|
)
|
|
CategoryUser.set_notification_level_for_category(
|
|
Fabricate(:user),
|
|
CategoryUser.notification_levels[:watching_first_post],
|
|
category.id,
|
|
)
|
|
end
|
|
|
|
it "does not create any notifications when silent is true" do
|
|
expect do
|
|
TopicsBulkAction.new(
|
|
admin,
|
|
[topic.id],
|
|
type: "change_category",
|
|
category_id: category.id,
|
|
silent: true,
|
|
).perform!
|
|
end.to not_change { Notification.count }
|
|
end
|
|
|
|
it "creates notifications when silent is false" do
|
|
expect do
|
|
TopicsBulkAction.new(
|
|
admin,
|
|
[topic.id],
|
|
type: "change_category",
|
|
category_id: category.id,
|
|
).perform!
|
|
end.to change { Notification.count }
|
|
end
|
|
end
|
|
|
|
context "when the user can edit the topic" do
|
|
context "when create_revision_on_bulk_topic_moves is enabled" do
|
|
before { SiteSetting.create_revision_on_bulk_topic_moves = true }
|
|
|
|
it "changes category and creates revision" do
|
|
old_category_id = topic.category_id
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "change_category",
|
|
category_id: category.id,
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([topic.id])
|
|
expect(topic.reload.category).to eq(category)
|
|
|
|
revision = topic.first_post.revisions.last
|
|
expect(revision.modifications).to eq({ "category_id" => [old_category_id, category.id] })
|
|
end
|
|
end
|
|
|
|
context "when create_revision_on_bulk_topic_moves is disabled" do
|
|
before { SiteSetting.create_revision_on_bulk_topic_moves = false }
|
|
|
|
it "changes category without revision" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "change_category",
|
|
category_id: category.id,
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([topic.id])
|
|
expect(topic.reload.category).to eq(category)
|
|
expect(topic.first_post.revisions.last).to be_nil
|
|
end
|
|
end
|
|
|
|
it "does nothing when category stays the same" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "change_category",
|
|
category_id: topic.category_id,
|
|
).perform!
|
|
|
|
expect(topic_ids).to be_empty
|
|
end
|
|
end
|
|
|
|
context "when the user can't edit the topic" do
|
|
it "doesn't change the category" do
|
|
Guardian.any_instance.expects(:can_edit?).returns(false)
|
|
original_category = topic.category
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "change_category",
|
|
category_id: category.id,
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(topic.reload.category).to eq(original_category)
|
|
end
|
|
end
|
|
|
|
context "when destination category does not allow the topic's tags" do
|
|
fab!(:destination_category, :category)
|
|
fab!(:other_tag) { Fabricate(:tag, name: "other-tag") }
|
|
fab!(:restricted_tag) { Fabricate(:tag, name: "restricted-tag") }
|
|
fab!(:source_category) { Fabricate(:category, tags: [restricted_tag]) }
|
|
fab!(:admin)
|
|
fab!(:topic_with_tag) { Fabricate(:topic, category: source_category, tags: [restricted_tag]) }
|
|
fab!(:first_post_for_tagged_topic) { Fabricate(:post, topic: topic_with_tag) }
|
|
|
|
before { destination_category.update!(tags: [other_tag]) }
|
|
|
|
it "does not change category" do
|
|
original_category = topic_with_tag.category
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
admin,
|
|
[topic_with_tag.id],
|
|
type: "change_category",
|
|
category_id: destination_category.id,
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(topic_with_tag.reload.category).to eq(original_category)
|
|
end
|
|
|
|
it "logs a warning with error details" do
|
|
Rails.logger.expects(:warn).with(includes("restricted-tag"))
|
|
|
|
TopicsBulkAction.new(
|
|
admin,
|
|
[topic_with_tag.id],
|
|
type: "change_category",
|
|
category_id: destination_category.id,
|
|
).perform!
|
|
end
|
|
|
|
it "exposes errors via attr_reader" do
|
|
operator =
|
|
TopicsBulkAction.new(
|
|
admin,
|
|
[topic_with_tag.id],
|
|
type: "change_category",
|
|
category_id: destination_category.id,
|
|
)
|
|
operator.perform!
|
|
|
|
expect(operator.errors).to be_present
|
|
expect(operator.errors.values.sum).to eq(1)
|
|
end
|
|
end
|
|
|
|
context "when destination category has the same tag group as source" do
|
|
fab!(:restricted_tag) { Fabricate(:tag, name: "restricted-tag") }
|
|
fab!(:tag_group) { Fabricate(:tag_group, tags: [restricted_tag]) }
|
|
fab!(:source_category) { Fabricate(:category, tag_groups: [tag_group]) }
|
|
fab!(:destination_category) { Fabricate(:category, tag_groups: [tag_group]) }
|
|
fab!(:admin)
|
|
fab!(:topic_with_tag) { Fabricate(:topic, category: source_category, tags: [restricted_tag]) }
|
|
fab!(:first_post_for_tagged_topic) { Fabricate(:post, topic: topic_with_tag) }
|
|
|
|
it "changes category successfully" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
admin,
|
|
[topic_with_tag.id],
|
|
type: "change_category",
|
|
category_id: destination_category.id,
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([topic_with_tag.id])
|
|
expect(topic_with_tag.reload.category).to eq(destination_category)
|
|
end
|
|
end
|
|
|
|
context "when destination category has allow_global_tags enabled" do
|
|
fab!(:global_tag) { Fabricate(:tag, name: "global-tag") }
|
|
fab!(:source_category, :category)
|
|
fab!(:destination_category) { Fabricate(:category, allow_global_tags: true) }
|
|
fab!(:admin)
|
|
fab!(:topic_with_tag) { Fabricate(:topic, category: source_category, tags: [global_tag]) }
|
|
fab!(:first_post_for_tagged_topic) { Fabricate(:post, topic: topic_with_tag) }
|
|
|
|
it "changes category successfully for unrestricted tags" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
admin,
|
|
[topic_with_tag.id],
|
|
type: "change_category",
|
|
category_id: destination_category.id,
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([topic_with_tag.id])
|
|
expect(topic_with_tag.reload.category).to eq(destination_category)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "destroy_post_timing" do
|
|
fab!(:first_post) { Fabricate(:post, topic: topic) }
|
|
|
|
before { PostTiming.process_timings(topic.user, topic.id, 10, [[1, 10]]) }
|
|
|
|
it "delegates to PostTiming.destroy_for" do
|
|
tba = TopicsBulkAction.new(topic.user, [topic.id], type: "destroy_post_timing")
|
|
topic_ids = nil
|
|
expect { topic_ids = tba.perform! }.to change { PostTiming.count }.by(-1)
|
|
expect(topic_ids).to contain_exactly(topic.id)
|
|
end
|
|
end
|
|
|
|
describe "delete" do
|
|
fab!(:topic) { Fabricate(:post).topic }
|
|
fab!(:moderator)
|
|
|
|
it "deletes the topic" do
|
|
tba = TopicsBulkAction.new(moderator, [topic.id], type: "delete")
|
|
tba.perform!
|
|
topic.reload
|
|
expect(topic).to be_trashed
|
|
end
|
|
end
|
|
|
|
describe "change_notification_level" do
|
|
it "updates the notification level when user can see topic" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "change_notification_level",
|
|
notification_level_id: 2,
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([topic.id])
|
|
expect(TopicUser.get(topic, topic.user).notification_level).to eq(2)
|
|
end
|
|
|
|
it "doesn't change level when user can't see topic" do
|
|
Guardian.any_instance.expects(:can_see?).returns(false)
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "change_notification_level",
|
|
notification_level_id: 2,
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(TopicUser.get(topic, topic.user)).to be_blank
|
|
end
|
|
|
|
["", nil, :missing].each do |invalid_value|
|
|
it "raises error when notification_level_id is #{invalid_value.inspect}" do
|
|
options = { type: "change_notification_level" }
|
|
options[:notification_level_id] = invalid_value unless invalid_value == :missing
|
|
|
|
expect do
|
|
TopicsBulkAction.new(topic.user, [topic.id], **options).perform!
|
|
end.to raise_error(Discourse::InvalidParameters, /notification_level_id/)
|
|
end
|
|
end
|
|
end
|
|
|
|
%w[close archive unlist].each do |action|
|
|
describe action do
|
|
it "#{action}s topic when user can moderate" do
|
|
Guardian.any_instance.expects(:can_moderate?).returns(true)
|
|
Guardian.any_instance.expects(:can_create?).returns(true)
|
|
|
|
topic_ids = TopicsBulkAction.new(topic.user, [topic.id], type: action).perform!
|
|
|
|
expect(topic_ids).to eq([topic.id])
|
|
topic.reload
|
|
case action
|
|
when "close"
|
|
expect(topic).to be_closed
|
|
when "archive"
|
|
expect(topic).to be_archived
|
|
when "unlist"
|
|
expect(topic).not_to be_visible
|
|
end
|
|
end
|
|
|
|
it "doesn't #{action} topic when user can't moderate" do
|
|
Guardian.any_instance.expects(:can_moderate?).returns(false)
|
|
|
|
topic_ids = TopicsBulkAction.new(topic.user, [topic.id], type: action).perform!
|
|
|
|
expect(topic_ids).to be_blank
|
|
topic.reload
|
|
case action
|
|
when "close"
|
|
expect(topic).not_to be_closed
|
|
when "archive"
|
|
expect(topic).not_to be_archived
|
|
when "unlist"
|
|
expect(topic).to be_visible
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "reset_bump_dates" do
|
|
it "resets bump date when user can update" do
|
|
post_created_at = 1.day.ago
|
|
create_post(topic_id: topic.id, created_at: post_created_at)
|
|
topic.update!(bumped_at: 1.hour.ago)
|
|
Guardian.any_instance.expects(:can_update_bumped_at?).returns(true)
|
|
|
|
topic_ids = TopicsBulkAction.new(topic.user, [topic.id], type: "reset_bump_dates").perform!
|
|
|
|
expect(topic_ids).to eq([topic.id])
|
|
expect(topic.reload.bumped_at).to eq_time(post_created_at)
|
|
end
|
|
|
|
it "doesn't reset bump date when user can't update" do
|
|
create_post(topic_id: topic.id, created_at: 1.day.ago)
|
|
bumped_at = 1.hour.ago
|
|
topic.update!(bumped_at: bumped_at)
|
|
Guardian.any_instance.expects(:can_update_bumped_at?).returns(false)
|
|
|
|
topic_ids = TopicsBulkAction.new(topic.user, [topic.id], type: "reset_bump_dates").perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(topic.reload.bumped_at).to eq_time(bumped_at)
|
|
end
|
|
end
|
|
|
|
describe "#pin" do
|
|
fab!(:moderator)
|
|
fab!(:topic_2, :topic)
|
|
|
|
it "pins topics in category when user can moderate" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
moderator,
|
|
[topic.id, topic_2.id],
|
|
type: "pin",
|
|
pinned_globally: false,
|
|
).perform!
|
|
|
|
expect(topic_ids).to contain_exactly(topic.id, topic_2.id)
|
|
expect(topic.reload.pinned_at).to be_present
|
|
expect(topic.pinned_globally).to eq(false)
|
|
expect(topic_2.reload.pinned_at).to be_present
|
|
expect(topic_2.pinned_globally).to eq(false)
|
|
end
|
|
|
|
it "pins topics globally with pinned_until when provided" do
|
|
pinned_until = 3.days.from_now.beginning_of_minute.to_s
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
moderator,
|
|
[topic.id, topic_2.id],
|
|
type: "pin",
|
|
pinned_globally: true,
|
|
pinned_until: pinned_until,
|
|
).perform!
|
|
|
|
expect(topic_ids).to contain_exactly(topic.id, topic_2.id)
|
|
expect(topic.reload.pinned_globally).to eq(true)
|
|
expect(topic.pinned_until).to be_within_one_second_of(Time.parse(pinned_until))
|
|
expect(topic_2.reload.pinned_globally).to eq(true)
|
|
expect(topic_2.pinned_until).to be_within_one_second_of(Time.parse(pinned_until))
|
|
end
|
|
|
|
it "does not pin when user cannot moderate" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
user,
|
|
[topic.id, topic_2.id],
|
|
type: "pin",
|
|
pinned_globally: false,
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(topic.reload.pinned_at).to be_nil
|
|
expect(topic_2.reload.pinned_at).to be_nil
|
|
end
|
|
end
|
|
|
|
describe "#unpin" do
|
|
fab!(:moderator)
|
|
fab!(:topic_2, :topic)
|
|
|
|
it "unpins multiple pinned topics and clears pinned_until when user can moderate" do
|
|
topic.update_pinned(true, false, 3.days.from_now.to_s)
|
|
topic_2.update_pinned(true, true, 5.days.from_now.to_s)
|
|
|
|
topic_ids = TopicsBulkAction.new(moderator, [topic.id, topic_2.id], type: "unpin").perform!
|
|
|
|
expect(topic_ids).to contain_exactly(topic.id, topic_2.id)
|
|
expect(topic.reload.pinned_at).to be_nil
|
|
expect(topic.pinned_until).to be_nil
|
|
expect(topic_2.reload.pinned_at).to be_nil
|
|
expect(topic_2.pinned_until).to be_nil
|
|
end
|
|
|
|
it "skips topics that are not pinned" do
|
|
topic.update_pinned(true, false)
|
|
|
|
topic_ids = TopicsBulkAction.new(moderator, [topic.id, topic_2.id], type: "unpin").perform!
|
|
|
|
expect(topic_ids).to eq([topic.id])
|
|
end
|
|
|
|
it "does not unpin when user cannot moderate" do
|
|
topic.update_pinned(true, false)
|
|
topic_2.update_pinned(true, false)
|
|
|
|
topic_ids = TopicsBulkAction.new(user, [topic.id, topic_2.id], type: "unpin").perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(topic.reload.pinned_at).to be_present
|
|
expect(topic_2.reload.pinned_at).to be_present
|
|
end
|
|
end
|
|
|
|
describe "change_tags" do
|
|
fab!(:first_post) { Fabricate(:post, topic:) }
|
|
fab!(:tag1, :tag)
|
|
fab!(:tag2, :tag)
|
|
|
|
before do
|
|
SiteSetting.tagging_enabled = true
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
topic.tags = [tag1, tag2]
|
|
end
|
|
|
|
context "when the user can edit the topic" do
|
|
fab!(:tag3, :tag)
|
|
|
|
it "changes tags to specified tags" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "change_tags",
|
|
tag_ids: [tag1.id, tag3.id],
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([topic.id])
|
|
expect(topic.reload.tags).to contain_exactly(tag1, tag3)
|
|
end
|
|
|
|
it "removes all tags with empty array" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(topic.user, [topic.id], type: "change_tags", tag_ids: []).perform!
|
|
|
|
expect(topic_ids).to eq([topic.id])
|
|
expect(topic.reload.tags).to be_empty
|
|
end
|
|
end
|
|
|
|
context "when tagging fails due to tag restrictions" do
|
|
fab!(:restricted_tag) { Fabricate(:tag, name: "restricted-tag") }
|
|
fab!(:tag_group) do
|
|
Fabricate(:tag_group, tags: [restricted_tag], permissions: { staff: :full })
|
|
end
|
|
|
|
it "does not include the topic in changed_ids and logs a warning" do
|
|
Rails.logger.expects(:warn).with(includes("restricted-tag"))
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "change_tags",
|
|
tag_ids: [restricted_tag.id],
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
end
|
|
end
|
|
|
|
context "when the user can't edit the topic" do
|
|
fab!(:tag3, :tag)
|
|
|
|
it "doesn't change the tags" do
|
|
Guardian.any_instance.expects(:can_edit?).returns(false)
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "change_tags",
|
|
tag_ids: [tag3.id],
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(topic.reload.tags).to contain_exactly(tag1, tag2)
|
|
end
|
|
end
|
|
|
|
it "does not send notifications" do
|
|
fab_tag3 = Fabricate(:tag)
|
|
topic_watcher = Fabricate(:user)
|
|
Jobs.run_immediately!
|
|
TopicUser.change(
|
|
topic_watcher,
|
|
topic.id,
|
|
notification_level: TopicUser.notification_levels[:watching],
|
|
)
|
|
|
|
expect do
|
|
TopicsBulkAction.new(
|
|
Fabricate(:admin),
|
|
[topic.id],
|
|
type: "change_tags",
|
|
tag_ids: [fab_tag3.id],
|
|
).perform!
|
|
end.to not_change { Notification.where(user: topic_watcher).count }
|
|
end
|
|
end
|
|
|
|
describe "append_tags" do
|
|
fab!(:first_post) { Fabricate(:post, topic:) }
|
|
fab!(:tag1, :tag)
|
|
fab!(:tag2, :tag)
|
|
fab!(:tag3, :tag)
|
|
|
|
before do
|
|
SiteSetting.tagging_enabled = true
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
topic.tags = [tag1, tag2]
|
|
end
|
|
|
|
context "when the user can edit the topic" do
|
|
it "appends existing tags" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "append_tags",
|
|
tag_ids: [tag3.id],
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([topic.id])
|
|
expect(topic.reload.tags).to contain_exactly(tag1, tag2, tag3)
|
|
end
|
|
|
|
it "is a no-op when appending empty array" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(topic.user, [topic.id], type: "append_tags", tag_ids: []).perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(topic.reload.tags).to contain_exactly(tag1, tag2)
|
|
end
|
|
end
|
|
|
|
context "when tagging fails due to tag restrictions" do
|
|
fab!(:restricted_tag) { Fabricate(:tag, name: "restricted-tag") }
|
|
fab!(:tag_group) do
|
|
Fabricate(:tag_group, tags: [restricted_tag], permissions: { staff: :full })
|
|
end
|
|
|
|
it "does not include the topic in changed_ids and logs a warning" do
|
|
Rails.logger.expects(:warn).with(includes("restricted-tag"))
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "append_tags",
|
|
tag_ids: [restricted_tag.id],
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(topic.reload.tags).to contain_exactly(tag1, tag2)
|
|
end
|
|
end
|
|
|
|
context "when the user can't edit the topic" do
|
|
it "doesn't change the tags" do
|
|
Guardian.any_instance.expects(:can_edit?).returns(false)
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
topic.user,
|
|
[topic.id],
|
|
type: "append_tags",
|
|
tag_ids: [tag3.id],
|
|
).perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(topic.reload.tags).to contain_exactly(tag1, tag2)
|
|
end
|
|
end
|
|
|
|
it "does not send notifications" do
|
|
topic_watcher = Fabricate(:user)
|
|
Jobs.run_immediately!
|
|
TopicUser.change(
|
|
topic_watcher,
|
|
topic.id,
|
|
notification_level: TopicUser.notification_levels[:watching],
|
|
)
|
|
|
|
expect do
|
|
TopicsBulkAction.new(
|
|
Fabricate(:admin),
|
|
[topic.id],
|
|
type: "append_tags",
|
|
tag_ids: [tag3.id],
|
|
).perform!
|
|
end.to not_change { Notification.where(user: topic_watcher).count }
|
|
end
|
|
end
|
|
|
|
describe "remove_tags" do
|
|
fab!(:first_post) { Fabricate(:post, topic:) }
|
|
fab!(:tag1, :tag)
|
|
fab!(:tag2, :tag)
|
|
|
|
before do
|
|
SiteSetting.tagging_enabled = true
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
topic.tags = [tag1, tag2]
|
|
end
|
|
|
|
context "when the user can edit the topic" do
|
|
it "removes all tags and updates tag counts" do
|
|
expect(tag1.reload.staff_topic_count).to eq(1)
|
|
|
|
topic_ids = TopicsBulkAction.new(topic.user, [topic.id], type: "remove_tags").perform!
|
|
|
|
expect(topic_ids).to eq([topic.id])
|
|
expect(topic.reload.tags).to be_empty
|
|
expect(tag1.reload.staff_topic_count).to eq(0)
|
|
end
|
|
end
|
|
|
|
context "when the user can't edit the topic" do
|
|
it "doesn't remove the tags" do
|
|
Guardian.any_instance.expects(:can_edit?).returns(false)
|
|
|
|
topic_ids = TopicsBulkAction.new(topic.user, [topic.id], type: "remove_tags").perform!
|
|
|
|
expect(topic_ids).to eq([])
|
|
expect(topic.reload.tags).to contain_exactly(tag1, tag2)
|
|
end
|
|
end
|
|
|
|
it "does not send notifications" do
|
|
topic_watcher = Fabricate(:user)
|
|
Jobs.run_immediately!
|
|
TopicUser.change(
|
|
topic_watcher,
|
|
topic.id,
|
|
notification_level: TopicUser.notification_levels[:watching],
|
|
)
|
|
|
|
expect do
|
|
TopicsBulkAction.new(Fabricate(:admin), [topic.id], type: "remove_tags").perform!
|
|
end.to not_change { Notification.where(user: topic_watcher).count }
|
|
end
|
|
end
|
|
|
|
describe "convert_to_public_topic" do
|
|
fab!(:admin)
|
|
fab!(:category)
|
|
fab!(:pm, :private_message_topic)
|
|
fab!(:pm_post) { Fabricate(:post, topic: pm, user: pm.user) }
|
|
|
|
it "converts PMs and skips non-PMs" do
|
|
regular = Fabricate(:post).topic
|
|
|
|
changed =
|
|
TopicsBulkAction.new(
|
|
admin,
|
|
[pm.id, regular.id],
|
|
type: "convert_to_public_topic",
|
|
category_id: category.id,
|
|
).perform!
|
|
|
|
expect(changed).to eq([pm.id])
|
|
expect(pm.reload.archetype).to eq(Archetype.default)
|
|
expect(pm.category_id).to eq(category.id)
|
|
end
|
|
|
|
it "is silent by default" do
|
|
Jobs.run_immediately!
|
|
bumped_at = pm.bumped_at
|
|
|
|
expect do
|
|
TopicsBulkAction.new(
|
|
admin,
|
|
[pm.id],
|
|
type: "convert_to_public_topic",
|
|
category_id: category.id,
|
|
).perform!
|
|
end.to not_change { PostRevision.count }
|
|
|
|
expect(pm.reload.bumped_at).to be_within(1.second).of(bumped_at)
|
|
expect(pm.posts.where(post_type: Post.types[:small_action])).to be_empty
|
|
end
|
|
end
|
|
|
|
describe "convert_to_private_message" do
|
|
fab!(:admin)
|
|
fab!(:public_topic, :topic)
|
|
fab!(:public_first_post) { Fabricate(:post, topic: public_topic, user: public_topic.user) }
|
|
|
|
it "converts public topics and skips PMs" do
|
|
pm = Fabricate(:private_message_topic)
|
|
Fabricate(:post, topic: pm, user: pm.user)
|
|
|
|
changed =
|
|
TopicsBulkAction.new(
|
|
admin,
|
|
[public_topic.id, pm.id],
|
|
type: "convert_to_private_message",
|
|
).perform!
|
|
|
|
expect(changed).to eq([public_topic.id])
|
|
expect(public_topic.reload.archetype).to eq(Archetype.private_message)
|
|
end
|
|
|
|
it "surfaces errors and does not mark topic as changed when cap is exceeded" do
|
|
SiteSetting.max_allowed_message_recipients = 1
|
|
Fabricate(:post, topic: public_topic)
|
|
|
|
operator = TopicsBulkAction.new(admin, [public_topic.id], type: "convert_to_private_message")
|
|
changed = operator.perform!
|
|
|
|
expect(changed).to be_empty
|
|
expect(public_topic.reload.archetype).to eq(Archetype.default)
|
|
expect(operator.errors).to be_present
|
|
end
|
|
end
|
|
|
|
describe "#manage_tags" do
|
|
fab!(:tag_1, :tag)
|
|
fab!(:tag_2, :tag)
|
|
fab!(:tag_3, :tag)
|
|
fab!(:tag_4, :tag)
|
|
fab!(:topic_1) { Fabricate(:topic_with_op, user: user, tags: [tag_1, tag_2]) }
|
|
fab!(:topic_2) { Fabricate(:topic_with_op, user: user, tags: [tag_2, tag_3]) }
|
|
fab!(:topic_3) { Fabricate(:topic_with_op, user: user, tags: [tag_1]) }
|
|
|
|
before do
|
|
SiteSetting.tagging_enabled = true
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
end
|
|
|
|
it "adds tags to each topic on top of its existing tags" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
user,
|
|
[topic_1.id, topic_2.id, topic_3.id],
|
|
type: "manage_tags",
|
|
add_tag_ids: [tag_3.id, tag_4.id],
|
|
).perform!
|
|
|
|
expect(topic_ids).to contain_exactly(topic_1.id, topic_2.id, topic_3.id)
|
|
expect(topic_1.reload.tags).to contain_exactly(tag_1, tag_2, tag_3, tag_4)
|
|
expect(topic_2.reload.tags).to contain_exactly(tag_2, tag_3, tag_4)
|
|
expect(topic_3.reload.tags).to contain_exactly(tag_1, tag_3, tag_4)
|
|
end
|
|
|
|
it "removes specific tags only from topics that have them" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
user,
|
|
[topic_1.id, topic_2.id, topic_3.id],
|
|
type: "manage_tags",
|
|
remove_tag_ids: [tag_1.id, tag_2.id],
|
|
).perform!
|
|
|
|
expect(topic_ids).to contain_exactly(topic_1.id, topic_2.id, topic_3.id)
|
|
expect(topic_1.reload.tags).to be_empty
|
|
expect(topic_2.reload.tags).to contain_exactly(tag_3)
|
|
expect(topic_3.reload.tags).to be_empty
|
|
end
|
|
|
|
it "replaces tags on topics that have the source tag" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
user,
|
|
[topic_1.id, topic_2.id, topic_3.id],
|
|
type: "manage_tags",
|
|
replace_tags: [
|
|
{ from_tag_id: tag_2.id, to_tag_id: tag_3.id },
|
|
{ from_tag_id: tag_3.id, to_tag_id: tag_4.id },
|
|
],
|
|
).perform!
|
|
|
|
expect(topic_ids).to contain_exactly(topic_1.id, topic_2.id)
|
|
expect(topic_1.reload.tags).to contain_exactly(tag_1, tag_3)
|
|
expect(topic_2.reload.tags).to contain_exactly(tag_3, tag_4)
|
|
expect(topic_3.reload.tags).to contain_exactly(tag_1)
|
|
end
|
|
|
|
it "applies remove, then add, then replace in that order" do
|
|
TopicsBulkAction.new(
|
|
user,
|
|
[topic_1.id, topic_2.id, topic_3.id],
|
|
type: "manage_tags",
|
|
remove_tag_ids: [tag_1.id, tag_2.id],
|
|
add_tag_ids: [tag_1.id, tag_3.id],
|
|
replace_tags: [
|
|
{ from_tag_id: tag_1.id, to_tag_id: tag_4.id },
|
|
{ from_tag_id: tag_3.id, to_tag_id: tag_2.id },
|
|
],
|
|
).perform!
|
|
|
|
expect(topic_1.reload.tags).to contain_exactly(tag_2, tag_4)
|
|
expect(topic_2.reload.tags).to contain_exactly(tag_2, tag_4)
|
|
expect(topic_3.reload.tags).to contain_exactly(tag_2, tag_4)
|
|
end
|
|
|
|
it "can clear all tags with remove_all_tags" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
user,
|
|
[topic_1.id, topic_2.id, topic_3.id],
|
|
type: "manage_tags",
|
|
remove_all_tags: true,
|
|
).perform!
|
|
|
|
expect(topic_ids).to contain_exactly(topic_1.id, topic_2.id, topic_3.id)
|
|
expect(topic_1.reload.tags).to be_empty
|
|
expect(topic_2.reload.tags).to be_empty
|
|
expect(topic_3.reload.tags).to be_empty
|
|
end
|
|
|
|
it "applies replacements sequentially so the first matching pair wins" do
|
|
TopicsBulkAction.new(
|
|
user,
|
|
[topic_1.id, topic_2.id, topic_3.id],
|
|
type: "manage_tags",
|
|
replace_tags: [
|
|
{ from_tag_id: tag_1.id, to_tag_id: tag_2.id },
|
|
{ from_tag_id: tag_1.id, to_tag_id: tag_3.id },
|
|
],
|
|
).perform!
|
|
|
|
expect(topic_1.reload.tags).to contain_exactly(tag_2)
|
|
expect(topic_2.reload.tags).to contain_exactly(tag_2, tag_3)
|
|
expect(topic_3.reload.tags).to contain_exactly(tag_2)
|
|
end
|
|
|
|
it "does not bump, revise, or notify watchers" do
|
|
topic_watcher = Fabricate(:user)
|
|
Jobs.run_immediately!
|
|
TopicUser.change(
|
|
topic_watcher,
|
|
topic_1.id,
|
|
notification_level: TopicUser.notification_levels[:watching],
|
|
)
|
|
topic_1.update!(bumped_at: 1.week.ago)
|
|
|
|
notifications_before = Notification.where(user: topic_watcher).count
|
|
bumped_at_before = topic_1.bumped_at
|
|
version_before = topic_1.first_post.version
|
|
|
|
TopicsBulkAction.new(
|
|
Fabricate(:admin),
|
|
[topic_1.id],
|
|
type: "manage_tags",
|
|
add_tag_ids: [tag_4.id],
|
|
).perform!
|
|
|
|
expect(Notification.where(user: topic_watcher).count).to eq(notifications_before)
|
|
expect(topic_1.reload.bumped_at).to be_within(1.second).of(bumped_at_before)
|
|
expect(topic_1.first_post.reload.version).to eq(version_before)
|
|
end
|
|
|
|
it "only updates topics the acting user can edit" do
|
|
other_user = Fabricate(:user, refresh_auto_groups: true)
|
|
other_topic = Fabricate(:topic_with_op, user: other_user, tags: [tag_1])
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
other_user,
|
|
[topic_1.id, other_topic.id],
|
|
type: "manage_tags",
|
|
add_tag_ids: [tag_4.id],
|
|
).perform!
|
|
|
|
expect(topic_ids).to contain_exactly(other_topic.id)
|
|
expect(topic_1.reload.tags).to contain_exactly(tag_1, tag_2)
|
|
expect(other_topic.reload.tags).to contain_exactly(tag_1, tag_4)
|
|
end
|
|
|
|
it "ignores unknown tag ids" do
|
|
topic_ids =
|
|
TopicsBulkAction.new(
|
|
user,
|
|
[topic_1.id],
|
|
type: "manage_tags",
|
|
add_tag_ids: [999_999],
|
|
).perform!
|
|
|
|
expect(topic_ids).to be_empty
|
|
expect(topic_1.reload.tags).to contain_exactly(tag_1, tag_2)
|
|
end
|
|
end
|
|
end
|