mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
Related: https://meta.discourse.org/t/manual-localization-doesnt-seem-to-be-working/407319 Users have gotten tripped over and over again when manually inserting translations for posts, not knowing they need to set the post language first before the translation is shown (explained [here](https://meta.discourse.org/t/manual-localization-doesnt-seem-to-be-working/407319/7?u=nat)). This PR adds the ability to allow the user to set the post language in the modal when they are working with translations, highlighting the fact that they need to set the post language before translations can be shown
2400 lines
81 KiB
Ruby
Vendored
2400 lines
81 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
describe PostRevisor do
|
|
fab!(:topic)
|
|
fab!(:newuser) { Fabricate(:newuser, last_seen_at: Date.today) }
|
|
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
|
fab!(:coding_horror)
|
|
fab!(:admin) { Fabricate(:admin, refresh_auto_groups: true) }
|
|
fab!(:moderator)
|
|
let(:post_args) { { user: newuser, topic: topic } }
|
|
|
|
describe "TopicChanges" do
|
|
let(:tc) do
|
|
topic.reload
|
|
PostRevisor::TopicChanges.new(topic, topic.user)
|
|
end
|
|
|
|
it "provides a guardian" do
|
|
expect(tc.guardian).to be_an_instance_of Guardian
|
|
end
|
|
|
|
it "tracks changes properly" do
|
|
expect(tc.diff).to eq({})
|
|
|
|
# it remembers changes we tell it to
|
|
tc.record_change("height", "180cm", "170cm")
|
|
expect(tc.diff["height"]).to eq(%w[180cm 170cm])
|
|
|
|
# it works with arrays of values
|
|
tc.record_change("colors", nil, %w[red blue])
|
|
expect(tc.diff["colors"]).to eq([nil, %w[red blue]])
|
|
|
|
# it does not record changes to the same val
|
|
tc.record_change("wat", "js", "js")
|
|
expect(tc.diff["wat"]).to be_nil
|
|
|
|
tc.record_change("tags", %w[a b], %w[a b])
|
|
expect(tc.diff["tags"]).to be_nil
|
|
end
|
|
end
|
|
|
|
describe "editing category" do
|
|
it "triggers the :post_edited event with topic_changed?" do
|
|
category = Fabricate(:category)
|
|
category.set_permissions(everyone: :full)
|
|
category.save!
|
|
post = create_post
|
|
events = DiscourseEvent.track_events { post.revise(post.user, category_id: category.id) }
|
|
|
|
event = events.find { |e| e[:event_name] == :post_edited }
|
|
|
|
expect(event[:params].first).to eq(post)
|
|
expect(event[:params].second).to eq(true)
|
|
expect(event[:params].third).to be_kind_of(PostRevisor)
|
|
expect(event[:params].third.topic_diff).to eq(
|
|
{ "category_id" => [SiteSetting.uncategorized_category_id, category.id] },
|
|
)
|
|
end
|
|
|
|
it "does not revise category when no permission to create a topic in category" do
|
|
category = Fabricate(:category)
|
|
category.set_permissions(staff: :full)
|
|
category.save!
|
|
|
|
post = create_post
|
|
old_id = post.topic.category_id
|
|
|
|
post.revise(post.user, category_id: category.id)
|
|
|
|
post.reload
|
|
expect(post.topic.category_id).to eq(old_id)
|
|
|
|
category.set_permissions(everyone: :full)
|
|
category.save!
|
|
|
|
post.revise(post.user, category_id: category.id)
|
|
|
|
post.reload
|
|
expect(post.topic.category_id).to eq(category.id)
|
|
end
|
|
|
|
it "does not revise category when the destination category requires topic approval" do
|
|
new_category = Fabricate(:category)
|
|
new_category.require_topic_approval = true
|
|
new_category.save!
|
|
|
|
post = create_post
|
|
old_category_id = post.topic.category_id
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(post.reload.topic.category_id).to eq(old_category_id)
|
|
|
|
new_category.require_topic_approval = false
|
|
new_category.save!
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(post.reload.topic.category_id).to eq(new_category.id)
|
|
end
|
|
|
|
it "does not revise category if incorrect amount of tags" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
new_category = Fabricate(:category, minimum_required_tags: 1)
|
|
|
|
post = create_post
|
|
old_category_id = post.topic.category_id
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(post.reload.topic.category_id).to eq(old_category_id)
|
|
|
|
tag = Fabricate(:tag)
|
|
topic_tag = Fabricate(:topic_tag, topic: post.topic, tag: tag)
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(post.reload.topic.category_id).to eq(new_category.id)
|
|
topic_tag.destroy
|
|
|
|
post.revise(post.user, category_id: new_category.id, tags: ["test_tag"])
|
|
expect(post.reload.topic.category_id).to eq(new_category.id)
|
|
end
|
|
|
|
it "allows category change with localized tags" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
tag = Fabricate(:tag)
|
|
tag_group = Fabricate(:tag_group, tags: [tag])
|
|
old_category = Fabricate(:category)
|
|
new_category = Fabricate(:category, tag_groups: [tag_group])
|
|
|
|
post = create_post(category: old_category)
|
|
|
|
post.revise(post.user, category_id: new_category.id, tags: [{ id: tag.id, name: tag.name }])
|
|
expect(post.reload.topic.category_id).to eq(new_category.id)
|
|
end
|
|
|
|
it "allows category change when clearing all tags with an empty array" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
tag = Fabricate(:tag)
|
|
old_category = Fabricate(:category)
|
|
new_category = Fabricate(:category)
|
|
|
|
post = create_post(category: old_category, tags: [tag.name])
|
|
expect(post.topic.tags).to contain_exactly(tag)
|
|
|
|
post.revise(post.user, category_id: new_category.id, tags: [])
|
|
expect(post.reload.topic.category_id).to eq(new_category.id)
|
|
expect(post.topic.tags).to be_empty
|
|
end
|
|
|
|
it "returns an error if the topic does not have minimum amount of tags that the new category requires" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
old_category = Fabricate(:category, minimum_required_tags: 0)
|
|
new_category = Fabricate(:category, minimum_required_tags: 1)
|
|
|
|
post = create_post(category: old_category)
|
|
topic = post.topic
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(topic.errors.full_messages).to eq([I18n.t("tags.minimum_required_tags", count: 1)])
|
|
end
|
|
|
|
it "returns an error if the topic has tags not allowed in the new category" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
tag1 = Fabricate(:tag)
|
|
tag2 = Fabricate(:tag)
|
|
tag_group = Fabricate(:tag_group, tags: [tag1])
|
|
tag_group2 = Fabricate(:tag_group, tags: [tag2])
|
|
|
|
old_category = Fabricate(:category, tag_groups: [tag_group])
|
|
new_category = Fabricate(:category, tag_groups: [tag_group2])
|
|
|
|
post = create_post(category: old_category, tags: [tag1.name])
|
|
topic = post.topic
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(topic.errors.full_messages).to eq(
|
|
[
|
|
I18n.t(
|
|
"tags.forbidden.restricted_tags_cannot_be_used_in_category",
|
|
count: 1,
|
|
tags: tag1.name,
|
|
category: new_category.name,
|
|
),
|
|
],
|
|
)
|
|
end
|
|
|
|
it "returns an error if the topic is missing tags required from a tag group in the new category" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
tag1 = Fabricate(:tag)
|
|
tag_group = Fabricate(:tag_group, tags: [tag1])
|
|
|
|
old_category = Fabricate(:category)
|
|
new_category =
|
|
Fabricate(
|
|
:category,
|
|
category_required_tag_groups: [
|
|
CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1),
|
|
],
|
|
)
|
|
|
|
post = create_post(category: old_category)
|
|
topic = post.topic
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(topic.errors.full_messages).to eq(
|
|
[
|
|
I18n.t(
|
|
"tags.required_tags_from_group",
|
|
count: 1,
|
|
tag_group_name: tag_group.name,
|
|
tags: tag1.name,
|
|
),
|
|
],
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "editing tags" do
|
|
subject(:post_revisor) { PostRevisor.new(post) }
|
|
|
|
fab!(:post)
|
|
|
|
before do
|
|
Jobs.run_immediately!
|
|
|
|
TopicUser.change(
|
|
newuser.id,
|
|
post.topic_id,
|
|
notification_level: TopicUser.notification_levels[:watching],
|
|
)
|
|
end
|
|
|
|
it "creates notifications" do
|
|
expect { post_revisor.revise!(admin, tags: ["new-tag"]) }.to change { Notification.count }.by(
|
|
1,
|
|
)
|
|
end
|
|
|
|
it "skips notifications if disable_tags_edit_notifications" do
|
|
SiteSetting.disable_tags_edit_notifications = true
|
|
|
|
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
|
|
|
|
it "edits a topic's tags" do
|
|
tag = Fabricate(:tag)
|
|
post_revisor.revise!(admin, tags: [{ id: tag.id, name: "outdated" }])
|
|
expect(post.topic.reload.tags).to contain_exactly(tag)
|
|
|
|
post_revisor.revise!(admin, tags: ["a-whole-new-tag"])
|
|
expect(post.topic.reload.tags).to match_array([have_attributes(name: "a-whole-new-tag")])
|
|
end
|
|
|
|
it "does not create an empty revision when only synonyms of existing tags are submitted" do
|
|
canonical = Fabricate(:tag, name: "apple-inc")
|
|
aapl = Fabricate(:tag, name: "aapl", target_tag: canonical)
|
|
appl = Fabricate(:tag, name: "appl", target_tag: canonical)
|
|
post.topic.tags << canonical
|
|
|
|
expect do
|
|
post_revisor.revise!(
|
|
admin,
|
|
tags: [
|
|
{ id: aapl.id, name: "aapl" },
|
|
{ id: appl.id, name: "appl" },
|
|
{ id: canonical.id, name: "apple-inc" },
|
|
],
|
|
)
|
|
end.not_to change { PostRevision.count }
|
|
expect(post.topic.reload.tags).to contain_exactly(canonical)
|
|
end
|
|
|
|
it "does not create an empty revision when synonym names are submitted as strings" do
|
|
canonical = Fabricate(:tag, name: "tesla-inc")
|
|
Fabricate(:tag, name: "tsla", target_tag: canonical)
|
|
post.topic.tags << canonical
|
|
|
|
expect do post_revisor.revise!(admin, tags: %w[tsla tesla-inc]) end.not_to change {
|
|
PostRevision.count
|
|
}
|
|
expect(post.topic.reload.tags).to contain_exactly(canonical)
|
|
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 do
|
|
SiteSetting.create_post_for_category_and_tag_changes = true
|
|
SiteSetting.whispers_allowed_groups = Group::AUTO_GROUPS[:staff]
|
|
end
|
|
|
|
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
|
|
|
|
it "Creates a small_action as a whisper when category is changed" do
|
|
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.post_type).to eq(Post.types[:whisper])
|
|
end
|
|
|
|
it "does not create a small_action or notification when a restricted tag is rejected" do
|
|
allowed_tag = Fabricate(:tag, name: "allowed-tag")
|
|
|
|
post.topic.update!(tags: [allowed_tag])
|
|
|
|
category = post.topic.category
|
|
category.update!(allow_global_tags: false)
|
|
category.tags = [allowed_tag]
|
|
category.save!
|
|
|
|
expect do
|
|
post_revisor.revise!(admin, tags: [allowed_tag.name, "disallowed-tag"])
|
|
end.not_to change { Post.where(topic_id: post.topic_id, action_code: "tags_changed").count }
|
|
|
|
expect(post.topic.reload.tags.pluck(:name)).to contain_exactly(allowed_tag.name)
|
|
expect(Jobs::NotifyTagChange.jobs.size).to eq(0)
|
|
end
|
|
|
|
describe "with PMs" do
|
|
fab!(:pm, :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 "editing locale" do
|
|
it "updates the post's locale" do
|
|
post = Fabricate(:post)
|
|
|
|
PostRevisor.new(post).revise!(post.user, locale: "ja")
|
|
|
|
post.reload
|
|
expect(post.locale).to eq("ja")
|
|
end
|
|
|
|
it "keeps the topic locale unchanged when editing the first post locale" do
|
|
post = Fabricate(:post, locale: "en")
|
|
post.topic.update!(locale: "fr")
|
|
|
|
PostRevisor.new(post).revise!(post.user, locale: "ja")
|
|
|
|
expect(post.topic.reload.locale).to eq("fr")
|
|
end
|
|
end
|
|
|
|
describe "revise wiki" do
|
|
before { SiteSetting.unique_posts_mins = 10 }
|
|
|
|
it "allows the user to change it to a wiki" do
|
|
pc =
|
|
PostCreator.new(newuser, topic_id: topic.id, raw: "this is a post that will become a wiki")
|
|
post = pc.create
|
|
expect(post.revise(post.user, wiki: true)).to be_truthy
|
|
post.reload
|
|
expect(post.wiki).to be_truthy
|
|
end
|
|
end
|
|
|
|
describe "revise" do
|
|
subject(:post_revisor) { PostRevisor.new(post) }
|
|
|
|
let(:post) { Fabricate(:post, post_args) }
|
|
let(:first_version_at) { post.last_version_at }
|
|
|
|
it "destroys last revision if edit is undone" do
|
|
old_raw = post.raw
|
|
|
|
post_revisor.revise!(admin, raw: "new post body", tags: ["new-tag"])
|
|
expect(post.topic.reload.tags.map(&:name)).to contain_exactly("new-tag")
|
|
expect(post.post_revisions.reload.size).to eq(1)
|
|
expect(post_revisor.raw_changed?).to eq(true)
|
|
|
|
post_revisor.revise!(admin, raw: old_raw, tags: [])
|
|
expect(post.topic.reload.tags.map(&:name)).to be_empty
|
|
expect(post.post_revisions.reload.size).to eq(0)
|
|
|
|
post_revisor.revise!(admin, raw: "next post body", tags: ["new-tag"])
|
|
expect(post.topic.reload.tags.map(&:name)).to contain_exactly("new-tag")
|
|
expect(post.post_revisions.reload.size).to eq(1)
|
|
end
|
|
|
|
describe "with the same body" do
|
|
it "doesn't change version" do
|
|
expect {
|
|
expect(post_revisor.revise!(post.user, raw: post.raw)).to eq(false)
|
|
post.reload
|
|
}.not_to change(post, :version)
|
|
end
|
|
end
|
|
|
|
describe "with nil raw contents" do
|
|
it "doesn't change version" do
|
|
expect {
|
|
expect(post_revisor.revise!(post.user, raw: nil)).to eq(false)
|
|
post.reload
|
|
}.not_to change(post, :version)
|
|
end
|
|
end
|
|
|
|
describe "topic is in slow mode" do
|
|
before { topic.update!(slow_mode_seconds: 1000) }
|
|
|
|
it "regular edits are not allowed by default" do
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 1000.minutes,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.errors.present?).to eq(true)
|
|
expect(post.errors.messages[:base].first).to be I18n.t("cannot_edit_on_slow_mode")
|
|
end
|
|
|
|
it "grace period editing is allowed" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.seconds,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.errors).to be_empty
|
|
end
|
|
|
|
it "regular edits are allowed if it was turned on in settings" do
|
|
SiteSetting.slow_mode_prevents_editing = false
|
|
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.errors).to be_empty
|
|
end
|
|
|
|
it "staff is allowed to edit posts even if the topic is in slow mode" do
|
|
admin = Fabricate(:admin)
|
|
post_revisor.revise!(
|
|
admin,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.errors).to be_empty
|
|
end
|
|
end
|
|
|
|
describe "grace period editing" do
|
|
it "correctly applies edits" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.seconds,
|
|
)
|
|
post.reload
|
|
|
|
expect(post.version).to eq(1)
|
|
expect(post.public_version).to eq(1)
|
|
expect(post.revisions.size).to eq(0)
|
|
expect(post.last_version_at).to eq_time(first_version_at)
|
|
expect(post_revisor.category_changed).to be_blank
|
|
end
|
|
|
|
it "does create a new version if a large diff happens" do
|
|
SiteSetting.editing_grace_period_max_diff = 10
|
|
|
|
post = Fabricate(:post, raw: "hello world")
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world123456789" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
|
|
post.reload
|
|
|
|
expect(post.version).to eq(1)
|
|
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world12345678901" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
|
|
expect(post.revisions.first.modifications["raw"][0]).to eq("hello world")
|
|
expect(post.revisions.first.modifications["cooked"][0]).to eq("<p>hello world</p>")
|
|
|
|
SiteSetting.editing_grace_period_max_diff_high_trust = 100
|
|
|
|
post.user.update_columns(trust_level: 2)
|
|
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world12345678901 123456789012" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.revisions.count).to eq(1)
|
|
end
|
|
|
|
it "creates a new version when diff computation exceeds the comparison budget" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
SiteSetting.editing_grace_period_max_diff = 1_000
|
|
|
|
post = Fabricate(:post, raw: "hello world")
|
|
revisor = PostRevisor.new(post)
|
|
|
|
ONPDiff
|
|
.any_instance
|
|
.stubs(:short_diff)
|
|
.raises(
|
|
ONPDiff::DiffLimitExceeded.new(
|
|
comparisons_used: 2_000_001,
|
|
comparison_budget: 2_000_000,
|
|
left_size: 11,
|
|
right_size: 12,
|
|
),
|
|
)
|
|
|
|
revisor.revise!(post.user, { raw: "hello world!" }, revised_at: post.updated_at + 1.second)
|
|
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.revisions.count).to eq(1)
|
|
end
|
|
|
|
it "creates a new version when the post is flagged" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
|
|
post = Fabricate(:post, raw: "hello world")
|
|
|
|
Fabricate(:flag_post_action, post: post, user: user)
|
|
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world, JK" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.revisions.count).to eq(1)
|
|
end
|
|
|
|
it "doesn't create a new version" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
SiteSetting.editing_grace_period_max_diff = 100
|
|
|
|
# making a revision
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
# "roll back"
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "Hello world" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 2.seconds,
|
|
)
|
|
|
|
post.reload
|
|
|
|
expect(post.version).to eq(1)
|
|
expect(post.public_version).to eq(1)
|
|
expect(post.revisions.size).to eq(0)
|
|
end
|
|
|
|
it "does not create a new version for tag-only topic change within grace period when tags unchanged" do
|
|
SiteSetting.tagging_enabled = true
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
|
|
post_revisor.revise!(post.user, { tags: [] }, revised_at: post.updated_at + 10.seconds)
|
|
post.reload
|
|
|
|
expect(post.version).to eq(1)
|
|
expect(post.revisions.size).to eq(0)
|
|
end
|
|
end
|
|
|
|
describe "edit reasons" do
|
|
it "does create a new version if an edit reason is provided" do
|
|
post = Fabricate(:post, raw: "hello world")
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world123456789", edit_reason: "this is my reason" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.revisions.count).to eq(1)
|
|
end
|
|
|
|
it "resets the edit_reason attribute in post model" do
|
|
freeze_time
|
|
SiteSetting.editing_grace_period = 5.seconds
|
|
post = Fabricate(:post, raw: "hello world")
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world123456789", edit_reason: "this is my reason" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
post.reload
|
|
expect(post.edit_reason).to eq("this is my reason")
|
|
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world4321" },
|
|
revised_at: post.updated_at + 7.seconds,
|
|
)
|
|
post.reload
|
|
expect(post.edit_reason).not_to be_present
|
|
end
|
|
|
|
it "does not create a new version if an edit reason is provided and its the same as the current edit reason" do
|
|
post = Fabricate(:post, raw: "hello world", edit_reason: "this is my reason")
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world123456789", edit_reason: "this is my reason" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
post.reload
|
|
expect(post.version).to eq(1)
|
|
expect(post.revisions.count).to eq(0)
|
|
end
|
|
|
|
it "does not clobber the existing edit reason for a revision if it is not provided in a subsequent revision" do
|
|
post = Fabricate(:post, raw: "hello world")
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world123456789", edit_reason: "this is my reason" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
post.reload
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello some other thing" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
expect(post.revisions.first.modifications[:edit_reason]).to eq([nil, "this is my reason"])
|
|
end
|
|
end
|
|
|
|
describe "hidden post" do
|
|
it "correctly stores the modification value" do
|
|
post.update(hidden: true, hidden_reason_id: Post.hidden_reasons[:flag_threshold_reached])
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(post.user, { raw: "hello world" }, revised_at: post.updated_at + 11.minutes)
|
|
expect(post.revisions.first.modifications.symbolize_keys).to eq(
|
|
cooked: ["<p>Hello world</p>", "<p>hello world</p>"],
|
|
raw: ["Hello world", "hello world"],
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "revision much later" do
|
|
let!(:revised_at) { post.updated_at + 2.minutes }
|
|
|
|
before do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
post_revisor.revise!(post.user, { raw: "updated body" }, revised_at: revised_at)
|
|
post.reload
|
|
end
|
|
|
|
it "doesn't update a category" do
|
|
expect(post_revisor.category_changed).to be_blank
|
|
end
|
|
|
|
it "updates the versions" do
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
end
|
|
|
|
it "creates a new revision" do
|
|
expect(post.revisions.size).to eq(1)
|
|
end
|
|
|
|
it "updates the last_version_at" do
|
|
expect(post.last_version_at.to_i).to eq(revised_at.to_i)
|
|
end
|
|
|
|
describe "new edit window" do
|
|
before do
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "yet another updated body" },
|
|
revised_at: revised_at,
|
|
)
|
|
post.reload
|
|
end
|
|
|
|
it "doesn't create a new version if you do another" do
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
end
|
|
|
|
it "doesn't change last_version_at" do
|
|
expect(post.last_version_at.to_i).to eq(revised_at.to_i)
|
|
end
|
|
|
|
it "doesn't update a category" do
|
|
expect(post_revisor.category_changed).to be_blank
|
|
end
|
|
|
|
context "after second window" do
|
|
let!(:new_revised_at) { revised_at + 2.minutes }
|
|
|
|
before do
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "yet another, another updated body" },
|
|
revised_at: new_revised_at,
|
|
)
|
|
post.reload
|
|
end
|
|
|
|
it "does create a new version after the edit window" do
|
|
expect(post.version).to eq(3)
|
|
expect(post.public_version).to eq(3)
|
|
end
|
|
|
|
it "does create a new version after the edit window" do
|
|
expect(post.last_version_at.to_i).to eq(new_revised_at.to_i)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "category topic" do
|
|
let!(:category) do
|
|
category = Fabricate(:category)
|
|
category.update_column(:topic_id, topic.id)
|
|
category
|
|
end
|
|
|
|
let(:new_description) { "this is my new description." }
|
|
|
|
it "should have no description by default" do
|
|
expect(category.description).to be_blank
|
|
end
|
|
|
|
context "with one paragraph description" do
|
|
before do
|
|
post_revisor.revise!(post.user, raw: new_description)
|
|
category.reload
|
|
end
|
|
|
|
it "returns the changed category info" do
|
|
expect(post_revisor.category_changed).to eq(category)
|
|
end
|
|
|
|
it "updates the description of the category" do
|
|
expect(category.description).to eq(new_description)
|
|
end
|
|
end
|
|
|
|
context "with multiple paragraph description" do
|
|
before do
|
|
post_revisor.revise!(post.user, raw: "#{new_description}\n\nOther content goes here.")
|
|
category.reload
|
|
end
|
|
|
|
it "returns the changed category info" do
|
|
expect(post_revisor.category_changed).to eq(category)
|
|
end
|
|
|
|
it "updates the description of the category" do
|
|
expect(category.description).to eq(new_description)
|
|
end
|
|
end
|
|
|
|
context "with invalid description without paragraphs" do
|
|
before do
|
|
post_revisor.revise!(post.user, raw: "# This is a title")
|
|
category.reload
|
|
end
|
|
|
|
it "returns a error for the user" do
|
|
expect(post.errors.present?).to eq(true)
|
|
expect(post.errors.messages[:base].first).to be I18n.t(
|
|
"category.errors.description_incomplete",
|
|
)
|
|
end
|
|
|
|
it "doesn't update the description of the category" do
|
|
expect(category.description).to eq(nil)
|
|
end
|
|
end
|
|
|
|
context "when updating back to the original paragraph" do
|
|
before do
|
|
category.update_column(:description, "this is my description")
|
|
post_revisor.revise!(post.user, raw: Category.post_template)
|
|
category.reload
|
|
end
|
|
|
|
it "puts the description back to nothing" do
|
|
expect(category.description).to be_blank
|
|
end
|
|
|
|
it "returns the changed category info" do
|
|
expect(post_revisor.category_changed).to eq(category)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "rate limiter" do
|
|
fab!(:changed_by) { coding_horror }
|
|
|
|
before do
|
|
RateLimiter.enable
|
|
SiteSetting.editing_grace_period = 0
|
|
end
|
|
|
|
it "triggers a rate limiter" do
|
|
EditRateLimiter.any_instance.expects(:performed!)
|
|
post_revisor.revise!(changed_by, raw: "updated body")
|
|
end
|
|
|
|
it "raises error when a user gets rate limited" do
|
|
SiteSetting.max_edits_per_day = 1
|
|
user = Fabricate(:user, trust_level: 1)
|
|
|
|
post_revisor.revise!(user, raw: "body (edited)")
|
|
|
|
expect do post_revisor.revise!(user, raw: "body (edited twice) ") end.to raise_error(
|
|
RateLimiter::LimitExceeded,
|
|
)
|
|
end
|
|
|
|
it "edit limits scale up depending on user's trust level" do
|
|
SiteSetting.max_edits_per_day = 1
|
|
SiteSetting.tl2_additional_edits_per_day_multiplier = 2
|
|
SiteSetting.tl3_additional_edits_per_day_multiplier = 3
|
|
SiteSetting.tl4_additional_edits_per_day_multiplier = 4
|
|
|
|
user = Fabricate(:user, trust_level: 2)
|
|
expect { post_revisor.revise!(user, raw: "body (edited)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited twice)") }.to_not raise_error
|
|
expect do post_revisor.revise!(user, raw: "body (edited three times) ") end.to raise_error(
|
|
RateLimiter::LimitExceeded,
|
|
)
|
|
|
|
user = Fabricate(:user, trust_level: 3)
|
|
expect { post_revisor.revise!(user, raw: "body (edited)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited twice)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited three times)") }.to_not raise_error
|
|
expect do post_revisor.revise!(user, raw: "body (edited four times) ") end.to raise_error(
|
|
RateLimiter::LimitExceeded,
|
|
)
|
|
|
|
user = Fabricate(:user, trust_level: 4)
|
|
expect { post_revisor.revise!(user, raw: "body (edited)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited twice)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited three times)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited four times)") }.to_not raise_error
|
|
expect do post_revisor.revise!(user, raw: "body (edited five times) ") end.to raise_error(
|
|
RateLimiter::LimitExceeded,
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "admin editing a new user's post" do
|
|
fab!(:changed_by, :admin)
|
|
|
|
before do
|
|
SiteSetting.newuser_max_embedded_media = 0
|
|
url = "http://i.imgur.com/wfn7rgU.jpg"
|
|
Oneboxer.stubs(:onebox).with(url, anything).returns("<img src='#{url}'>")
|
|
post_revisor.revise!(changed_by, raw: "So, post them here!\n#{url}")
|
|
end
|
|
|
|
it "allows an admin to insert images into a new user's post" do
|
|
expect(post.errors).to be_blank
|
|
end
|
|
|
|
it "marks the admin as the last updater" do
|
|
expect(post.last_editor_id).to eq(changed_by.id)
|
|
end
|
|
end
|
|
|
|
describe "new user editing their own post" do
|
|
before do
|
|
SiteSetting.newuser_max_embedded_media = 0
|
|
url = "http://i.imgur.com/FGg7Vzu.gif"
|
|
Oneboxer.stubs(:cached_onebox).with(url, anything).returns("<img src='#{url}'>")
|
|
post_revisor.revise!(post.user, raw: "So, post them here!\n#{url}")
|
|
end
|
|
|
|
it "doesn't allow images to be inserted" do
|
|
expect(post.errors).to be_present
|
|
end
|
|
end
|
|
|
|
describe "with a new body" do
|
|
before { SiteSetting.editing_grace_period_max_diff = 1000 }
|
|
|
|
fab!(:changed_by) { coding_horror }
|
|
let!(:result) { post_revisor.revise!(changed_by, raw: "lets update the body. Здравствуйте") }
|
|
|
|
it "correctly updates raw" do
|
|
expect(result).to eq(true)
|
|
expect(post.raw).to eq("lets update the body. Здравствуйте")
|
|
expect(post.invalidate_oneboxes).to eq(true)
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
expect(post.revisions.size).to eq(1)
|
|
expect(post.revisions.first.user_id).to eq(changed_by.id)
|
|
|
|
# updates word count
|
|
expect(post.word_count).to eq(5)
|
|
post.topic.reload
|
|
expect(post.topic.word_count).to eq(5)
|
|
end
|
|
|
|
it "increases the post_edits stat count" do
|
|
expect do post_revisor.revise!(post.user, { raw: "This is a new revision" }) end.to change {
|
|
post.user.user_stat.post_edits_count.to_i
|
|
}.by(1)
|
|
end
|
|
|
|
context "when second poster posts again quickly" do
|
|
it "is a grace period edit, because the second poster posted again quickly" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
post_revisor.revise!(
|
|
changed_by,
|
|
{ raw: "yet another updated body" },
|
|
revised_at: post.updated_at + 10.seconds,
|
|
)
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
expect(post.revisions.size).to eq(1)
|
|
end
|
|
end
|
|
|
|
context "when passing skip_revision as true" do
|
|
before do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
post_revisor.revise!(
|
|
changed_by,
|
|
{ raw: "yet another updated body" },
|
|
revised_at: post.updated_at + 10.hours,
|
|
skip_revision: true,
|
|
)
|
|
post.reload
|
|
end
|
|
|
|
it "does not create new revision " do
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
expect(post.revisions.size).to eq(1)
|
|
end
|
|
end
|
|
|
|
context "when editing the before_edit_post event signature" do
|
|
it "contains post and params" do
|
|
params = { raw: "body (edited)" }
|
|
events = DiscourseEvent.track_events { post_revisor.revise!(user, params) }
|
|
expect(events).to include(event_name: :before_edit_post, params: [post, params])
|
|
end
|
|
end
|
|
|
|
context "when editing the post_edited event signature for extensibility" do
|
|
it "exposes revise opts via the PostRevisor payload" do
|
|
params = { raw: "body (edited)" }
|
|
opts = { suggested_edit: true }
|
|
|
|
events = DiscourseEvent.track_events { post_revisor.revise!(user, params, opts) }
|
|
event = events.find { |e| e[:event_name] == :post_edited }
|
|
|
|
expect(event[:params].third).to be_kind_of(PostRevisor)
|
|
expect(event[:params].third.opts).to include(suggested_edit: true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "topic excerpt" do
|
|
it "topic excerpt is updated only if first post is revised" do
|
|
revisor = PostRevisor.new(post)
|
|
first_post = topic.first_post
|
|
expect {
|
|
revisor.revise!(
|
|
first_post.user,
|
|
{ raw: "Edit the first post" },
|
|
revised_at: first_post.updated_at + 10.seconds,
|
|
)
|
|
topic.reload
|
|
}.to change { topic.excerpt }
|
|
second_post = Fabricate(:post, post_args.merge(post_number: 2, topic_id: topic.id))
|
|
expect {
|
|
PostRevisor.new(second_post).revise!(second_post.user, raw: "Edit the 2nd post")
|
|
topic.reload
|
|
}.to_not change { topic.excerpt }
|
|
end
|
|
end
|
|
|
|
describe "changing post ownership" do
|
|
it "does not call Topic.reset_highest when only user_id is changed" do
|
|
new_owner = Fabricate(:user)
|
|
Topic.expects(:reset_highest).never
|
|
|
|
post_revisor.revise!(admin, user_id: new_owner.id)
|
|
end
|
|
|
|
it "calls Topic.reset_highest when user_id and other fields are changed" do
|
|
new_owner = Fabricate(:user)
|
|
Topic.expects(:reset_highest).once
|
|
|
|
post_revisor.revise!(admin, user_id: new_owner.id, raw: "updated body")
|
|
end
|
|
|
|
it "does not increment post_edits_count when system user changes ownership" do
|
|
new_owner = Fabricate(:user)
|
|
system_user = Discourse.system_user
|
|
|
|
expect do post_revisor.revise!(system_user, user_id: new_owner.id) end.not_to change {
|
|
system_user.user_stat.post_edits_count.to_i
|
|
}
|
|
end
|
|
end
|
|
|
|
it "doesn't strip starting whitespaces" do
|
|
post_revisor.revise!(post.user, raw: " <-- whitespaces --> ")
|
|
post.reload
|
|
expect(post.raw).to eq(" <-- whitespaces -->")
|
|
end
|
|
|
|
it "revises and tracks changes of topic titles" do
|
|
new_title = "New topic title"
|
|
result =
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ title: new_title },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.title).to eq(new_title)
|
|
expect(post.revisions.first.modifications["title"][1]).to eq(new_title)
|
|
expect(post_revisor.topic_title_changed?).to eq(true)
|
|
expect(post_revisor.raw_changed?).to eq(false)
|
|
end
|
|
|
|
it "revises and tracks changes of topic archetypes for staff" do
|
|
new_archetype = Archetype.banner
|
|
result =
|
|
post_revisor.revise!(
|
|
admin,
|
|
{ archetype: new_archetype },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.archetype).to eq(new_archetype)
|
|
expect(post.revisions.first.modifications["archetype"][1]).to eq(new_archetype)
|
|
expect(post_revisor.raw_changed?).to eq(false)
|
|
end
|
|
|
|
it "does not allow regular users to change topic archetype to banner" do
|
|
result =
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ archetype: Archetype.banner },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
|
|
expect(result).to eq(false)
|
|
post.reload
|
|
expect(post.topic.archetype).to eq(Archetype.default)
|
|
end
|
|
|
|
it "revises and tracks changes of topic tags" do
|
|
post_revisor.revise!(admin, tags: ["new-tag"])
|
|
expect(post.post_revisions.last.modifications).to eq("tags" => [[], ["new-tag"]])
|
|
expect(post_revisor.raw_changed?).to eq(false)
|
|
|
|
post_revisor.revise!(admin, tags: %w[new-tag new-tag-2])
|
|
before, after = post.post_revisions.last.modifications["tags"]
|
|
expect(before).to contain_exactly("new-tag")
|
|
expect(after).to contain_exactly("new-tag", "new-tag-2")
|
|
expect(post_revisor.raw_changed?).to eq(false)
|
|
|
|
post_revisor.revise!(admin, tags: ["new-tag-3"])
|
|
before, after = post.post_revisions.last.modifications["tags"]
|
|
expect(before).to contain_exactly("new-tag", "new-tag-2")
|
|
expect(after).to contain_exactly("new-tag-3")
|
|
expect(post_revisor.raw_changed?).to eq(false)
|
|
end
|
|
|
|
it "tracks tag changes by IDs (integers) with revision storing names" do
|
|
tag1 = Fabricate(:tag, name: "existing-tag")
|
|
tag2 = Fabricate(:tag, name: "another-tag")
|
|
|
|
post.topic.update!(tags: [tag1])
|
|
|
|
post_revisor.revise!(admin, tags: [{ id: tag2.id, name: tag2.name }])
|
|
|
|
modifications = post.post_revisions.last.modifications
|
|
expect(modifications["tags"]).to eq([["existing-tag"], ["another-tag"]])
|
|
end
|
|
|
|
it "tracks tag changes from empty to tagged using objects" do
|
|
tag = Fabricate(:tag, name: "new-via-id")
|
|
|
|
post_revisor.revise!(admin, tags: [{ id: tag.id, name: tag.name }])
|
|
|
|
modifications = post.post_revisions.last.modifications
|
|
expect(modifications["tags"]).to eq([[], ["new-via-id"]])
|
|
end
|
|
|
|
it "tracks tag changes from tagged to empty" do
|
|
tag = Fabricate(:tag, name: "will-remove")
|
|
post.topic.update!(tags: [tag])
|
|
|
|
post_revisor.revise!(admin, tags: [])
|
|
|
|
modifications = post.post_revisions.last.modifications
|
|
expect(modifications["tags"]).to eq([["will-remove"], []])
|
|
end
|
|
|
|
describe "#publish_changes" do
|
|
let!(:post) { Fabricate(:post, topic: topic) }
|
|
|
|
it "should publish topic changes to clients" do
|
|
revisor = PostRevisor.new(topic.ordered_posts.first, topic)
|
|
|
|
message =
|
|
MessageBus
|
|
.track_publish("/topic/#{topic.id}") do
|
|
revisor.revise!(newuser, title: "this is a test topic")
|
|
end
|
|
.first
|
|
|
|
payload = message.data
|
|
expect(payload[:reload_topic]).to eq(true)
|
|
end
|
|
end
|
|
|
|
context "when logging staff edits" do
|
|
it "doesn't log when a regular user revises a post" do
|
|
post_revisor.revise!(post.user, raw: "lets totally update the body")
|
|
log =
|
|
UserHistory.where(acting_user_id: post.user.id, action: UserHistory.actions[:post_edit])
|
|
expect(log).to be_blank
|
|
end
|
|
|
|
it "logs an edit when a staff member revises a post" do
|
|
post_revisor.revise!(moderator, raw: "lets totally update the body")
|
|
log =
|
|
UserHistory.where(
|
|
acting_user_id: moderator.id,
|
|
action: UserHistory.actions[:post_edit],
|
|
).first
|
|
expect(log).to be_present
|
|
expect(log.details).to eq("Hello world\n\n---\n\nlets totally update the body")
|
|
end
|
|
|
|
it "doesn't log an edit when skip_staff_log is true" do
|
|
post_revisor.revise!(
|
|
moderator,
|
|
{ raw: "lets totally update the body" },
|
|
skip_staff_log: true,
|
|
)
|
|
log =
|
|
UserHistory.where(
|
|
acting_user_id: moderator.id,
|
|
action: UserHistory.actions[:post_edit],
|
|
).first
|
|
expect(log).to be_blank
|
|
end
|
|
|
|
it "doesn't log an edit when a staff member edits their own post" do
|
|
revisor = PostRevisor.new(Fabricate(:post, user: moderator))
|
|
revisor.revise!(moderator, raw: "my own edit to my own thing")
|
|
|
|
log =
|
|
UserHistory.where(acting_user_id: moderator.id, action: UserHistory.actions[:post_edit])
|
|
expect(log).to be_blank
|
|
end
|
|
end
|
|
|
|
context "when logging group moderator edits" do
|
|
fab!(:group_user)
|
|
fab!(:category) { Fabricate(:category, topic: topic) }
|
|
fab!(:category_moderation_group) do
|
|
Fabricate(:category_moderation_group, category:, group: group_user.group)
|
|
end
|
|
|
|
before do
|
|
SiteSetting.enable_category_group_moderation = true
|
|
topic.update!(category: category)
|
|
post.update!(topic: topic)
|
|
end
|
|
|
|
it "logs an edit when a group moderator revises the category description" do
|
|
PostRevisor.new(post).revise!(
|
|
group_user.user,
|
|
raw: "a group moderator can update the description",
|
|
)
|
|
|
|
log =
|
|
UserHistory.where(
|
|
acting_user_id: group_user.user.id,
|
|
action: UserHistory.actions[:post_edit],
|
|
).first
|
|
expect(log).to be_present
|
|
expect(log.details).to eq(
|
|
"Hello world\n\n---\n\na group moderator can update the description",
|
|
)
|
|
end
|
|
end
|
|
|
|
context "with staff_edit_locks_post" do
|
|
context "when disabled" do
|
|
before { SiteSetting.staff_edit_locks_post = false }
|
|
|
|
it "does not lock the post when revised" do
|
|
result = post_revisor.revise!(moderator, raw: "lets totally update the body")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post).not_to be_locked
|
|
end
|
|
end
|
|
|
|
context "when enabled" do
|
|
before { SiteSetting.staff_edit_locks_post = true }
|
|
|
|
it "locks the post when revised by staff" do
|
|
result = post_revisor.revise!(moderator, raw: "lets totally update the body")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post).to be_locked
|
|
end
|
|
|
|
it "doesn't lock the wiki posts" do
|
|
post.wiki = true
|
|
result = post_revisor.revise!(moderator, raw: "some new raw content")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post).not_to be_locked
|
|
end
|
|
|
|
it "doesn't lock the post when the raw did not change" do
|
|
result = post_revisor.revise!(moderator, title: "New topic title, cool!")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.title).to eq("New topic title, cool!")
|
|
expect(post).not_to be_locked
|
|
end
|
|
|
|
it "doesn't lock the post when revised by a regular user" do
|
|
result = post_revisor.revise!(user, raw: "lets totally update the body")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post).not_to be_locked
|
|
end
|
|
|
|
it "doesn't lock the post when revised by system user" do
|
|
result =
|
|
post_revisor.revise!(Discourse.system_user, raw: "I usually replace hotlinked images")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post).not_to be_locked
|
|
end
|
|
|
|
it "doesn't lock a staff member's post" do
|
|
staff_post = Fabricate(:post, user: moderator)
|
|
revisor = PostRevisor.new(staff_post)
|
|
|
|
result = revisor.revise!(moderator, raw: "lets totally update the body")
|
|
expect(result).to eq(true)
|
|
staff_post.reload
|
|
expect(staff_post).not_to be_locked
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with alerts" do
|
|
fab!(:mentioned_user, :user)
|
|
|
|
before { Jobs.run_immediately! }
|
|
|
|
it "generates a notification for a mention" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "Random user is mentioning @#{mentioned_user.username_lower}",
|
|
)
|
|
}.to change { Notification.where(notification_type: Notification.types[:mentioned]).count }
|
|
end
|
|
|
|
it "never generates a notification for a mention when the System user revise a post" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
Discourse.system_user,
|
|
raw: "System user is mentioning @#{mentioned_user.username_lower}",
|
|
)
|
|
}.not_to change {
|
|
Notification.where(notification_type: Notification.types[:mentioned]).count
|
|
}
|
|
end
|
|
end
|
|
|
|
context "with tagging" do
|
|
context "with tagging disabled" do
|
|
before { SiteSetting.tagging_enabled = false }
|
|
|
|
it "doesn't add the tags" do
|
|
result =
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "lets totally update the body",
|
|
tags: %w[totally update],
|
|
)
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.size).to eq(0)
|
|
end
|
|
end
|
|
|
|
context "with tagging enabled" do
|
|
before { SiteSetting.tagging_enabled = true }
|
|
|
|
context "when can create tags" do
|
|
before do
|
|
SiteSetting.create_tag_allowed_groups = "1|3|#{Group::AUTO_GROUPS[:trust_level_0]}"
|
|
SiteSetting.tag_topic_allowed_groups = "1|3|#{Group::AUTO_GROUPS[:trust_level_0]}"
|
|
end
|
|
|
|
it "can create all tags if none exist" do
|
|
expect {
|
|
@result =
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "lets totally update the body",
|
|
tags: %w[totally update],
|
|
)
|
|
}.to change { Tag.count }.by(2)
|
|
expect(@result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name).sort).to eq(%w[totally update])
|
|
end
|
|
|
|
it "creates missing tags if some exist" do
|
|
Fabricate(:tag, name: "totally")
|
|
expect {
|
|
@result =
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "lets totally update the body",
|
|
tags: %w[totally update],
|
|
)
|
|
}.to change { Tag.count }.by(1)
|
|
expect(@result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name).sort).to eq(%w[totally update])
|
|
end
|
|
|
|
it "can remove all tags" do
|
|
topic.tags = [Fabricate(:tag, name: "super"), Fabricate(:tag, name: "stuff")]
|
|
result = post_revisor.revise!(user, raw: "lets totally update the body", tags: [])
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.size).to eq(0)
|
|
end
|
|
|
|
it "can't add staff-only tags" do
|
|
create_staff_only_tags(["important"])
|
|
result =
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "lets totally update the body",
|
|
tags: %w[important stuff],
|
|
)
|
|
expect(result).to eq(false)
|
|
expect(post.topic.errors.present?).to eq(true)
|
|
end
|
|
|
|
it "staff can add staff-only tags" do
|
|
create_staff_only_tags(["important"])
|
|
result =
|
|
post_revisor.revise!(
|
|
admin,
|
|
raw: "lets totally update the body",
|
|
tags: %w[important stuff],
|
|
)
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name).sort).to eq(%w[important stuff])
|
|
end
|
|
|
|
it "triggers the :post_edited event with topic_changed?" do
|
|
topic.tags = [Fabricate(:tag, name: "super"), Fabricate(:tag, name: "stuff")]
|
|
|
|
events =
|
|
DiscourseEvent.track_events do
|
|
post_revisor.revise!(user, raw: "lets totally update the body", tags: [])
|
|
end
|
|
|
|
event = events.find { |e| e[:event_name] == :post_edited }
|
|
|
|
expect(event[:params].first).to eq(post)
|
|
expect(event[:params].second).to eq(true)
|
|
expect(event[:params].third).to be_kind_of(PostRevisor)
|
|
expect(event[:params].third.topic_diff).to eq({ "tags" => [%w[stuff super], []] })
|
|
end
|
|
|
|
context "with staff-only tags" do
|
|
before do
|
|
create_staff_only_tags(["important"])
|
|
topic = post.topic
|
|
topic.tags = [
|
|
Fabricate(:tag, name: "super"),
|
|
Tag.where(name: "important").first,
|
|
Fabricate(:tag, name: "stuff"),
|
|
]
|
|
end
|
|
|
|
it "staff-only tags can't be removed" do
|
|
result =
|
|
post_revisor.revise!(user, raw: "lets totally update the body", tags: ["stuff"])
|
|
expect(result).to eq(false)
|
|
expect(post.topic.errors.present?).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name).sort).to eq(%w[important stuff super])
|
|
end
|
|
|
|
it "can't remove all tags if some are staff-only" do
|
|
result = post_revisor.revise!(user, raw: "lets totally update the body", tags: [])
|
|
expect(result).to eq(false)
|
|
expect(post.topic.errors.present?).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name).sort).to eq(%w[important stuff super])
|
|
end
|
|
|
|
it "staff-only tags can be removed by staff" do
|
|
result =
|
|
post_revisor.revise!(admin, raw: "lets totally update the body", tags: ["stuff"])
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name)).to eq(["stuff"])
|
|
end
|
|
|
|
it "staff can remove all tags" do
|
|
result = post_revisor.revise!(admin, raw: "lets totally update the body", tags: [])
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.size).to eq(0)
|
|
end
|
|
end
|
|
|
|
context "with hidden tags" do
|
|
fab!(:super_tag) { Fabricate(:tag, name: "super") }
|
|
fab!(:stuff_tag) { Fabricate(:tag, name: "stuff") }
|
|
let(:bumped_at) { 1.day.ago }
|
|
|
|
before do
|
|
topic.update!(bumped_at: bumped_at)
|
|
create_hidden_tags(%w[important secret])
|
|
topic = post.topic
|
|
topic.tags = [super_tag, Tag.where(name: "important").first, stuff_tag]
|
|
end
|
|
|
|
it "creates a hidden revision" do
|
|
post_revisor.revise!(
|
|
Fabricate(:admin),
|
|
raw: post.raw,
|
|
tags: topic.tags.map(&:name) + ["secret"],
|
|
)
|
|
expect(post.reload.revisions.first.hidden).to eq(true)
|
|
end
|
|
|
|
it "doesn't increment public_version for hidden revisions" do
|
|
post_revisor.revise!(admin, raw: post.raw, tags: topic.tags.map(&:name) + ["secret"])
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(1)
|
|
end
|
|
|
|
it "doesn't decrement public_version when hidden revision is destroyed" do
|
|
original_tags = topic.tags.map(&:name)
|
|
post_revisor.revise!(admin, raw: post.raw, tags: original_tags + ["secret"])
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(1)
|
|
expect(post.revisions.count).to eq(1)
|
|
|
|
post_revisor.revise!(admin, raw: post.raw, tags: original_tags)
|
|
post.reload
|
|
expect(post.version).to eq(1)
|
|
expect(post.public_version).to eq(1)
|
|
expect(post.revisions.count).to eq(0)
|
|
end
|
|
|
|
it "creates a separate revision when a different user changes hidden tags instead of folding into the first author's revision" do
|
|
admin_a = Fabricate(:admin)
|
|
admin_b = Fabricate(:admin)
|
|
original_tags = topic.tags.map(&:name)
|
|
|
|
PostRevisor.new(post.reload).revise!(
|
|
admin_a,
|
|
raw: post.raw,
|
|
tags: original_tags + ["secret"],
|
|
)
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.revisions.last.user_id).to eq(admin_a.id)
|
|
|
|
PostRevisor.new(post.reload).revise!(admin_b, raw: post.raw, tags: original_tags)
|
|
post.reload
|
|
expect(post.version).to eq(3)
|
|
expect(post.revisions.count).to eq(2)
|
|
expect(post.revisions.last.user_id).to eq(admin_b.id)
|
|
end
|
|
|
|
it "increments public_version when hidden tag added with other visible changes" do
|
|
post_revisor.revise!(
|
|
admin,
|
|
raw: "#{post.raw} with additional content",
|
|
tags: topic.tags.map(&:name) + ["secret"],
|
|
)
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
expect(post.revisions.first.hidden).to eq(false)
|
|
end
|
|
|
|
it "doesn't notify topic owner about hidden tags" do
|
|
PostActionNotifier.enable
|
|
Jobs.run_immediately!
|
|
expect {
|
|
post_revisor.revise!(
|
|
Fabricate(:admin),
|
|
raw: post.raw,
|
|
tags: topic.tags.map(&:name) + ["secret"],
|
|
)
|
|
}.not_to change {
|
|
Notification.where(notification_type: Notification.types[:edited]).count
|
|
}
|
|
end
|
|
end
|
|
|
|
context "with required tag group" do
|
|
fab!(:tag1, :tag)
|
|
fab!(:tag2, :tag)
|
|
fab!(:tag3, :tag)
|
|
fab!(:tag_group) { Fabricate(:tag_group, tags: [tag1, tag2]) }
|
|
fab!(:category) do
|
|
Fabricate(
|
|
:category,
|
|
name: "beta",
|
|
category_required_tag_groups: [
|
|
CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1),
|
|
],
|
|
)
|
|
end
|
|
|
|
before { post.topic.update(category: category) }
|
|
|
|
it "doesn't allow removing all tags from the group" do
|
|
post.topic.tags = [tag1, tag2]
|
|
result = post_revisor.revise!(user, raw: "lets totally update the body", tags: [])
|
|
expect(result).to eq(false)
|
|
end
|
|
|
|
it "allows removing some tags" do
|
|
post.topic.tags = [tag1, tag2, tag3]
|
|
result =
|
|
post_revisor.revise!(user, raw: "lets totally update the body", tags: [tag1.name])
|
|
expect(result).to eq(true)
|
|
expect(post.reload.topic.tags.map(&:name)).to eq([tag1.name])
|
|
end
|
|
|
|
it "allows admins to remove the tags" do
|
|
post.topic.tags = [tag1, tag2, tag3]
|
|
result = post_revisor.revise!(admin, raw: "lets totally update the body", tags: [])
|
|
expect(result).to eq(true)
|
|
expect(post.reload.topic.tags.size).to eq(0)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when cannot create tags" do
|
|
before do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_4]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
end
|
|
|
|
it "only uses existing tags" do
|
|
Fabricate(:tag, name: "totally")
|
|
expect {
|
|
@result =
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "lets totally update the body",
|
|
tags: %w[totally update],
|
|
)
|
|
}.to_not change { Tag.count }
|
|
expect(@result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name)).to eq(["totally"])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with uploads" do
|
|
let(:image1) { Fabricate(:upload) }
|
|
let(:image2) { Fabricate(:upload) }
|
|
let(:image3) { Fabricate(:upload) }
|
|
let(:image4) { Fabricate(:upload) }
|
|
let(:post_args) { { user: user, topic: topic, raw: <<~RAW } }
|
|
This is a post with multiple uploads
|
|

