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:
Renato Atilio
2023-05-25 15:38:34 -03:00
committed by GitHub
parent 6a65fa982d
commit c539f749f1
11 changed files with 1346 additions and 23 deletions

View File

@@ -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

View File

@@ -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)