diff --git a/app/jobs/regular/create_linked_topic.rb b/app/jobs/regular/create_linked_topic.rb new file mode 100644 index 00000000000..e88ead16abd --- /dev/null +++ b/app/jobs/regular/create_linked_topic.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Jobs + class CreateLinkedTopic < ::Jobs::Base + + def execute(args) + reference_post = Post.find_by(id: args[:post_id]) + return unless reference_post.present? + parent_topic = reference_post.topic + return unless parent_topic.present? + parent_topic_id = parent_topic.id + parent_title = parent_topic.title + + ActiveRecord::Base.transaction do + linked_topic_record = parent_topic.linked_topic + if linked_topic_record.present? + raw_title = parent_title.delete_suffix(I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: "", count: linked_topic_record.sequence)) + original_topic_id = linked_topic_record.original_topic_id + sequence = linked_topic_record.sequence + 1 + else + raw_title = parent_title + + # update parent topic title to append title_suffix_locale + parent_title = I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: parent_title, count: 1) + parent_topic.title = parent_title + parent_topic.save! + + # create linked topic record + original_topic_id = parent_topic_id + LinkedTopic.create!(topic_id: parent_topic_id, original_topic_id: original_topic_id, sequence: 1) + sequence = 2 + end + + # fetch previous topic titles + previous_topics = "" + linked_topic_ids = LinkedTopic.where(original_topic_id: original_topic_id).pluck(:topic_id) + Topic.where(id: linked_topic_ids).order(:id).each do |topic| + previous_topics += "- [#{topic.title}](#{topic.url})\n" + end + + # create new topic + new_topic_title = I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: raw_title, count: sequence) + new_topic_raw = I18n.t('create_linked_topic.post_raw', parent_title: "[#{parent_title}](#{reference_post.full_url})", previous_topics: previous_topics) + system_user = Discourse.system_user + new_post = PostCreator.create!( + system_user, + title: new_topic_title, + raw: new_topic_raw, + skip_validations: true) + new_topic = new_post.topic + new_topic_id = new_topic.id + + # create linked_topic record + LinkedTopic.create!(topic_id: new_topic_id, original_topic_id: original_topic_id, sequence: sequence) + + # copy over topic tracking state from old topic + params = { + old_topic_id: parent_topic_id, + new_topic_id: new_topic_id + } + DB.exec(<<~SQL, params) + INSERT INTO topic_users(user_id, topic_id, notification_level, + notifications_reason_id) + SELECT tu.user_id, + :new_topic_id AS topic_id, + tu.notification_level, + 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 tu.notification_level != 1 + ON CONFLICT (topic_id, user_id) DO NOTHING + SQL + + # add moderator post to old topic + parent_topic.add_moderator_post(system_user, I18n.t('create_linked_topic.moderator_post_raw', new_title: "[#{new_topic_title}](#{new_topic.url})")) + end + end + end +end diff --git a/app/models/linked_topic.rb b/app/models/linked_topic.rb new file mode 100644 index 00000000000..01934b07515 --- /dev/null +++ b/app/models/linked_topic.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class LinkedTopic < ActiveRecord::Base + belongs_to :topic +end + +# == Schema Information +# +# Table name: linked_topics +# +# id :bigint not null, primary key +# topic_id :bigint not null +# original_topic_id :bigint not null +# sequence :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_linked_topics_on_topic_id_and_original_topic_id (topic_id,original_topic_id) UNIQUE +# index_linked_topics_on_topic_id_and_sequence (topic_id,sequence) UNIQUE +# diff --git a/app/models/topic.rb b/app/models/topic.rb index cecd42df79b..4df894e2bc0 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -242,6 +242,7 @@ class Topic < ActiveRecord::Base has_one :first_post, -> { where post_number: 1 }, class_name: 'Post' has_one :topic_search_data has_one :topic_embed, dependent: :destroy + has_one :linked_topic, dependent: :destroy belongs_to :image_upload, class_name: 'Upload' has_many :topic_thumbnails, through: :image_upload diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4c2c6fe0a52..5133993cd7e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2140,6 +2140,7 @@ en: notify_about_queued_posts_after: "If there are posts that have been waiting to be reviewed for more than this many hours, send a notification to all moderators. Set to 0 to disable these notifications." auto_close_messages_post_count: "Maximum number of posts allowed in a message before it is automatically closed (0 to disable)" auto_close_topics_post_count: "Maximum number of posts allowed in a topic before it is automatically closed (0 to disable)" + auto_close_topics_create_linked_topic: "Create a new linked topic when a topic is auto-closed based on 'auto close topics post count' setting" code_formatting_style: "Code button in composer will default to this code formatting style" @@ -4936,3 +4937,10 @@ en: %{keys} No action is required at this time, however, it is considered good security practice to cycle all your important credentials every few years. + + create_linked_topic: + topic_title_with_sequence: + one: "%{topic_title} (Part %{count})" + other: "%{topic_title} (Part %{count})" + post_raw: "Continuing the discussion from %{parent_title}.\n\nPrevious discussions:\n\n%{previous_topics}" + moderator_post_raw: "Continue discussion here: %{new_title}" diff --git a/config/site_settings.yml b/config/site_settings.yml index b0ad977432d..78aa9d6832c 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -930,6 +930,8 @@ posting: default: 500 auto_close_topics_post_count: default: 10000 + auto_close_topics_create_linked_topic: + default: true code_formatting_style: client: true type: enum diff --git a/db/migrate/20201027110546_create_linked_topics.rb b/db/migrate/20201027110546_create_linked_topics.rb new file mode 100644 index 00000000000..0f3c8497d62 --- /dev/null +++ b/db/migrate/20201027110546_create_linked_topics.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateLinkedTopics < ActiveRecord::Migration[6.0] + def change + create_table :linked_topics do |t| + t.bigint :topic_id, null: false + t.bigint :original_topic_id, null: false + t.integer :sequence, null: false + + t.timestamps + end + + add_index :linked_topics, [:topic_id, :original_topic_id], unique: true + add_index :linked_topics, [:topic_id, :sequence], unique: true + end +end diff --git a/lib/post_creator.rb b/lib/post_creator.rb index ad7f0e74b25..0921f46ce7f 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -381,6 +381,11 @@ class PostCreator locale: SiteSetting.default_locale ) ) + + if SiteSetting.auto_close_topics_create_linked_topic? + # enqueue a job to create a linked topic + Jobs.enqueue_in(5.seconds, :create_linked_topic, post_id: @post.id) + end end end diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index c59d92c7273..685d4b2ee45 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -411,6 +411,29 @@ describe PostCreator do locale: :en )) end + + describe "auto_close_topics_create_linked_topic is enabled" do + before do + SiteSetting.auto_close_topics_create_linked_topic = true + end + + it "enqueues a job to create a new linked topic" do + freeze_time + post + + post_2 = PostCreator.new( + topic.user, + topic_id: topic.id, + raw: "this is a second post" + ).create + + topic.reload + + expect(topic.closed).to eq(true) + expect(topic_timer.reload.deleted_at).to eq_time(Time.zone.now) + expect(job_enqueued?(job: :create_linked_topic, args: { post_id: post_2.id })).to eq(true) + end + end end end end diff --git a/spec/jobs/create_linked_topic_spec.rb b/spec/jobs/create_linked_topic_spec.rb new file mode 100644 index 00000000000..c8471717397 --- /dev/null +++ b/spec/jobs/create_linked_topic_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'jobs/regular/create_linked_topic' + +describe Jobs::CreateLinkedTopic do + + it "returns when the post cannot be found" do + expect { Jobs::CreateLinkedTopic.new.perform(post_id: 1, sync_exec: true) }.not_to raise_error + end + + context 'with a post' do + + fab!(:topic) { Fabricate(:topic) } + fab!(:post) do + Fabricate(:post, topic: topic) + end + fab!(:user_1) { Fabricate(:user) } + fab!(:user_2) { Fabricate(:user) } + + let :watching do + TopicUser.notification_levels[:watching] + end + + let :tracking do + TopicUser.notification_levels[:tracking] + end + + let :muted do + TopicUser.notification_levels[:muted] + end + + before do + SiteSetting.auto_close_topics_create_linked_topic = true + Fabricate(:topic_user, notification_level: tracking, topic: topic, user: user_1) + Fabricate(:topic_user, notification_level: muted, topic: topic, user: user_2) + end + + it 'creates a linked topic' do + Jobs::CreateLinkedTopic.new.execute(post_id: post.id) + + raw_title = topic.title + topic.reload + new_topic = Topic.last + linked_topic = new_topic.linked_topic + expect(topic.title).to include(I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: raw_title, count: 1)) + expect(topic.posts.last.raw).to eq(I18n.t('create_linked_topic.moderator_post_raw', new_title: "[#{new_topic.title}](#{new_topic.url})")) + expect(new_topic.title).to include(I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: raw_title, count: 2)) + expect(new_topic.first_post.raw).to include(topic.url) + expect(new_topic.topic_users.count).to eq(3) + expect(new_topic.topic_users.pluck(:notification_level)).to contain_exactly(muted, tracking, watching) + expect(linked_topic.topic_id).to eq(new_topic.id) + expect(linked_topic.original_topic_id).to eq(topic.id) + expect(linked_topic.sequence).to eq(2) + end + end + +end