|
|

|
|
RAW
|
|
|
|
it "updates linked post uploads" do
|
|
post.link_post_uploads
|
|
expect(post.upload_references.pluck(:upload_id)).to contain_exactly(image1.id, image2.id)
|
|
|
|
post_revisor.revise!(user, raw: <<~RAW)
|
|
This is a post with multiple uploads
|
|

|
|

|
|

|
|
RAW
|
|
|
|
expect(post.reload.upload_references.pluck(:upload_id)).to contain_exactly(
|
|
image2.id,
|
|
image3.id,
|
|
image4.id,
|
|
)
|
|
end
|
|
|
|
context "with secure uploads uploads" do
|
|
let!(:image5) { Fabricate(:secure_upload) }
|
|
before do
|
|
Jobs.run_immediately!
|
|
setup_s3
|
|
SiteSetting.authorized_extensions = "png|jpg|gif|mp4"
|
|
SiteSetting.secure_uploads = true
|
|
stub_upload(image5)
|
|
end
|
|
|
|
it "updates the upload secure status, which is secure by default from the composer. set to false for a public topic" do
|
|
stub_image_size
|
|
post_revisor.revise!(user, raw: <<~RAW)
|
|
This is a post with a secure upload
|
|

|
|
RAW
|
|
|
|
expect(image5.reload.secure).to eq(false)
|
|
expect(image5.security_last_changed_reason).to eq(
|
|
"access control post dictates security | source: post processor",
|
|
)
|
|
end
|
|
|
|
it "does not update the upload secure status, which is secure by default from the composer for a private" do
|
|
post.topic.update(category: Fabricate(:private_category, group: Fabricate(:group)))
|
|
stub_image_size
|
|
post_revisor.revise!(user, raw: <<~RAW)
|
|
This is a post with a secure upload
|
|

