diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 482242e3b46..55820ed18d0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -177,6 +177,8 @@ en: removed_user: "Removed %{who} %{when}" removed_group: "Removed %{who} %{when}" autobumped: "Automatically bumped %{when}" + tags_changed: "Tags updated %{when}" + category_changed: "Category updated %{when}" autoclosed: enabled: "Closed %{when}" disabled: "Opened %{when}" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 8a8a48a0539..ad01fea739c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -60,6 +60,13 @@ en: remove_posts_deleted_by_author: "Deleted by author" redirect_warning: "We were unable to verify that the link you selected was actually posted to the forum. If you wish to proceed anyway, select the link below." on_another_topic: "On another topic" + + topic_category_changed: "From %{from} to %{to}" + topic_tag_changed: + added_and_removed: "Added %{added} and removed %{removed}" + added: "Added %{added}" + removed: "Removed %{removed}" + inline_oneboxer: topic_page_title_post_number: "#%{post_number}" topic_page_title_post_number_by_user: "#%{post_number} by %{username}" @@ -2609,6 +2616,7 @@ en: suppress_overlapping_tags_in_list: "If tags match exact words in topic titles, don't show the tag" remove_muted_tags_from_latest: "Don't show topics tagged only with muted tags in the latest topic list." force_lowercase_tags: "Force all new tags to be entirely lowercase." + create_post_for_category_and_tag_changes: "Create a small action post when a topic's category or tags change" automatically_clean_unused_tags: "Automatically delete tags that are not being used on any topics or private messages on a daily basis." watched_precedence_over_muted: "Notify me about topics in categories or tags I’m watching that also belong to one I have muted" diff --git a/config/site_settings.yml b/config/site_settings.yml index 0df63c8e487..cd238ac1ada 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -3231,6 +3231,8 @@ tags: force_lowercase_tags: default: true client: true + create_post_for_category_and_tag_changes: + default: false automatically_clean_unused_tags: default: false diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb index a438dbbf248..fc18c6e4ed6 100644 --- a/lib/post_revisor.rb +++ b/lib/post_revisor.rb @@ -79,14 +79,15 @@ class PostRevisor track_and_revise topic_changes, :archetype, attribute end - track_topic_field(:category_id) do |tc, category_id, fields| + track_topic_field(:category_id) do |tc, new_category_id, fields| current_category = tc.topic.category - new_category = (category_id.nil? || category_id.zero?) ? nil : Category.find(category_id) + new_category = + (new_category_id.nil? || new_category_id.zero?) ? nil : Category.find(new_category_id) if new_category.nil? && tc.topic.private_message? tc.record_change("category_id", current_category.id, nil) tc.topic.category_id = nil - elsif new_category.nil? || tc.guardian.can_move_topic_to_category?(category_id) + elsif new_category.nil? || tc.guardian.can_move_topic_to_category?(new_category_id) tags = fields[:tags] || tc.topic.tags.map(&:name) if new_category && !DiscourseTagging.validate_category_tags(tc.guardian, tc.topic, new_category, tags) @@ -94,8 +95,14 @@ class PostRevisor next end - tc.record_change("category_id", tc.topic.category_id, category_id) - tc.check_result(tc.topic.change_category_to_id(category_id)) + tc.record_change("category_id", current_category&.id, new_category&.id) + tc.check_result(tc.topic.change_category_to_id(new_category_id)) + create_small_action_for_category_change( + topic: tc.topic, + user: tc.user, + old_category: current_category, + new_category: new_category, + ) end end @@ -112,14 +119,25 @@ class PostRevisor DB.after_commit do post = tc.topic.ordered_posts.first notified_user_ids = [post.user_id, post.last_editor_id].uniq + + added_tags = tags - prev_tags + removed_tags = prev_tags - tags + if !SiteSetting.disable_tags_edit_notifications Jobs.enqueue( :notify_tag_change, post_id: post.id, notified_user_ids: notified_user_ids, - diff_tags: ((tags - prev_tags) | (prev_tags - tags)), + diff_tags: (added_tags | removed_tags), ) end + + create_small_action_for_tag_changes( + topic: tc.topic, + user: tc.user, + added_tags: added_tags, + removed_tags: removed_tags, + ) end end end @@ -135,6 +153,56 @@ class PostRevisor end end + def self.create_small_action_for_category_change(topic:, user:, old_category:, new_category:) + if !old_category || !new_category || !SiteSetting.create_post_for_category_and_tag_changes + return + end + + topic.add_moderator_post( + user, + I18n.t( + "topic_category_changed", + from: "##{old_category.slug_ref}", + to: "##{new_category.slug_ref}", + ), + post_type: Post.types[:small_action], + action_code: "category_changed", + ) + end + + def self.create_small_action_for_tag_changes(topic:, user:, added_tags:, removed_tags:) + return if !SiteSetting.create_post_for_category_and_tag_changes + + topic.add_moderator_post( + user, + tags_changed_raw(added: added_tags, removed: removed_tags), + post_type: Post.types[:small_action], + action_code: "tags_changed", + custom_fields: { + tags_added: added_tags, + tags_removed: removed_tags, + }, + ) + end + + def self.tags_changed_raw(added:, removed:) + if removed.present? && added.present? + I18n.t( + "topic_tag_changed.added_and_removed", + added: tag_list_to_raw(added), + removed: tag_list_to_raw(removed), + ) + elsif added.present? + I18n.t("topic_tag_changed.added", added: tag_list_to_raw(added)) + elsif removed.present? + I18n.t("topic_tag_changed.removed", removed: tag_list_to_raw(removed)) + end + end + + def self.tag_list_to_raw(tag_list) + tag_list.sort.map { |tag_name| "##{tag_name}" }.join(", ") + end + # AVAILABLE OPTIONS: # - revised_at: changes the date of the revision # - force_new_version: bypass grace period edit window diff --git a/spec/lib/post_revisor_spec.rb b/spec/lib/post_revisor_spec.rb index 5b295c75ad6..7c669e52a66 100644 --- a/spec/lib/post_revisor_spec.rb +++ b/spec/lib/post_revisor_spec.rb @@ -222,6 +222,87 @@ RSpec.describe PostRevisor do expect { post_revisor.revise!(admin, tags: ["new-tag"]) }.not_to change { Notification.count } end + + it "doesn't create a small_action post when create_post_for_category_and_tag_changes is false" do + SiteSetting.create_post_for_category_and_tag_changes = false + + expect { post_revisor.revise!(admin, tags: ["new-tag"]) }.not_to change { Post.count } + end + + describe "when `create_post_for_category_and_tag_changes` site setting is enabled" do + fab!(:tag1) { Fabricate(:tag, name: "First tag") } + fab!(:tag2) { Fabricate(:tag, name: "Second tag") } + + before { SiteSetting.create_post_for_category_and_tag_changes = true } + + it "Creates a small_action post with correct translation when both adding and removing tags" do + post.topic.update!(tags: [tag1]) + + expect { post_revisor.revise!(admin, tags: [tag2.name]) }.to change { + Post.where(topic_id: post.topic_id, action_code: "tags_changed").count + }.by(1) + + expect(post.topic.ordered_posts.last.raw).to eq( + I18n.t( + "topic_tag_changed.added_and_removed", + added: "##{tag2.name}", + removed: "##{tag1.name}", + ), + ) + end + + it "Creates a small_action post with correct translation when adding tags" do + post.topic.update!(tags: []) + + expect { post_revisor.revise!(admin, tags: [tag1.name]) }.to change { + Post.where(topic_id: post.topic_id, action_code: "tags_changed").count + }.by(1) + + expect(post.topic.ordered_posts.last.raw).to eq( + I18n.t("topic_tag_changed.added", added: "##{tag1.name}"), + ) + end + + it "Creates a small_action post with correct translation when removing tags" do + post.topic.update!(tags: [tag1, tag2]) + + expect { post_revisor.revise!(admin, tags: []) }.to change { + Post.where(topic_id: post.topic_id, action_code: "tags_changed").count + }.by(1) + + expect(post.topic.ordered_posts.last.raw).to eq( + I18n.t("topic_tag_changed.removed", removed: "##{tag1.name}, ##{tag2.name}"), + ) + end + + it "Creates a small_action post when category is changed" do + current_category = post.topic.category + category = Fabricate(:category) + + expect { post_revisor.revise!(admin, category_id: category.id) }.to change { + Post.where(topic_id: post.topic_id, action_code: "category_changed").count + }.by(1) + + expect(post.topic.ordered_posts.last.raw).to eq( + I18n.t( + "topic_category_changed", + to: "##{category.slug}", + from: "##{current_category.slug}", + ), + ) + end + + describe "with PMs" do + fab!(:pm) { Fabricate(:private_message_topic) } + let(:first_post) { create_post(user: admin, topic: pm, allow_uncategorized_topics: false) } + fab!(:category) { Fabricate(:category, topic_count: 1) } + it "Does not create a category change small_action post when converting to a topic" do + expect do + TopicConverter.new(first_post.topic, admin).convert_to_public_topic(category.id) + end.to change { category.reload.topic_count }.by(1) + end + end + end end describe "revise wiki" do