mirror of
https://github.com/discourse/discourse.git
synced 2024-12-18 21:54:22 +08:00
415abe6491
When freeze_original option is passed to PostMover, and we are moving all posts there is an issue. We attempt to put the small_action right after the last moved post. The issue is when there is an existing small action after the last moved "real" post. We then try to put the moderator post at the same location of the existing small action, which causes an index conflict and the move fails. This makes sure that we place the moderator post at the verrrrrry end of the topic :)
789 lines
27 KiB
Ruby
789 lines
27 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class PostMover
|
|
attr_reader :original_topic, :destination_topic, :user, :post_ids
|
|
|
|
def self.move_types
|
|
@move_types ||= Enum.new(:new_topic, :existing_topic)
|
|
end
|
|
|
|
# options:
|
|
# freeze_original: :boolean - if true, the original topic will be frozen but not deleted and posts will be "copied" to topic
|
|
def initialize(original_topic, user, post_ids, move_to_pm: false, options: {})
|
|
@original_topic = original_topic
|
|
@original_topic_title = original_topic.title
|
|
@user = user
|
|
@post_ids = post_ids
|
|
# For now we store a copy of post_ids. If `freeze_original` is present, we will have new post_ids.
|
|
# When we create the new posts, we will pluck out post_ids out of this and replace with updated ids.
|
|
@post_ids_after_move = post_ids
|
|
@move_to_pm = move_to_pm
|
|
@options = options
|
|
end
|
|
|
|
def to_topic(id, participants: nil, chronological_order: false)
|
|
@move_type = PostMover.move_types[:existing_topic]
|
|
@creating_new_topic = false
|
|
@chronological_order = chronological_order
|
|
|
|
topic = Topic.find_by_id(id)
|
|
if topic.archetype != @original_topic.archetype &&
|
|
[@original_topic.archetype, topic.archetype].include?(Archetype.private_message)
|
|
raise Discourse::InvalidParameters
|
|
end
|
|
|
|
Topic.transaction { move_posts_to topic }
|
|
add_allowed_users(participants) if participants.present? && @move_to_pm
|
|
enqueue_jobs(topic)
|
|
topic
|
|
end
|
|
|
|
def to_new_topic(title, category_id = nil, tags = nil)
|
|
@move_type = PostMover.move_types[:new_topic]
|
|
@creating_new_topic = true
|
|
|
|
post = Post.find_by(id: post_ids.first)
|
|
raise Discourse::InvalidParameters unless post
|
|
archetype = @move_to_pm ? Archetype.private_message : Archetype.default
|
|
|
|
topic =
|
|
Topic.transaction do
|
|
new_topic =
|
|
Topic.create!(
|
|
user: post.user,
|
|
title: title,
|
|
category_id: category_id,
|
|
created_at: post.created_at,
|
|
archetype: archetype,
|
|
)
|
|
DiscourseTagging.tag_topic_by_names(new_topic, Guardian.new(user), tags)
|
|
move_posts_to new_topic
|
|
watch_new_topic
|
|
update_topic_excerpt new_topic
|
|
new_topic
|
|
end
|
|
enqueue_jobs(topic)
|
|
topic
|
|
end
|
|
|
|
private
|
|
|
|
def update_topic_excerpt(topic)
|
|
topic.update_excerpt(topic.first_post.excerpt_for_topic)
|
|
end
|
|
|
|
def move_posts_to(topic)
|
|
Guardian.new(user).ensure_can_see! topic
|
|
@destination_topic = topic
|
|
|
|
# when a topic contains some posts after moving posts to another topic we shouldn't close it
|
|
# two types of posts should prevent a topic from closing:
|
|
# 1. regular posts
|
|
# 2. almost all whispers
|
|
# we should only exclude whispers with action_code: 'split_topic'
|
|
# because we use such whispers as a small-action posts when moving posts to the secret message
|
|
# (in this case we don't want everyone to see that posts were moved, that's why we use whispers)
|
|
original_topic_posts_count =
|
|
@original_topic
|
|
.posts
|
|
.where(
|
|
"post_type = ? or (post_type = ? and action_code != 'split_topic')",
|
|
Post.types[:regular],
|
|
Post.types[:whisper],
|
|
)
|
|
.count
|
|
@full_move = original_topic_posts_count == posts.length
|
|
|
|
@first_post_number_moved =
|
|
posts.first.is_first_post? ? posts[1]&.post_number : posts.first.post_number
|
|
|
|
if @options[:freeze_original]
|
|
# in this case we need to add the moderator post after the last copied post
|
|
if @full_move
|
|
@first_post_number_moved = @original_topic.ordered_posts.last.post_number + 1
|
|
else
|
|
from_posts = @original_topic.ordered_posts.where("post_number > ?", posts.last.post_number)
|
|
shift_post_numbers(from_posts)
|
|
@first_post_number_moved = posts.last.post_number + 1
|
|
end
|
|
end
|
|
|
|
move_each_post
|
|
handle_moved_references
|
|
|
|
create_moderator_post_in_original_topic
|
|
update_statistics
|
|
update_user_actions
|
|
update_last_post_stats
|
|
update_upload_security_status
|
|
update_bookmarks
|
|
|
|
close_topic_and_schedule_deletion if @full_move
|
|
|
|
destination_topic.reload
|
|
DiscourseEvent.trigger(
|
|
:posts_moved,
|
|
destination_topic_id: destination_topic.id,
|
|
original_topic_id: original_topic.id,
|
|
)
|
|
destination_topic
|
|
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
|
|
@move_map = {}
|
|
@reply_count = {}
|
|
posts.each_with_index do |post, offset|
|
|
@move_map[post.post_number] = offset + max_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
|
|
|
|
posts.each do |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)
|
|
|
|
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
|
|
end
|
|
|
|
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)
|
|
@post_creator =
|
|
PostCreator.new(
|
|
post.user,
|
|
raw: post.raw,
|
|
topic_id: destination_topic.id,
|
|
acting_user: user,
|
|
cook_method: post.cook_method,
|
|
via_email: post.via_email,
|
|
raw_email: post.raw_email,
|
|
skip_validations: true,
|
|
created_at: post.created_at,
|
|
guardian: Guardian.new(user),
|
|
skip_jobs: true,
|
|
)
|
|
new_post = @post_creator.create!
|
|
|
|
move_email_logs(post, new_post)
|
|
|
|
PostAction.copy(post, new_post)
|
|
|
|
PostRevision.copy(post, new_post)
|
|
|
|
attrs_to_update = {
|
|
reply_count: @reply_count[1] || 0,
|
|
version: post.version,
|
|
public_version: post.public_version,
|
|
}
|
|
|
|
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
|
|
|
|
# When freezing original, ensure the notification generated points
|
|
# to the newly created post, not the old OP
|
|
if @options[:freeze_original]
|
|
@post_ids_after_move =
|
|
@post_ids_after_move.map { |post_id| post_id == post.id ? new_post.id : post_id }
|
|
end
|
|
|
|
DiscourseEvent.trigger(:first_post_moved, new_post, post)
|
|
DiscourseEvent.trigger(:post_moved, new_post, original_topic.id, post)
|
|
|
|
# we don't want to keep the old topic's OP bookmarked when we are
|
|
# moving it into a new topic
|
|
Bookmark.where(bookmarkable: post).update_all(bookmarkable_id: new_post.id)
|
|
|
|
new_post
|
|
end
|
|
|
|
def move(post)
|
|
update = {
|
|
reply_count: @reply_count[post.post_number] || 0,
|
|
post_number: @move_map[post.post_number],
|
|
reply_to_post_number: @move_map[post.reply_to_post_number],
|
|
topic_id: destination_topic.id,
|
|
sort_order: @move_map[post.post_number],
|
|
baked_version: nil,
|
|
}
|
|
|
|
update[:reply_to_user_id] = nil unless @move_map[post.reply_to_post_number]
|
|
|
|
moved_post =
|
|
if @options[:freeze_original]
|
|
post.dup
|
|
else
|
|
post
|
|
end
|
|
|
|
moved_post.attributes = update
|
|
moved_post.disable_rate_limits! if @options[:freeze_original]
|
|
moved_post.save(validate: false)
|
|
|
|
if moved_post.id != post.id
|
|
@post_ids_after_move =
|
|
@post_ids_after_move.map { |post_id| post_id == post.id ? moved_post.id : post_id }
|
|
end
|
|
|
|
DiscourseEvent.trigger(:post_moved, moved_post, original_topic.id, post)
|
|
|
|
# Move any links from the post to the new topic
|
|
moved_post.topic_links.update_all(topic_id: destination_topic.id)
|
|
|
|
moved_post
|
|
end
|
|
|
|
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,
|
|
post_user_id: post.user_id,
|
|
new_topic_id: destination_topic.id,
|
|
new_post_number: new_post_number,
|
|
new_topic_title: destination_topic.title,
|
|
}
|
|
end
|
|
|
|
def store_movement(metadata, new_post)
|
|
metadata[:new_post_id] = new_post.id
|
|
metadata[:now] = Time.zone.now
|
|
metadata[:created_new_topic] = @creating_new_topic
|
|
metadata[:old_topic_title] = @original_topic_title
|
|
metadata[:user_id] = @user.id
|
|
metadata[:full_move] = @full_move
|
|
|
|
DB.exec(<<~SQL, metadata)
|
|
INSERT INTO moved_posts(old_topic_id, old_topic_title, old_post_id, old_post_number, post_user_id, user_id, full_move, new_topic_id, new_topic_title, new_post_id, new_post_number, created_new_topic, created_at, updated_at)
|
|
VALUES (:old_topic_id, :old_topic_title, :old_post_id, :old_post_number, :post_user_id, :user_id, :full_move, :new_topic_id, :new_topic_title, :new_post_id, :new_post_number, :created_new_topic, :now, :now)
|
|
SQL
|
|
end
|
|
|
|
def shift_post_numbers(from_posts)
|
|
from_posts.reverse_each { |post| post.update_columns(post_number: post.post_number + 1) }
|
|
end
|
|
|
|
def move_incoming_emails
|
|
DB.exec <<~SQL
|
|
UPDATE incoming_emails ie
|
|
SET topic_id = mp.new_topic_id,
|
|
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
|
|
|
|
def move_email_logs(old_post, new_post)
|
|
EmailLog.where(post_id: old_post.id).update_all(post_id: new_post.id)
|
|
end
|
|
|
|
def move_notifications
|
|
DB.exec <<~SQL
|
|
UPDATE notifications n
|
|
SET topic_id = mp.new_topic_id,
|
|
post_number = mp.new_post_number,
|
|
data = (data :: JSONB ||
|
|
jsonb_strip_nulls(
|
|
jsonb_build_object(
|
|
'topic_title', CASE WHEN data :: JSONB ->> 'topic_title' IS NULL
|
|
THEN NULL
|
|
ELSE mp.new_topic_title END
|
|
)
|
|
)) :: JSON
|
|
FROM moved_posts mp
|
|
WHERE n.topic_id = mp.old_topic_id AND n.post_number = mp.old_post_number
|
|
AND n.notification_type <> #{Notification.types[:watching_first_post]}
|
|
SQL
|
|
end
|
|
|
|
def update_reply_counts
|
|
DB.exec <<~SQL
|
|
UPDATE posts p
|
|
SET reply_count = GREATEST(0, reply_count - x.moved_reply_count)
|
|
FROM (
|
|
SELECT r.post_id, mp.new_topic_id, COUNT(1) AS moved_reply_count
|
|
FROM moved_posts mp
|
|
JOIN post_replies r ON (mp.old_post_id = r.reply_post_id)
|
|
GROUP BY r.post_id, mp.new_topic_id
|
|
) x
|
|
WHERE x.post_id = p.id AND x.new_topic_id <> p.topic_id
|
|
SQL
|
|
end
|
|
|
|
def update_quotes
|
|
DB.exec <<~SQL
|
|
UPDATE posts p
|
|
SET raw = REPLACE(p.raw,
|
|
', post:' || mp.old_post_number || ', topic:' || mp.old_topic_id,
|
|
', post:' || mp.new_post_number || ', topic:' || mp.new_topic_id),
|
|
baked_version = NULL
|
|
FROM moved_posts mp, quoted_posts qp
|
|
WHERE p.id = qp.post_id AND mp.old_post_id = qp.quoted_post_id
|
|
SQL
|
|
end
|
|
|
|
def move_first_post_replies
|
|
DB.exec <<~SQL
|
|
UPDATE post_replies pr
|
|
SET post_id = mp.new_post_id
|
|
FROM moved_posts mp
|
|
WHERE mp.old_post_id <> mp.new_post_id AND pr.post_id = mp.old_post_id AND
|
|
EXISTS (SELECT 1 FROM moved_posts mr WHERE mr.new_post_id = pr.reply_post_id)
|
|
SQL
|
|
end
|
|
|
|
def delete_post_replies
|
|
DB.exec <<~SQL
|
|
DELETE FROM post_replies pr USING moved_posts mp
|
|
WHERE (SELECT topic_id FROM posts WHERE id = pr.post_id) <>
|
|
(SELECT topic_id FROM posts WHERE id = pr.reply_post_id)
|
|
AND (pr.reply_post_id = mp.old_post_id OR pr.post_id = mp.old_post_id)
|
|
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 DISTINCT ON (topic_id, post_number, user_id) topic_id, user_id, post_number, msecs
|
|
FROM temp_post_timings
|
|
ORDER BY topic_id, post_number, user_id, msecs DESC
|
|
ON CONFLICT (topic_id, post_number, user_id) DO UPDATE
|
|
SET msecs = GREATEST(post_timings.msecs, excluded.msecs)
|
|
SQL
|
|
end
|
|
|
|
def copy_first_post_timings
|
|
DB.exec(<<~SQL, post_ids: @post_ids_after_move)
|
|
INSERT INTO post_timings (topic_id, user_id, post_number, msecs)
|
|
SELECT mp.new_topic_id, pt.user_id, mp.new_post_number, pt.msecs
|
|
FROM post_timings pt
|
|
JOIN moved_posts mp ON (pt.topic_id = mp.old_topic_id AND pt.post_number = mp.old_post_number)
|
|
WHERE mp.old_post_id <> mp.new_post_id
|
|
AND mp.old_post_id IN (:post_ids)
|
|
ON CONFLICT (topic_id, post_number, user_id) DO UPDATE
|
|
SET msecs = GREATEST(post_timings.msecs, excluded.msecs)
|
|
SQL
|
|
end
|
|
|
|
def delete_invalid_post_timings
|
|
DB.exec <<~SQL
|
|
DELETE
|
|
FROM post_timings pt
|
|
USING moved_posts mp
|
|
WHERE pt.topic_id = mp.new_topic_id
|
|
AND pt.post_number = mp.new_post_number
|
|
SQL
|
|
end
|
|
|
|
def move_post_timings
|
|
DB.exec(<<~SQL, post_ids: @post_ids_after_move)
|
|
UPDATE post_timings pt
|
|
SET topic_id = mp.new_topic_id,
|
|
post_number = mp.new_post_number
|
|
FROM moved_posts mp
|
|
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
|
|
AND mp.new_post_id IN (:post_ids)
|
|
SQL
|
|
end
|
|
|
|
def copy_topic_users
|
|
params = {
|
|
old_topic_id: original_topic.id,
|
|
new_topic_id: destination_topic.id,
|
|
old_highest_post_number: destination_topic.highest_post_number,
|
|
old_highest_staff_post_number: destination_topic.highest_staff_post_number,
|
|
}
|
|
|
|
DB.exec(<<~SQL, params)
|
|
INSERT INTO topic_users(user_id, topic_id, posted, last_read_post_number,
|
|
last_emailed_post_number, first_visited_at, last_visited_at, notification_level,
|
|
notifications_changed_at, notifications_reason_id)
|
|
SELECT tu.user_id,
|
|
:new_topic_id AS topic_id,
|
|
EXISTS(
|
|
SELECT 1
|
|
FROM posts p
|
|
WHERE p.topic_id = :new_topic_id
|
|
AND p.user_id = tu.user_id
|
|
LIMIT 1
|
|
) AS posted,
|
|
(
|
|
SELECT MAX(lr.new_post_number)
|
|
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,
|
|
tu.notification_level,
|
|
tu.notifications_changed_at,
|
|
tu.notifications_reason_id
|
|
FROM topic_users tu
|
|
JOIN topics t ON (t.id = :new_topic_id)
|
|
WHERE tu.topic_id = :old_topic_id
|
|
AND GREATEST(
|
|
tu.last_read_post_number,
|
|
tu.last_emailed_post_number
|
|
) >= (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
|
|
WHEN topic_users.last_read_post_number = :old_highest_staff_post_number OR (
|
|
:old_highest_post_number < :old_highest_staff_post_number
|
|
AND topic_users.last_read_post_number = :old_highest_post_number
|
|
AND NOT EXISTS(SELECT 1
|
|
FROM users u
|
|
WHERE u.id = topic_users.user_id
|
|
AND (admin OR moderator))
|
|
) THEN
|
|
GREATEST(topic_users.last_read_post_number,
|
|
excluded.last_read_post_number)
|
|
ELSE topic_users.last_read_post_number END,
|
|
last_emailed_post_number = CASE
|
|
WHEN topic_users.last_emailed_post_number = :old_highest_staff_post_number OR (
|
|
:old_highest_post_number < :old_highest_staff_post_number
|
|
AND topic_users.last_emailed_post_number = :old_highest_post_number
|
|
AND NOT EXISTS(SELECT 1
|
|
FROM users u
|
|
WHERE u.id = topic_users.user_id
|
|
AND (admin OR moderator))
|
|
) THEN
|
|
GREATEST(topic_users.last_emailed_post_number,
|
|
excluded.last_emailed_post_number)
|
|
ELSE topic_users.last_emailed_post_number END,
|
|
first_visited_at = LEAST(topic_users.first_visited_at, excluded.first_visited_at),
|
|
last_visited_at = GREATEST(topic_users.last_visited_at, excluded.last_visited_at)
|
|
SQL
|
|
end
|
|
|
|
def update_statistics
|
|
destination_topic.update_statistics
|
|
original_topic.update_statistics
|
|
TopicUser.update_post_action_cache(
|
|
topic_id: [original_topic.id, destination_topic.id],
|
|
post_id: @post_ids,
|
|
)
|
|
end
|
|
|
|
def update_user_actions
|
|
UserAction.synchronize_target_topic_ids(posts.map(&:id))
|
|
end
|
|
|
|
def create_moderator_post_in_original_topic
|
|
move_type_str = PostMover.move_types[@move_type].to_s
|
|
move_type_str.sub!("topic", "message") if @move_to_pm
|
|
|
|
topic_link =
|
|
if posts.first.is_first_post?
|
|
"[#{destination_topic.title}](#{destination_topic.relative_url})"
|
|
else
|
|
"[#{destination_topic.title}](#{posts.first.relative_url})"
|
|
end
|
|
|
|
post_type = @move_to_pm ? Post.types[:whisper] : Post.types[:small_action]
|
|
|
|
continue =
|
|
DiscoursePluginRegistry.apply_modifier(
|
|
:post_mover_create_moderator_post,
|
|
true,
|
|
user: user,
|
|
post_type: post_type,
|
|
post_ids: @post_ids_after_move,
|
|
full_move: @full_move,
|
|
original_topic: original_topic,
|
|
destination_topic: destination_topic,
|
|
first_post_number_moved: @first_post_number_moved,
|
|
)
|
|
return if !continue
|
|
|
|
message =
|
|
I18n.with_locale(SiteSetting.default_locale) do
|
|
I18n.t(
|
|
"move_posts.#{move_type_str}_moderator_post",
|
|
count: posts.length,
|
|
topic_link: topic_link,
|
|
)
|
|
end
|
|
|
|
original_topic.add_moderator_post(
|
|
user,
|
|
message,
|
|
post_type: post_type,
|
|
action_code: "split_topic",
|
|
post_number: @first_post_number_moved,
|
|
)
|
|
end
|
|
|
|
def posts
|
|
@posts ||=
|
|
begin
|
|
Post
|
|
.where(topic: @original_topic, id: post_ids)
|
|
.where.not(post_type: Post.types[:small_action])
|
|
.where.not(raw: "")
|
|
.order(:created_at)
|
|
.tap { |posts| raise Discourse::InvalidParameters.new(:post_ids) if posts.empty? }
|
|
end
|
|
end
|
|
|
|
def update_last_post_stats
|
|
post = destination_topic.ordered_posts.where.not(post_type: Post.types[:whisper]).last
|
|
if post && post_ids.include?(post.id)
|
|
attrs = {}
|
|
attrs[:last_posted_at] = post.created_at
|
|
attrs[:last_post_user_id] = post.user_id
|
|
attrs[:bumped_at] = Time.now
|
|
attrs[:updated_at] = Time.now
|
|
destination_topic.update_columns(attrs)
|
|
end
|
|
end
|
|
|
|
def update_upload_security_status
|
|
DB.after_commit { Jobs.enqueue(:update_topic_upload_security, topic_id: @destination_topic.id) }
|
|
end
|
|
|
|
def update_bookmarks
|
|
DB.after_commit do
|
|
Jobs.enqueue(:sync_topic_user_bookmarked, topic_id: @original_topic.id)
|
|
Jobs.enqueue(:sync_topic_user_bookmarked, topic_id: @destination_topic.id)
|
|
end
|
|
end
|
|
|
|
def watch_new_topic
|
|
if @destination_topic.archetype == Archetype.private_message
|
|
if @original_topic.archetype == Archetype.private_message
|
|
notification_levels =
|
|
TopicUser
|
|
.where(topic_id: @original_topic.id, user_id: posts.pluck(:user_id))
|
|
.pluck(:user_id, :notification_level)
|
|
.to_h
|
|
else
|
|
notification_levels =
|
|
posts
|
|
.pluck(:user_id)
|
|
.uniq
|
|
.map { |user_id| [user_id, TopicUser.notification_levels[:watching]] }
|
|
.to_h
|
|
end
|
|
else
|
|
notification_levels = [[@destination_topic.user_id, TopicUser.notification_levels[:watching]]]
|
|
end
|
|
|
|
notification_levels.each do |user_id, notification_level|
|
|
TopicUser.change(
|
|
user_id,
|
|
@destination_topic.id,
|
|
notification_level: notification_level,
|
|
notifications_reason_id:
|
|
TopicUser.notification_reasons[
|
|
destination_topic.user_id == user_id ? :created_topic : :created_post
|
|
],
|
|
)
|
|
end
|
|
end
|
|
|
|
def add_allowed_users(usernames)
|
|
return if usernames.blank?
|
|
|
|
names = usernames.split(",").flatten
|
|
User
|
|
.where(username: names)
|
|
.find_each do |user|
|
|
unless destination_topic.topic_allowed_users.where(user_id: user.id).exists?
|
|
destination_topic.topic_allowed_users.build(user_id: user.id)
|
|
end
|
|
end
|
|
destination_topic.save!
|
|
end
|
|
|
|
def enqueue_jobs(topic)
|
|
@post_creator.enqueue_jobs if @post_creator
|
|
|
|
Jobs.enqueue(:notify_moved_posts, post_ids: @post_ids_after_move, moved_by_id: user.id)
|
|
|
|
Jobs.enqueue(:delete_inaccessible_notifications, topic_id: topic.id)
|
|
end
|
|
|
|
def close_topic_and_schedule_deletion
|
|
@original_topic.update_status("closed", true, @user)
|
|
return if @options[:freeze_original] # we only close the topic when freezing it
|
|
|
|
days_to_deleting = SiteSetting.delete_merged_stub_topics_after_days
|
|
if days_to_deleting == 0
|
|
is_allowed_to_delete_after_merge =
|
|
DiscoursePluginRegistry.apply_modifier(
|
|
:is_allowed_to_delete_after_merge,
|
|
Guardian.new(@user).can_delete?(@original_topic),
|
|
@original_topic,
|
|
@user,
|
|
)
|
|
if is_allowed_to_delete_after_merge
|
|
first_post = @original_topic.ordered_posts.first
|
|
|
|
PostDestroyer.new(
|
|
@user,
|
|
first_post,
|
|
context: I18n.t("topic_statuses.auto_deleted_by_merge"),
|
|
).destroy
|
|
|
|
@original_topic.trash!(Discourse.system_user)
|
|
end
|
|
elsif days_to_deleting > 0
|
|
@original_topic.set_or_create_timer(
|
|
TopicTimer.types[:delete],
|
|
days_to_deleting * 24,
|
|
by_user: @user,
|
|
)
|
|
end
|
|
end
|
|
end
|