|
|
RAW
|
|
|
|
expect(image5.reload.secure).to eq(true)
|
|
expect(image5.security_last_changed_reason).to eq(
|
|
"access control post dictates security | source: post processor",
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with drafts" do
|
|
it "does not advance draft sequence if keep_existing_draft option is true" do
|
|
post = Fabricate(:post, user: user)
|
|
topic = post.topic
|
|
draft_key = "topic_#{topic.id}"
|
|
data = { reply: "test 12222" }.to_json
|
|
Draft.set(user, draft_key, 0, data)
|
|
Draft.set(user, draft_key, 0, data)
|
|
expect {
|
|
PostRevisor.new(post).revise!(
|
|
post.user,
|
|
{ title: "updated title for my topic" },
|
|
keep_existing_draft: true,
|
|
)
|
|
}.to not_change {
|
|
Draft.where(user: user, draft_key: draft_key).first.sequence
|
|
}.and not_change {
|
|
DraftSequence.where(user_id: user.id, draft_key: draft_key).first.sequence
|
|
}
|
|
|
|
expect {
|
|
PostRevisor.new(post).revise!(post.user, { title: "updated title for my topic" })
|
|
}.to change { Draft.where(user: user, draft_key: draft_key).count }.from(1).to(
|
|
0,
|
|
).and change {
|
|
DraftSequence.where(user_id: user.id, draft_key: draft_key).first.sequence
|
|
}.by(1)
|
|
end
|
|
end
|
|
|
|
context "when skipping validations" do
|
|
fab!(:post) { Fabricate(:post, raw: "aaa", skip_validation: true) }
|
|
|
|
it "can revise multiple times and remove unnecessary revisions" do
|
|
post_revisor.revise!(admin, { raw: "bbb" }, skip_validations: true)
|
|
expect(post.errors).to be_empty
|
|
|
|
# Revert to old version which was invalid to destroy previously created
|
|
# post revision and trigger another post save.
|
|
post_revisor.revise!(admin, { raw: "aaa" }, skip_validations: true)
|
|
expect(post.errors).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the review_every_post setting is enabled" do
|
|
let(:post) { Fabricate(:post, post_args) }
|
|
let(:revisor) { PostRevisor.new(post) }
|
|
|
|
before { SiteSetting.review_every_post = true }
|
|
|
|
it "queues the post when a regular user edits it" do
|
|
expect {
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
}.to change(ReviewablePost, :count).by(1)
|
|
end
|
|
|
|
it "does nothing when a staff member edits a post" do
|
|
admin = Fabricate(:admin)
|
|
|
|
expect { revisor.revise!(admin, { raw: "updated body" }) }.not_to change(
|
|
ReviewablePost,
|
|
:count,
|
|
)
|
|
end
|
|
|
|
it "skips grace period edits" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
|
|
expect {
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.seconds,
|
|
)
|
|
}.not_to change(ReviewablePost, :count)
|
|
end
|
|
end
|
|
|
|
describe "topic bumping" do
|
|
subject(:post_revisor) { PostRevisor.new(post) }
|
|
|
|
let(:post) { Fabricate(:post, post_args) }
|
|
|
|
it "doesn't bump the topic when editing the last post" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump the topic when editing a post that isn't the last post" do
|
|
create_post(topic_id: post.topic.id)
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump the topic when editing the topic title" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ title: "This is an updated topic title" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump the topic when editing the topic category" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ category_id: Fabricate(:category).id },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump the topic when editing tags" do
|
|
expect { post_revisor.revise!(post.user, { tags: %w[totally update] }) }.not_to change {
|
|
post.topic.bumped_at
|
|
}
|
|
end
|
|
|
|
describe "should_bump_topic plugin modifier" do
|
|
let(:plugin_instance) { Plugin::Instance.new }
|
|
let(:modifier_return_value) { nil }
|
|
let(:modifier_block) do
|
|
Proc.new do |value, modifier_post, modifier_post_changes, modifier_topic_changes, editor|
|
|
modifier_return_value
|
|
end
|
|
end
|
|
|
|
before { plugin_instance.register_modifier(:should_bump_topic, &modifier_block) }
|
|
|
|
after do
|
|
DiscoursePluginRegistry.unregister_modifier(
|
|
plugin_instance,
|
|
:should_bump_topic,
|
|
&modifier_block
|
|
)
|
|
end
|
|
|
|
context "when the modifier returns false" do
|
|
let(:modifier_return_value) { false }
|
|
|
|
it "prevents bumping" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
end
|
|
|
|
context "when the modifier returns true" do
|
|
let(:modifier_return_value) { true }
|
|
|
|
it "bumps the topic" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.to change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
it "bumps the persisted topic when editing raw and title" do
|
|
post.topic.update!(bumped_at: 1.day.ago)
|
|
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body", title: "This is an updated topic title" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.to change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
it "keeps post changes available to the modifier before advancing the draft" do
|
|
DiscoursePluginRegistry.unregister_modifier(
|
|
plugin_instance,
|
|
:should_bump_topic,
|
|
&modifier_block
|
|
)
|
|
|
|
inspecting_modifier =
|
|
Proc.new do |value, modifier_post, modifier_post_changes, modifier_topic_changes, editor|
|
|
modifier_post.is_first_post? && modifier_post_changes.any?
|
|
end
|
|
|
|
plugin_instance.register_modifier(:should_bump_topic, &inspecting_modifier)
|
|
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body", title: "Updated topic title" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.to change { post.topic.reload.bumped_at }
|
|
ensure
|
|
if defined?(inspecting_modifier)
|
|
DiscoursePluginRegistry.unregister_modifier(
|
|
plugin_instance,
|
|
:should_bump_topic,
|
|
&inspecting_modifier
|
|
)
|
|
end
|
|
plugin_instance.register_modifier(:should_bump_topic, &modifier_block)
|
|
end
|
|
|
|
it "doesn't bump the topic when the title edit is invalid" do
|
|
original_raw = post.raw
|
|
post.topic.update!(bumped_at: 1.day.ago)
|
|
|
|
messages =
|
|
MessageBus.track_publish(TopicTrackingState::LATEST_MESSAGE_BUS_CHANNEL) do
|
|
expect {
|
|
result =
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body", title: "New Title" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
expect(result).to eq(false)
|
|
}.not_to change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
expect(messages).to be_empty
|
|
expect(post.reload.raw).to eq(original_raw)
|
|
expect(post.version).to eq(1)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "for a wiki topic" do
|
|
before { post.update!(wiki: true) }
|
|
|
|
it "bumps the topic when the OP is edited" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.to change { post.topic.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump the topic when another post is edited" do
|
|
other_post = Fabricate(:post, topic: topic)
|
|
post_revisor_other = PostRevisor.new(other_post)
|
|
|
|
expect {
|
|
post_revisor_other.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
end
|
|
|
|
context "with hidden tags" do
|
|
let(:bumped_at) { 1.day.ago }
|
|
|
|
before do
|
|
post.topic.update!(bumped_at: bumped_at)
|
|
create_hidden_tags(%w[important secret])
|
|
post.topic.tags = [
|
|
Fabricate(:tag, name: "super"),
|
|
Tag.where(name: "important").first,
|
|
Fabricate(:tag, name: "stuff"),
|
|
]
|
|
end
|
|
|
|
it "doesn't bump topic if only staff-only tags are added" do
|
|
expect {
|
|
result =
|
|
post_revisor.revise!(
|
|
Fabricate(:admin),
|
|
raw: post.raw,
|
|
tags: post.topic.tags.map(&:name) + ["secret"],
|
|
)
|
|
expect(result).to eq(true)
|
|
}.to_not change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump topic if only staff-only tags are removed" do
|
|
expect {
|
|
result =
|
|
post_revisor.revise!(
|
|
Fabricate(:admin),
|
|
raw: post.raw,
|
|
tags: post.topic.tags.map(&:name) - %w[important secret],
|
|
)
|
|
expect(result).to eq(true)
|
|
}.to_not change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump topic if only staff-only tags are removed and there are no tags left" do
|
|
post.topic.tags = Tag.where(name: %w[important secret]).to_a
|
|
expect {
|
|
result = post_revisor.revise!(Fabricate(:admin), raw: post.raw, tags: [])
|
|
expect(result).to eq(true)
|
|
}.to_not change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump topic if empty string is given" do
|
|
post.topic.tags = Tag.where(name: %w[important secret]).to_a
|
|
expect {
|
|
result = post_revisor.revise!(Fabricate(:admin), raw: post.raw, tags: [""])
|
|
expect(result).to eq(true)
|
|
}.to_not change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump topic if non staff-only tags are added" do
|
|
expect {
|
|
result =
|
|
post_revisor.revise!(
|
|
Fabricate(:admin),
|
|
raw: post.raw,
|
|
tags: post.topic.tags.map(&:name) + [Fabricate(:tag).name],
|
|
)
|
|
expect(result).to eq(true)
|
|
}.not_to change { post.topic.reload.bumped_at }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "draft cleanup" do
|
|
fab!(:post)
|
|
|
|
it "deletes the draft after successful revision" do
|
|
draft_key = post.topic.draft_key
|
|
Draft.set(post.user, draft_key, 0, '{"reply":"test draft"}')
|
|
|
|
expect(Draft.find_by(user_id: post.user.id, draft_key: draft_key)).to be_present
|
|
|
|
post.revise(post.user, raw: "updated content here for the test")
|
|
|
|
expect(Draft.find_by(user_id: post.user.id, draft_key: draft_key)).to be_nil
|
|
end
|
|
|
|
it "deletes the draft even when draft sequence exceeds DraftSequence" do
|
|
draft_key = post.topic.draft_key
|
|
|
|
# Simulate edge case: draft sequence is higher than DraftSequence
|
|
# When DraftSequence.next! runs, it increments to 6, but sequence < 6 doesn't catch sequence 6
|
|
Draft.create!(user: post.user, draft_key: draft_key, data: '{"reply":"test"}', sequence: 6)
|
|
DraftSequence.create!(user_id: post.user.id, draft_key: draft_key, sequence: 5)
|
|
|
|
expect(Draft.find_by(user_id: post.user.id, draft_key: draft_key)).to be_present
|
|
|
|
post.revise(post.user, raw: "updated content here for the test")
|
|
|
|
expect(Draft.find_by(user_id: post.user.id, draft_key: draft_key)).to be_nil
|
|
end
|
|
end
|
|
|
|
describe "revising reply_to_post_number" do
|
|
fab!(:topic)
|
|
fab!(:op_author, :user)
|
|
fab!(:first_reply_author, :user)
|
|
fab!(:second_reply_author, :user)
|
|
fab!(:editor) { Fabricate(:user, refresh_auto_groups: true) }
|
|
|
|
fab!(:original_post) { Fabricate(:post, topic: topic, user: op_author, post_number: 1) }
|
|
fab!(:first_reply) do
|
|
PostCreator.create!(
|
|
first_reply_author,
|
|
topic_id: topic.id,
|
|
raw: "first reply body, long enough to be valid",
|
|
reply_to_post_number: 1,
|
|
)
|
|
end
|
|
fab!(:second_reply) do
|
|
PostCreator.create!(
|
|
second_reply_author,
|
|
topic_id: topic.id,
|
|
raw: "second reply body, long enough to be valid",
|
|
reply_to_post_number: 1,
|
|
)
|
|
end
|
|
fab!(:post_to_edit) do
|
|
PostCreator.create!(
|
|
editor,
|
|
topic_id: topic.id,
|
|
raw: "my reply body, long enough to be valid",
|
|
reply_to_post_number: 1,
|
|
)
|
|
end
|
|
|
|
def revise(value, opts = {})
|
|
PostRevisor.new(post_to_edit).revise!(
|
|
editor,
|
|
{ reply_to_post_number: value },
|
|
{ bypass_rate_limiter: true }.merge(opts),
|
|
)
|
|
end
|
|
|
|
it "reparents to another earlier post and syncs reply_to_user_id" do
|
|
original_parent_reply_count = original_post.reload.reply_count
|
|
|
|
expect(revise(first_reply.post_number)).to eq(true)
|
|
|
|
post_to_edit.reload
|
|
expect(post_to_edit.reply_to_post_number).to eq(first_reply.post_number)
|
|
expect(post_to_edit.reply_to_user_id).to eq(first_reply.user_id)
|
|
expect(first_reply.reload.reply_count).to eq(1)
|
|
expect(original_post.reload.reply_count).to eq(original_parent_reply_count - 1)
|
|
end
|
|
|
|
it "removes the reply relationship when set to nil" do
|
|
expect(revise(nil)).to eq(true)
|
|
|
|
post_to_edit.reload
|
|
expect(post_to_edit.reply_to_post_number).to be_nil
|
|
expect(post_to_edit.reply_to_user_id).to be_nil
|
|
expect(PostReply.where(post_id: original_post.id, reply_post_id: post_to_edit.id)).to be_empty
|
|
end
|
|
|
|
it "tracks the change in a PostRevision" do
|
|
revise(first_reply.post_number, force_new_version: true)
|
|
|
|
revision = post_to_edit.post_revisions.order(:number).last
|
|
expect(revision.modifications["reply_to_post_number"]).to eq([1, first_reply.post_number])
|
|
end
|
|
|
|
it "does not create a revision when the value is unchanged" do
|
|
expect { revise(post_to_edit.reply_to_post_number) }.not_to change {
|
|
post_to_edit.post_revisions.count
|
|
}
|
|
end
|
|
|
|
it "rejects a self-reference" do
|
|
expect(revise(post_to_edit.post_number)).to eq(false)
|
|
expect(post_to_edit.errors[:reply_to_post_number]).to be_present
|
|
expect(post_to_edit.reload.reply_to_post_number).to eq(1)
|
|
end
|
|
|
|
it "rejects a later post in the topic" do
|
|
later_post =
|
|
PostCreator.create!(
|
|
first_reply_author,
|
|
topic_id: topic.id,
|
|
raw: "a later post, long enough to be valid",
|
|
)
|
|
expect(revise(later_post.post_number)).to eq(false)
|
|
expect(post_to_edit.errors[:reply_to_post_number]).to be_present
|
|
end
|
|
|
|
it "rejects a post that does not exist in the topic" do
|
|
expect(revise(999)).to eq(false)
|
|
expect(post_to_edit.errors[:reply_to_post_number]).to be_present
|
|
end
|
|
|
|
it "rejects a deleted post" do
|
|
first_reply.trash!
|
|
expect(revise(first_reply.post_number)).to eq(false)
|
|
expect(post_to_edit.errors[:reply_to_post_number]).to be_present
|
|
end
|
|
|
|
it "keeps the PostReply row for the old parent if the post still quotes it" do
|
|
post_to_edit.update!(
|
|
raw: "quoting\n[quote=\"#{op_author.username}, post:1, topic:#{topic.id}\"]hi[/quote]",
|
|
)
|
|
post_to_edit.extract_quoted_post_numbers
|
|
post_to_edit.save!
|
|
|
|
expect(revise(first_reply.post_number)).to eq(true)
|
|
expect(
|
|
PostReply.where(post_id: original_post.id, reply_post_id: post_to_edit.id),
|
|
).to be_present
|
|
end
|
|
|
|
it "rejects a target the editor cannot see" do
|
|
SiteSetting.whispers_allowed_groups = Group::AUTO_GROUPS[:staff]
|
|
whisper =
|
|
PostCreator.create!(
|
|
admin,
|
|
topic_id: topic.id,
|
|
raw: "a whisper the editor cannot see",
|
|
post_type: Post.types[:whisper],
|
|
)
|
|
|
|
expect(revise(whisper.post_number)).to eq(false)
|
|
expect(post_to_edit.errors[:reply_to_post_number]).to be_present
|
|
end
|
|
|
|
it "does not create new PostReply rows when the post save fails" do
|
|
SiteSetting.min_post_length = 500
|
|
new_parent_reply_count = first_reply.reload.reply_count
|
|
|
|
result =
|
|
PostRevisor.new(post_to_edit).revise!(
|
|
editor,
|
|
{ raw: "too short", reply_to_post_number: first_reply.post_number },
|
|
bypass_rate_limiter: true,
|
|
)
|
|
|
|
expect(result).to eq(false)
|
|
expect(PostReply.where(post_id: first_reply.id, reply_post_id: post_to_edit.id)).to be_empty
|
|
expect(first_reply.reload.reply_count).to eq(new_parent_reply_count)
|
|
end
|
|
|
|
it "cleans up the PostReply row when the previous parent is already trashed" do
|
|
original_post.trash!
|
|
|
|
expect(
|
|
PostReply.where(post_id: original_post.id, reply_post_id: post_to_edit.id),
|
|
).to be_present
|
|
|
|
expect(revise(first_reply.post_number)).to eq(true)
|
|
|
|
expect(PostReply.where(post_id: original_post.id, reply_post_id: post_to_edit.id)).to be_empty
|
|
end
|
|
|
|
context "with nested reply stats" do
|
|
def direct_reply_count(post)
|
|
NestedViewPostStat.where(post_id: post.id).pick(:direct_reply_count) || 0
|
|
end
|
|
|
|
def total_descendant_count(post)
|
|
NestedViewPostStat.where(post_id: post.id).pick(:total_descendant_count) || 0
|
|
end
|
|
|
|
it "moves the subtree between ancestor chains on reparent" do
|
|
SiteSetting.nested_replies_enabled = true
|
|
|
|
nested_op = Fabricate(:post, topic: topic, user: op_author)
|
|
nested_reparent_target =
|
|
PostCreator.create!(
|
|
first_reply_author,
|
|
topic_id: topic.id,
|
|
raw: "reparent target, long enough to be valid",
|
|
reply_to_post_number: nested_op.post_number,
|
|
)
|
|
moved_post =
|
|
PostCreator.create!(
|
|
editor,
|
|
topic_id: topic.id,
|
|
raw: "the one we'll reparent, long enough",
|
|
reply_to_post_number: nested_op.post_number,
|
|
)
|
|
|
|
# Before: nested_op has two direct children (target + moved_post).
|
|
expect(direct_reply_count(nested_op)).to eq(2)
|
|
expect(total_descendant_count(nested_op)).to eq(2)
|
|
expect(direct_reply_count(nested_reparent_target)).to eq(0)
|
|
expect(total_descendant_count(nested_reparent_target)).to eq(0)
|
|
|
|
result =
|
|
PostRevisor.new(moved_post).revise!(
|
|
editor,
|
|
{ reply_to_post_number: nested_reparent_target.post_number },
|
|
bypass_rate_limiter: true,
|
|
)
|
|
|
|
expect(result).to eq(true)
|
|
# After: moved_post now lives under target. nested_op stays a shared
|
|
# ancestor (2 total descendants) but loses a direct child; target
|
|
# gains one direct + one total.
|
|
expect(direct_reply_count(nested_op)).to eq(1)
|
|
expect(total_descendant_count(nested_op)).to eq(2)
|
|
expect(direct_reply_count(nested_reparent_target)).to eq(1)
|
|
expect(total_descendant_count(nested_reparent_target)).to eq(1)
|
|
end
|
|
|
|
it "moves the subtree up when the new target is the current grandparent" do
|
|
SiteSetting.nested_replies_enabled = true
|
|
|
|
nested_op = Fabricate(:post, topic: topic, user: op_author)
|
|
middle =
|
|
PostCreator.create!(
|
|
first_reply_author,
|
|
topic_id: topic.id,
|
|
raw: "middle, long enough to be valid",
|
|
reply_to_post_number: nested_op.post_number,
|
|
)
|
|
moved_post =
|
|
PostCreator.create!(
|
|
editor,
|
|
topic_id: topic.id,
|
|
raw: "will move up, long enough",
|
|
reply_to_post_number: middle.post_number,
|
|
)
|
|
|
|
expect(direct_reply_count(nested_op)).to eq(1)
|
|
expect(total_descendant_count(nested_op)).to eq(2)
|
|
expect(direct_reply_count(middle)).to eq(1)
|
|
expect(total_descendant_count(middle)).to eq(1)
|
|
|
|
PostRevisor.new(moved_post).revise!(
|
|
editor,
|
|
{ reply_to_post_number: nested_op.post_number },
|
|
bypass_rate_limiter: true,
|
|
)
|
|
|
|
# middle loses its only descendant; nested_op's total stays at 2,
|
|
# its direct child count goes from 1 to 2.
|
|
expect(direct_reply_count(middle)).to eq(0)
|
|
expect(total_descendant_count(middle)).to eq(0)
|
|
expect(direct_reply_count(nested_op)).to eq(2)
|
|
expect(total_descendant_count(nested_op)).to eq(2)
|
|
end
|
|
end
|
|
end
|
|
end
|