discourse/plugins/poll/spec/integration/post_mover_spec.rb
Alan Guo Xiang Tan 9b8c97c9fa
FIX: preserve poll data when moving posts to another topic (#37791)
What is the problem?

The poll plugin does not handle the `post_moved` event fired by
`PostMover`. For normal reply moves `Post#id` is unchanged so poll
data is retained, but when moving the first post of a topic
(`PostMover` recreates it via `PostCreator#create!` in the
destination topic) or when an admin moves a reply with the "keep a
copy in the original topic" option (`PostMover#move` with
`freeze_original: true`, which duplicates the post), the new post
gets a different `Post#id` and the `Poll` records remain pointing at
the old `Post#id` via `Poll#post_id`, orphaning them along with
their associated `PollOption` and `PollVote` records.

What is the solution?

Add an `on(:post_moved)` handler in `plugins/poll/plugin.rb` that:

1. Skips processing when `Post#id` is unchanged (normal reply moves).
2. Removes empty `Poll` and `PollOption` records that the poll
   plugin's `validate_post` hook auto-creates when `PostCreator`
   rebuilds the post from raw. Deletes in FK order: `PollVote` →
   `PollOption` → `Poll`.
3. Reassigns the original polls — which carry the actual votes — to
   the new post by updating `Poll#post_id` via `Poll.update_all`.
4. Updates `DiscoursePoll::HAS_POLLS` custom field on both posts via
   `PollsUpdater.update_post_custom_fields` — sets it on the new
   post (needed for the `freeze_original` path which bypasses
   `PostCreator`) and clears it on the old post so
   `TopicView#polls` does not issue unnecessary queries.
5. Wraps steps 2-3 in an `ActiveRecord::Base.transaction`.
2026-02-13 12:30:12 +08:00

97 lines
3.2 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe "Poll post_moved handler" do
fab!(:admin)
fab!(:user1, :user)
fab!(:user2, :user)
fab!(:first_post) do
Fabricate(
:post,
user: admin,
raw: "[poll name=first_post_poll type=regular]\n- Option A\n- Option B\n[/poll]",
)
end
fab!(:source_topic) { first_post.topic }
fab!(:reply) do
Fabricate(
:post,
topic: source_topic,
user: admin,
raw: "[poll name=reply_poll type=regular]\n- Option A\n- Option B\n[/poll]",
)
end
fab!(:first_post_poll) { first_post.polls.find_by(name: "first_post_poll") }
fab!(:reply_poll) { reply.polls.find_by(name: "reply_poll") }
fab!(:user1_first_post_poll_votes) do
first_post_poll.poll_options.map do |option|
Fabricate(:poll_vote, poll: first_post_poll, poll_option: option, user: user1)
end
end
fab!(:user2_reply_poll_votes) do
reply_poll.poll_options.map do |option|
Fabricate(:poll_vote, poll: reply_poll, poll_option: option, user: user2)
end
end
fab!(:destination_topic) { Fabricate(:post, user: admin).topic }
before { Jobs.run_immediately! }
it "moves polls, options, and votes when the first post is moved" do
original_options = first_post_poll.poll_options.to_a
source_topic.move_posts(admin, [first_post.id], destination_topic_id: destination_topic.id)
new_post = destination_topic.posts.last
moved_poll = new_post.polls.find_by(name: "first_post_poll")
expect(moved_poll).to eq(first_post_poll)
expect(moved_poll.poll_options).to contain_exactly(*original_options)
expect(moved_poll.poll_votes.map { |vote| [vote.user, vote.poll_option] }).to contain_exactly(
*original_options.map { |option| [user1, option] },
)
expect(new_post.custom_fields[DiscoursePoll::HAS_POLLS]).to eq(true)
expect(Poll.exists?(post_id: first_post.id)).to eq(false)
end
it "moves polls when a copy is kept in the original topic" do
original_options = reply_poll.poll_options.to_a
PostMover.new(source_topic, admin, [reply.id], options: { freeze_original: true }).to_topic(
destination_topic.id,
)
moved_reply = destination_topic.posts.last
moved_poll = moved_reply.polls.find_by(name: "reply_poll")
expect(moved_poll).to eq(reply_poll)
expect(moved_poll.poll_options).to contain_exactly(*original_options)
expect(moved_poll.poll_votes.map { |vote| [vote.user, vote.poll_option] }).to contain_exactly(
*original_options.map { |option| [user2, option] },
)
expect(moved_reply.custom_fields[DiscoursePoll::HAS_POLLS]).to eq(true)
expect(Poll.exists?(post_id: reply.id)).to eq(false)
expect(reply.reload.custom_fields[DiscoursePoll::HAS_POLLS]).to eq(nil)
end
it "preserves polls when a reply is moved without creating a new post" do
source_topic.move_posts(admin, [reply.id], destination_topic_id: destination_topic.id)
expect(reply.reload.topic_id).to eq(destination_topic.id)
poll = reply.polls.find_by(name: "reply_poll")
expect(poll).to eq(reply_poll)
expect(poll.poll_options).to contain_exactly(*reply_poll.poll_options)
expect(poll.poll_votes.map { |vote| [vote.user, vote.poll_option] }).to contain_exactly(
*poll.poll_options.map { |option| [user2, option] },
)
end
end