mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: support for chronologically merging posts into existing topic (#21374)
When a user chooses to move a topic/message to an existing topic/message, they can now opt to merge the posts chronologically (using a checkbox in the UI).
This commit is contained in:
@@ -14,8 +14,9 @@ class PostMover
|
||||
@move_to_pm = move_to_pm
|
||||
end
|
||||
|
||||
def to_topic(id, participants: nil)
|
||||
def to_topic(id, participants: nil, chronological_order: false)
|
||||
@move_type = PostMover.move_types[:existing_topic]
|
||||
@chronological_order = chronological_order
|
||||
|
||||
topic = Topic.find_by_id(id)
|
||||
if topic.archetype != @original_topic.archetype &&
|
||||
@@ -84,8 +85,13 @@ class PostMover
|
||||
.count
|
||||
moving_all_posts = original_topic_posts_count == posts.length
|
||||
|
||||
@first_post_number_moved =
|
||||
posts.first.is_first_post? ? posts[1]&.post_number : posts.first.post_number
|
||||
|
||||
create_temp_table
|
||||
move_each_post
|
||||
handle_moved_references
|
||||
|
||||
create_moderator_post_in_original_topic
|
||||
update_statistics
|
||||
update_user_actions
|
||||
@@ -118,7 +124,30 @@ class PostMover
|
||||
SQL
|
||||
end
|
||||
|
||||
def handle_moved_references
|
||||
move_incoming_emails
|
||||
move_notifications
|
||||
update_reply_counts
|
||||
update_quotes
|
||||
move_first_post_replies
|
||||
delete_post_replies
|
||||
copy_shifted_post_timings_to_temp
|
||||
delete_invalid_post_timings
|
||||
copy_shifted_post_timings_from_temp
|
||||
move_post_timings
|
||||
copy_first_post_timings
|
||||
copy_topic_users
|
||||
end
|
||||
|
||||
def move_each_post
|
||||
if @chronological_order
|
||||
move_each_post_chronological
|
||||
else
|
||||
move_each_post_sequential
|
||||
end
|
||||
end
|
||||
|
||||
def move_each_post_sequential
|
||||
max_post_number = destination_topic.max_post_number + 1
|
||||
|
||||
@post_creator = nil
|
||||
@@ -133,7 +162,7 @@ class PostMover
|
||||
end
|
||||
|
||||
posts.each do |post|
|
||||
metadata = movement_metadata(post)
|
||||
metadata = movement_metadata(post, new_post_number: @move_map[post.post_number])
|
||||
new_post = post.is_first_post? ? create_first_post(post) : move(post)
|
||||
|
||||
store_movement(metadata, new_post)
|
||||
@@ -142,17 +171,72 @@ class PostMover
|
||||
destination_topic.topic_allowed_users.create!(user_id: post.user_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
move_incoming_emails
|
||||
move_notifications
|
||||
update_reply_counts
|
||||
update_quotes
|
||||
move_first_post_replies
|
||||
delete_post_replies
|
||||
delete_invalid_post_timings
|
||||
copy_first_post_timings
|
||||
move_post_timings
|
||||
copy_topic_users
|
||||
def move_each_post_chronological
|
||||
destination_posts = destination_topic.ordered_posts.with_deleted
|
||||
|
||||
# drops posts from destination_topic until it finds one that was created after posts.first
|
||||
min_created_at = posts.first.created_at
|
||||
moved_posts = destination_posts.drop_while { |post| post.created_at <= min_created_at }
|
||||
|
||||
# if no post in destination_topic was created after posts.first it's equal to sequential
|
||||
if moved_posts.empty?
|
||||
initial_post_number = destination_topic.max_post_number + 1
|
||||
else
|
||||
initial_post_number = moved_posts.first.post_number
|
||||
end
|
||||
|
||||
last_index = 0
|
||||
posts.each do |post|
|
||||
while last_index < moved_posts.length && moved_posts[last_index].created_at <= post.created_at
|
||||
last_index += 1
|
||||
end
|
||||
|
||||
moved_posts.insert(last_index, post)
|
||||
end
|
||||
|
||||
@post_creator = nil
|
||||
@move_map = {}
|
||||
@shift_map = {}
|
||||
@reply_count = {}
|
||||
next_post_number = initial_post_number
|
||||
moved_posts.each do |post|
|
||||
if post.topic_id == destination_topic.id
|
||||
# avoid shifting to a lower post number
|
||||
next_post_number = post.post_number if post.post_number > next_post_number
|
||||
|
||||
@shift_map[post.post_number] = next_post_number
|
||||
else
|
||||
@move_map[post.post_number] = next_post_number
|
||||
|
||||
if post.reply_to_post_number.present?
|
||||
@reply_count[post.reply_to_post_number] = (@reply_count[post.reply_to_post_number] || 0) +
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
next_post_number += 1
|
||||
end
|
||||
|
||||
moved_posts.reverse_each do |post|
|
||||
if post.topic_id == destination_topic.id
|
||||
metadata = movement_metadata(post, new_post_number: @shift_map[post.post_number])
|
||||
new_post = move_same_topic(post)
|
||||
else
|
||||
metadata = movement_metadata(post, new_post_number: @move_map[post.post_number])
|
||||
new_post = post.is_first_post? ? create_first_post(post) : move(post)
|
||||
|
||||
if @move_to_pm && !destination_topic.topic_allowed_users.exists?(user_id: post.user_id)
|
||||
destination_topic.topic_allowed_users.create!(user_id: post.user_id)
|
||||
end
|
||||
end
|
||||
|
||||
store_movement(metadata, new_post)
|
||||
end
|
||||
|
||||
# change topic owner if there's a new first post
|
||||
destination_topic.update_column(:user_id, posts.first.user_id) if initial_post_number == 1
|
||||
end
|
||||
|
||||
def create_first_post(post)
|
||||
@@ -175,7 +259,15 @@ class PostMover
|
||||
move_email_logs(post, new_post)
|
||||
|
||||
PostAction.copy(post, new_post)
|
||||
new_post.update_column(:reply_count, @reply_count[1] || 0)
|
||||
|
||||
attrs_to_update = { reply_count: @reply_count[1] || 0 }
|
||||
|
||||
if new_post.post_number != @move_map[post.post_number]
|
||||
attrs_to_update[:post_number] = @move_map[post.post_number]
|
||||
attrs_to_update[:sort_order] = @move_map[post.post_number]
|
||||
end
|
||||
|
||||
new_post.update_columns(attrs_to_update)
|
||||
new_post.custom_fields = post.custom_fields
|
||||
new_post.save_custom_fields
|
||||
|
||||
@@ -189,8 +281,6 @@ class PostMover
|
||||
end
|
||||
|
||||
def move(post)
|
||||
@first_post_number_moved ||= post.post_number
|
||||
|
||||
update = {
|
||||
reply_count: @reply_count[post.post_number] || 0,
|
||||
post_number: @move_map[post.post_number],
|
||||
@@ -213,13 +303,30 @@ class PostMover
|
||||
post
|
||||
end
|
||||
|
||||
def movement_metadata(post)
|
||||
def move_same_topic(post)
|
||||
update = {
|
||||
post_number: @shift_map[post.post_number],
|
||||
sort_order: @shift_map[post.post_number],
|
||||
baked_version: nil,
|
||||
}
|
||||
|
||||
if @shift_map[post.reply_to_post_number]
|
||||
update[:reply_to_post_number] = @shift_map[post.reply_to_post_number]
|
||||
end
|
||||
|
||||
post.attributes = update
|
||||
post.save(validate: false)
|
||||
|
||||
post
|
||||
end
|
||||
|
||||
def movement_metadata(post, new_post_number: nil)
|
||||
{
|
||||
old_topic_id: post.topic_id,
|
||||
old_post_id: post.id,
|
||||
old_post_number: post.post_number,
|
||||
new_topic_id: destination_topic.id,
|
||||
new_post_number: @move_map[post.post_number],
|
||||
new_post_number: new_post_number,
|
||||
new_topic_title: destination_topic.title,
|
||||
}
|
||||
end
|
||||
@@ -240,6 +347,7 @@ class PostMover
|
||||
post_id = mp.new_post_id
|
||||
FROM moved_posts mp
|
||||
WHERE ie.topic_id = mp.old_topic_id AND ie.post_id = mp.old_post_id
|
||||
AND mp.old_topic_id <> mp.new_topic_id
|
||||
SQL
|
||||
end
|
||||
|
||||
@@ -311,6 +419,31 @@ class PostMover
|
||||
SQL
|
||||
end
|
||||
|
||||
def copy_shifted_post_timings_to_temp
|
||||
DB.exec("DROP TABLE IF EXISTS temp_post_timings") if Rails.env.test?
|
||||
|
||||
# copy post_timings for shifted posts to a temp table using the new_post_number
|
||||
# they'll be copied back after delete_invalid_post_timings makes room for them
|
||||
DB.exec <<~SQL
|
||||
CREATE TEMPORARY TABLE temp_post_timings ON COMMIT DROP
|
||||
AS (
|
||||
SELECT pt.topic_id, mp.new_post_number as post_number, pt.user_id, pt.msecs
|
||||
FROM post_timings pt
|
||||
JOIN moved_posts mp
|
||||
ON mp.old_topic_id = pt.topic_id
|
||||
AND mp.old_post_number = pt.post_number
|
||||
AND mp.old_topic_id = mp.new_topic_id
|
||||
)
|
||||
SQL
|
||||
end
|
||||
|
||||
def copy_shifted_post_timings_from_temp
|
||||
DB.exec <<~SQL
|
||||
INSERT INTO post_timings (topic_id, user_id, post_number, msecs)
|
||||
SELECT topic_id, user_id, post_number, msecs FROM temp_post_timings
|
||||
SQL
|
||||
end
|
||||
|
||||
def copy_first_post_timings
|
||||
DB.exec <<~SQL
|
||||
INSERT INTO post_timings (topic_id, user_id, post_number, msecs)
|
||||
@@ -342,6 +475,7 @@ class PostMover
|
||||
WHERE pt.topic_id = mp.old_topic_id
|
||||
AND pt.post_number = mp.old_post_number
|
||||
AND mp.old_post_id = mp.new_post_id
|
||||
AND mp.old_topic_id <> mp.new_topic_id
|
||||
SQL
|
||||
end
|
||||
|
||||
@@ -371,12 +505,14 @@ class PostMover
|
||||
FROM moved_posts lr
|
||||
WHERE lr.old_topic_id = tu.topic_id
|
||||
AND lr.old_post_number <= tu.last_read_post_number
|
||||
AND lr.old_topic_id <> lr.new_topic_id
|
||||
) AS last_read_post_number,
|
||||
(
|
||||
SELECT MAX(le.new_post_number)
|
||||
FROM moved_posts le
|
||||
WHERE le.old_topic_id = tu.topic_id
|
||||
AND le.old_post_number <= tu.last_emailed_post_number
|
||||
AND le.old_topic_id <> le.new_topic_id
|
||||
) AS last_emailed_post_number,
|
||||
GREATEST(tu.first_visited_at, t.created_at) AS first_visited_at,
|
||||
GREATEST(tu.last_visited_at, t.created_at) AS last_visited_at,
|
||||
@@ -389,7 +525,7 @@ class PostMover
|
||||
AND GREATEST(
|
||||
tu.last_read_post_number,
|
||||
tu.last_emailed_post_number
|
||||
) >= (SELECT MIN(old_post_number) FROM moved_posts)
|
||||
) >= (SELECT MIN(mp.old_post_number) FROM moved_posts mp WHERE mp.old_topic_id <> mp.new_topic_id)
|
||||
ON CONFLICT (topic_id, user_id) DO UPDATE
|
||||
SET posted = excluded.posted,
|
||||
last_read_post_number = CASE
|
||||
|
||||
@@ -1236,7 +1236,11 @@ class Topic < ActiveRecord::Base
|
||||
)
|
||||
|
||||
if opts[:destination_topic_id]
|
||||
topic = post_mover.to_topic(opts[:destination_topic_id], participants: opts[:participants])
|
||||
topic =
|
||||
post_mover.to_topic(
|
||||
opts[:destination_topic_id],
|
||||
**opts.slice(:participants, :chronological_order),
|
||||
)
|
||||
|
||||
DiscourseEvent.trigger(:topic_merged, post_mover.original_topic, post_mover.destination_topic)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user