mirror of
https://github.com/discourse/discourse.git
synced 2026-08-11 02:59:07 +08:00
Previously, `PostMover` detected a full merge by comparing the sizes of two differently-filtered post sets — a topic-wide census (`regular OR (whisper AND action_code != 'split_topic')`, with no content filter and a NULL-unsafe `!=`) against the movable set (which excludes `small_action` and blank-`raw` posts). Any post the two filters classified differently threw the counts off, so a topic that had ever been assigned (its blank-`raw` tracking whisper is counted but never movable) would never close on a full merge, was never scheduled for deletion, and — via discourse-topic-voting, which only transfers votes once the source topic closes — left its votes stranded. The same asymmetry ran the other way for ordinary content whispers (dropped from the census by the NULL comparison), so one left behind could wrongly close and delete the source with its content still inside. This change computes `@full_move` directly as "no close-preventing post is left out of the move" — a set difference between the close-preventing posts and the moved posts — using a single NULL-safe predicate (`regular`/`whisper`, `raw <> ''`, `action_code IS DISTINCT FROM 'split_topic'`). Fully merged topics now reliably close and transfer their votes, while a topic still holding whisper content correctly stays open.
163 lines
6.4 KiB
Ruby
Vendored
163 lines
6.4 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
describe DiscourseTopicVoting::TopicMerger do
|
|
fab!(:user_0, :user)
|
|
fab!(:user_1, :user)
|
|
fab!(:user_2, :user)
|
|
fab!(:user_3, :user)
|
|
fab!(:user_4, :user)
|
|
fab!(:user_5, :user)
|
|
|
|
fab!(:category_1, :category)
|
|
fab!(:category_2, :category)
|
|
|
|
fab!(:topic_0) { Fabricate(:topic, category: category_1) }
|
|
fab!(:topic_1) { Fabricate(:topic, category: category_2) }
|
|
|
|
before { SiteSetting.topic_voting_enabled = true }
|
|
|
|
describe ".merge" do
|
|
it "does nothing when the source topic is still open" do
|
|
DiscourseTopicVoting::Vote.create!(user: user_0, topic: topic_0)
|
|
topic_0.update_vote_count
|
|
|
|
described_class.merge(topic_0, topic_1)
|
|
|
|
user_0.reload
|
|
expect(user_0.topics_with_vote.pluck(:topic_id)).to contain_exactly(topic_0.id)
|
|
expect(user_0.topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
end
|
|
|
|
it "archives moved votes when the destination topic is closed" do
|
|
topic_0.update_status("closed", true, Discourse.system_user)
|
|
topic_1.update_status("closed", true, Discourse.system_user)
|
|
|
|
DiscourseTopicVoting::Vote.create!(user: user_0, topic: topic_0)
|
|
topic_0.update_vote_count
|
|
|
|
described_class.merge(topic_0, topic_1)
|
|
|
|
user_0.reload
|
|
expect(user_0.topics_with_vote.pluck(:topic_id)).to be_blank
|
|
expect(user_0.topics_with_archived_vote.pluck(:topic_id)).to contain_exactly(topic_1.id)
|
|
expect(topic_0.reload.vote_count).to eq(0)
|
|
expect(topic_1.reload.vote_count).to eq(1)
|
|
end
|
|
|
|
it "enqueues the backfill badges job for the destination topic when votes move" do
|
|
topic_0.update_status("closed", true, Discourse.system_user)
|
|
|
|
DiscourseTopicVoting::Vote.create!(user: user_0, topic: topic_0)
|
|
topic_0.update_vote_count
|
|
|
|
expect { described_class.merge(topic_0, topic_1) }.to change(
|
|
Jobs::DiscourseTopicVoting::BackfillBadges.jobs,
|
|
:size,
|
|
).by(1)
|
|
expect(Jobs::DiscourseTopicVoting::BackfillBadges.jobs.last["args"].first).to include(
|
|
"topic_id" => topic_1.id,
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when merging topics via move_posts (topic_merged)" do
|
|
let(:users) { [user_0, user_1, user_2, user_3, user_4, user_5] }
|
|
|
|
before do
|
|
SiteSetting.topic_voting_show_who_voted = false
|
|
|
|
Fabricate(:post, topic: topic_0, user: user_0)
|
|
Fabricate(:post, topic: topic_0, user: user_0)
|
|
|
|
DiscourseTopicVoting::Vote.create!(user: users[0], topic: topic_0)
|
|
DiscourseTopicVoting::Vote.create!(user: users[1], topic: topic_1)
|
|
DiscourseTopicVoting::Vote.create!(user: users[2], topic: topic_0)
|
|
DiscourseTopicVoting::Vote.create!(user: users[2], topic: topic_1)
|
|
DiscourseTopicVoting::Vote.create!(user: users[4], topic: topic_0, archive: true)
|
|
DiscourseTopicVoting::Vote.create!(user: users[5], topic: topic_0, archive: true)
|
|
DiscourseTopicVoting::Vote.create!(user: users[5], topic: topic_1)
|
|
|
|
[topic_0, topic_1].each { |t| t.update_vote_count }
|
|
end
|
|
|
|
it "moves votes when entire topic is merged" do
|
|
topic_0.move_posts(
|
|
Discourse.system_user,
|
|
topic_0.posts.pluck(:id),
|
|
destination_topic_id: topic_1.id,
|
|
)
|
|
|
|
users.each { |user| user.reload }
|
|
expect(users[0].topics_with_vote.pluck(:topic_id)).to contain_exactly(topic_1.id)
|
|
expect(users[0].topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
|
|
expect(users[1].topics_with_vote.pluck(:topic_id)).to contain_exactly(topic_1.id)
|
|
expect(users[1].topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
|
|
expect(users[2].topics_with_vote.pluck(:topic_id)).to contain_exactly(topic_1.id)
|
|
expect(users[2].topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
|
|
expect(users[3].topics_with_vote.pluck(:topic_id)).to be_blank
|
|
expect(users[3].topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
|
|
expect(users[4].topics_with_vote.pluck(:topic_id)).to contain_exactly(topic_1.id)
|
|
expect(users[4].topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
|
|
expect(users[5].topics_with_vote.pluck(:topic_id)).to contain_exactly(topic_1.id)
|
|
expect(users[5].topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
|
|
expect(topic_0.reload.vote_count).to eq(0)
|
|
expect(topic_1.reload.vote_count).to eq(5)
|
|
|
|
merged_post = topic_0.posts.find_by(action_code: "split_topic")
|
|
expect(merged_post.raw).to include(I18n.t("topic_voting.votes_moved", count: 2))
|
|
expect(merged_post.raw).to include(I18n.t("topic_voting.duplicated_votes", count: 2))
|
|
end
|
|
|
|
it "moves votes when the source topic has an action whisper with no content" do
|
|
Fabricate(
|
|
:post,
|
|
topic: topic_0,
|
|
user: user_0,
|
|
post_type: Post.types[:whisper],
|
|
action_code: "some_action",
|
|
).update_columns(raw: "", cooked: "")
|
|
|
|
topic_0.move_posts(
|
|
Discourse.system_user,
|
|
topic_0.posts.pluck(:id),
|
|
destination_topic_id: topic_1.id,
|
|
)
|
|
|
|
expect(topic_0.reload).to be_closed
|
|
expect(topic_0.vote_count).to eq(0)
|
|
expect(topic_1.reload.vote_count).to eq(5)
|
|
|
|
users[0].reload
|
|
expect(users[0].topics_with_vote.pluck(:topic_id)).to contain_exactly(topic_1.id)
|
|
end
|
|
|
|
it "does not move votes when not all posts are moved and the original topic does not get closed" do
|
|
topic_0.move_posts(
|
|
Discourse.system_user,
|
|
[topic_0.posts.order(:post_number).first.id],
|
|
destination_topic_id: topic_1.id,
|
|
)
|
|
|
|
users.each { |user| user.reload }
|
|
expect(users[0].topics_with_vote.pluck(:topic_id)).to contain_exactly(topic_0.id)
|
|
expect(users[0].topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
expect(users[1].topics_with_vote.pluck(:topic_id)).to contain_exactly(topic_1.id)
|
|
expect(users[1].topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
expect(users[2].topics_with_vote.pluck(:topic_id)).to contain_exactly(topic_0.id, topic_1.id)
|
|
expect(users[2].topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
expect(users[3].topics_with_vote.pluck(:topic_id)).to be_blank
|
|
expect(users[3].topics_with_archived_vote.pluck(:topic_id)).to be_blank
|
|
expect(users[4].topics_with_vote.pluck(:topic_id)).to be_blank
|
|
expect(users[4].topics_with_archived_vote.pluck(:topic_id)).to contain_exactly(topic_0.id)
|
|
|
|
expect(topic_0.reload.vote_count).to eq(4)
|
|
expect(topic_1.reload.vote_count).to eq(3)
|
|
end
|
|
end
|
|
end
|