mirror of
https://github.com/discourse/discourse-solved.git
synced 2026-09-17 15:02:13 -04:00
* FEATURE: change status on unsolve & fix assign changes When a topic is unsolved, it should have an option, defined in the settings, to change its status to that state. Fix assign changes when a topic was solved, previously it was changing the assignee. * DEV: Change names in tests and remove comments * DEV: Update change status on solve implementation Update tests to verify that the change status on solve feature is working as expected. Change the implementation to loop throught the topic assignments and update the status. * DEV: address review feedback
535 lines
18 KiB
Ruby
535 lines
18 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "rails_helper"
|
|
|
|
RSpec.describe "Managing Posts solved status" do
|
|
let(:topic) { Fabricate(:topic) }
|
|
fab!(:user) { Fabricate(:trust_level_4) }
|
|
let(:p1) { Fabricate(:post, topic: topic) }
|
|
|
|
before { SiteSetting.allow_solved_on_all_topics = true }
|
|
|
|
describe "customer filters" do
|
|
before do
|
|
SiteSetting.allow_solved_on_all_topics = false
|
|
SiteSetting.enable_solved_tags = solvable_tag.name
|
|
end
|
|
|
|
fab!(:solvable_category) do
|
|
category = Fabricate(:category)
|
|
|
|
CategoryCustomField.create(
|
|
category_id: category.id,
|
|
name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD,
|
|
value: "true",
|
|
)
|
|
|
|
category
|
|
end
|
|
|
|
fab!(:solvable_tag) { Fabricate(:tag) }
|
|
|
|
fab!(:solved_in_category) do
|
|
Fabricate(
|
|
:custom_topic,
|
|
category: solvable_category,
|
|
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
|
|
value: "42",
|
|
)
|
|
end
|
|
|
|
fab!(:solved_in_tag) do
|
|
Fabricate(
|
|
:custom_topic,
|
|
tags: [solvable_tag],
|
|
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
|
|
value: "42",
|
|
)
|
|
end
|
|
|
|
fab!(:unsolved_in_category) { Fabricate(:topic, category: solvable_category) }
|
|
fab!(:unsolved_in_tag) { Fabricate(:topic, tags: [solvable_tag]) }
|
|
|
|
fab!(:unsolved_topic) { Fabricate(:topic) }
|
|
|
|
it "can filter by solved status" do
|
|
expect(
|
|
TopicsFilter
|
|
.new(guardian: Guardian.new)
|
|
.filter_from_query_string("status:solved")
|
|
.pluck(:id),
|
|
).to contain_exactly(solved_in_category.id, solved_in_tag.id)
|
|
end
|
|
|
|
it "can filter by unsolved status" do
|
|
expect(
|
|
TopicsFilter
|
|
.new(guardian: Guardian.new)
|
|
.filter_from_query_string("status:unsolved")
|
|
.pluck(:id),
|
|
).to contain_exactly(unsolved_in_category.id, unsolved_in_tag.id)
|
|
end
|
|
end
|
|
|
|
describe "search" do
|
|
before { SearchIndexer.enable }
|
|
|
|
after { SearchIndexer.disable }
|
|
|
|
it "can prioritize solved topics in search" do
|
|
normal_post =
|
|
Fabricate(
|
|
:post,
|
|
raw: "My reply carrot",
|
|
topic: Fabricate(:topic, title: "A topic that is not solved but open"),
|
|
)
|
|
|
|
solved_post =
|
|
Fabricate(
|
|
:post,
|
|
raw: "My solution carrot",
|
|
topic: Fabricate(:topic, title: "A topic that will be closed", closed: true),
|
|
)
|
|
|
|
DiscourseSolved.accept_answer!(solved_post, Discourse.system_user)
|
|
|
|
result = Search.execute("carrot")
|
|
expect(result.posts.pluck(:id)).to eq([normal_post.id, solved_post.id])
|
|
|
|
SiteSetting.prioritize_solved_topics_in_search = true
|
|
|
|
result = Search.execute("carrot")
|
|
expect(result.posts.pluck(:id)).to eq([solved_post.id, normal_post.id])
|
|
end
|
|
|
|
describe "#advanced_search" do
|
|
fab!(:category_enabled) do
|
|
category = Fabricate(:category)
|
|
category_custom_field =
|
|
CategoryCustomField.new(
|
|
category_id: category.id,
|
|
name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD,
|
|
value: "true",
|
|
)
|
|
category_custom_field.save
|
|
category
|
|
end
|
|
fab!(:category_disabled) do
|
|
category = Fabricate(:category)
|
|
category_custom_field =
|
|
CategoryCustomField.new(
|
|
category_id: category.id,
|
|
name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD,
|
|
value: "false",
|
|
)
|
|
category_custom_field.save
|
|
category
|
|
end
|
|
fab!(:tag)
|
|
fab!(:topic_unsolved) do
|
|
Fabricate(
|
|
:custom_topic,
|
|
user: user,
|
|
category: category_enabled,
|
|
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
|
|
)
|
|
end
|
|
fab!(:topic_unsolved_2) { Fabricate(:topic, user: user, tags: [tag]) }
|
|
fab!(:topic_solved) do
|
|
Fabricate(
|
|
:custom_topic,
|
|
user: user,
|
|
category: category_enabled,
|
|
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
|
|
)
|
|
end
|
|
fab!(:topic_disabled_1) do
|
|
Fabricate(
|
|
:custom_topic,
|
|
user: user,
|
|
category: category_disabled,
|
|
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
|
|
)
|
|
end
|
|
fab!(:topic_disabled_2) do
|
|
Fabricate(
|
|
:custom_topic,
|
|
user: user,
|
|
category: category_disabled,
|
|
custom_topic_name: "another_custom_field",
|
|
)
|
|
end
|
|
fab!(:post_unsolved) { Fabricate(:post, topic: topic_unsolved) }
|
|
fab!(:post_unsolved_2) { Fabricate(:post, topic: topic_unsolved_2) }
|
|
fab!(:post_solved) do
|
|
post = Fabricate(:post, topic: topic_solved)
|
|
DiscourseSolved.accept_answer!(post, Discourse.system_user)
|
|
post
|
|
end
|
|
fab!(:post_disabled_1) { Fabricate(:post, topic: topic_disabled_1) }
|
|
fab!(:post_disabled_2) { Fabricate(:post, topic: topic_disabled_2) }
|
|
|
|
before do
|
|
SiteSetting.enable_solved_tags = tag.name
|
|
SearchIndexer.enable
|
|
Jobs.run_immediately!
|
|
|
|
SearchIndexer.index(topic_unsolved, force: true)
|
|
SearchIndexer.index(topic_unsolved_2, force: true)
|
|
SearchIndexer.index(topic_solved, force: true)
|
|
SearchIndexer.index(topic_disabled_1, force: true)
|
|
SearchIndexer.index(topic_disabled_2, force: true)
|
|
end
|
|
|
|
after { SearchIndexer.disable }
|
|
|
|
describe "searches for unsolved topics" do
|
|
describe "when allow solved on all topics is disabled" do
|
|
before { SiteSetting.allow_solved_on_all_topics = false }
|
|
|
|
it "only returns unsolved posts from categories and tags where solving is enabled" do
|
|
result = Search.execute("status:unsolved")
|
|
expect(result.posts.pluck(:id)).to match_array([post_unsolved.id, post_unsolved_2.id])
|
|
end
|
|
|
|
it "returns the filtered results when combining search with a tag" do
|
|
result = Search.execute("status:unsolved tag:#{tag.name}")
|
|
expect(result.posts.pluck(:id)).to match_array([post_unsolved_2.id])
|
|
end
|
|
end
|
|
|
|
describe "when allow solved on all topics is enabled" do
|
|
before { SiteSetting.allow_solved_on_all_topics = true }
|
|
it "only returns posts where the post is not solved" do
|
|
result = Search.execute("status:unsolved")
|
|
expect(result.posts.pluck(:id)).to match_array(
|
|
[post_unsolved.id, post_unsolved_2.id, post_disabled_1.id, post_disabled_2.id],
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "auto bump" do
|
|
it "does not automatically bump solved topics" do
|
|
category = Fabricate(:category_with_definition)
|
|
|
|
post = create_post(category: category)
|
|
post2 = create_post(category: category)
|
|
|
|
DiscourseSolved.accept_answer!(post, Discourse.system_user)
|
|
|
|
category.num_auto_bump_daily = 2
|
|
category.save!
|
|
|
|
freeze_time 1.month.from_now
|
|
|
|
expect(category.auto_bump_topic!).to eq(true)
|
|
|
|
freeze_time 13.hours.from_now
|
|
|
|
expect(category.auto_bump_topic!).to eq(false)
|
|
|
|
expect(post.topic.reload.posts_count).to eq(1)
|
|
expect(post2.topic.reload.posts_count).to eq(2)
|
|
end
|
|
end
|
|
|
|
describe "accepting a post as the answer" do
|
|
before do
|
|
sign_in(user)
|
|
SiteSetting.solved_topics_auto_close_hours = 2
|
|
end
|
|
|
|
it "can mark a post as the accepted answer correctly" do
|
|
freeze_time
|
|
|
|
post "/solution/accept.json", params: { id: p1.id }
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(p1.reload.custom_fields["is_accepted_answer"]).to eq("true")
|
|
|
|
topic.reload
|
|
|
|
expect(topic.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
|
|
|
|
expect(topic.custom_fields["solved_auto_close_topic_timer_id"].to_i).to eq(
|
|
topic.public_topic_timer.id,
|
|
)
|
|
|
|
expect(topic.public_topic_timer.execute_at).to eq_time(Time.zone.now + 2.hours)
|
|
|
|
expect(topic.public_topic_timer.based_on_last_post).to eq(true)
|
|
end
|
|
|
|
it "gives priority to category's solved_topics_auto_close_hours setting" do
|
|
freeze_time
|
|
custom_auto_close_category = Fabricate(:category)
|
|
topic_2 = Fabricate(:topic, category: custom_auto_close_category)
|
|
post_2 = Fabricate(:post, topic: topic_2)
|
|
custom_auto_close_category.custom_fields["solved_topics_auto_close_hours"] = 4
|
|
custom_auto_close_category.save_custom_fields
|
|
|
|
post "/solution/accept.json", params: { id: post_2.id }
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(post_2.reload.custom_fields["is_accepted_answer"]).to eq("true")
|
|
|
|
topic_2.reload
|
|
|
|
expect(topic_2.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
|
|
|
|
expect(topic_2.custom_fields["solved_auto_close_topic_timer_id"].to_i).to eq(
|
|
topic_2.public_topic_timer.id,
|
|
)
|
|
|
|
expect(topic_2.public_topic_timer.execute_at).to eq_time(Time.zone.now + 4.hours)
|
|
|
|
expect(topic_2.public_topic_timer.based_on_last_post).to eq(true)
|
|
end
|
|
|
|
it "sends notifications to correct users" do
|
|
SiteSetting.notify_on_staff_accept_solved = true
|
|
user = Fabricate(:user)
|
|
topic = Fabricate(:topic, user: user)
|
|
post = Fabricate(:post, post_number: 2, topic: topic)
|
|
|
|
op = topic.user
|
|
user = post.user
|
|
|
|
expect { DiscourseSolved.accept_answer!(post, Discourse.system_user) }.to change {
|
|
user.notifications.count
|
|
}.by(1) & change { op.notifications.count }.by(1)
|
|
|
|
notification = user.notifications.last
|
|
expect(notification.notification_type).to eq(Notification.types[:custom])
|
|
expect(notification.topic_id).to eq(post.topic_id)
|
|
expect(notification.post_number).to eq(post.post_number)
|
|
|
|
notification = op.notifications.last
|
|
expect(notification.notification_type).to eq(Notification.types[:custom])
|
|
expect(notification.topic_id).to eq(post.topic_id)
|
|
expect(notification.post_number).to eq(post.post_number)
|
|
end
|
|
|
|
it "does not set a timer when the topic is closed" do
|
|
topic.update!(closed: true)
|
|
post "/solution/accept.json", params: { id: p1.id }
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
p1.reload
|
|
topic.reload
|
|
|
|
expect(p1.custom_fields["is_accepted_answer"]).to eq("true")
|
|
expect(topic.public_topic_timer).to eq(nil)
|
|
expect(topic.closed).to eq(true)
|
|
end
|
|
|
|
it "works with staff and trashed topics" do
|
|
topic.trash!(Discourse.system_user)
|
|
|
|
post "/solution/accept.json", params: { id: p1.id }
|
|
expect(response.status).to eq(403)
|
|
|
|
sign_in(Fabricate(:admin))
|
|
post "/solution/accept.json", params: { id: p1.id }
|
|
expect(response.status).to eq(200)
|
|
|
|
p1.reload
|
|
expect(p1.custom_fields["is_accepted_answer"]).to eq("true")
|
|
end
|
|
|
|
it "removes the solution when the post is deleted" do
|
|
reply = Fabricate(:post, post_number: 2, topic: topic)
|
|
|
|
post "/solution/accept.json", params: { id: reply.id }
|
|
expect(response.status).to eq(200)
|
|
|
|
reply.reload
|
|
expect(reply.custom_fields["is_accepted_answer"]).to eq("true")
|
|
expect(reply.topic.custom_fields["accepted_answer_post_id"].to_i).to eq(reply.id)
|
|
|
|
PostDestroyer.new(Discourse.system_user, reply).destroy
|
|
|
|
reply.reload
|
|
expect(reply.custom_fields["is_accepted_answer"]).to eq(nil)
|
|
expect(reply.topic.custom_fields["accepted_answer_post_id"]).to eq(nil)
|
|
end
|
|
|
|
it "does not allow you to accept a whisper" do
|
|
whisper = Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
|
|
sign_in(Fabricate(:admin))
|
|
|
|
post "/solution/accept.json", params: { id: whisper.id }
|
|
expect(response.status).to eq(403)
|
|
end
|
|
|
|
it "triggers a webhook" do
|
|
Fabricate(:solved_web_hook)
|
|
post "/solution/accept.json", params: { id: p1.id }
|
|
|
|
job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first
|
|
|
|
expect(job_args["event_name"]).to eq("accepted_solution")
|
|
payload = JSON.parse(job_args["payload"])
|
|
expect(payload["id"]).to eq(p1.id)
|
|
end
|
|
end
|
|
|
|
describe "#unaccept" do
|
|
before { sign_in(user) }
|
|
|
|
describe "when solved_topics_auto_close_hours is enabled" do
|
|
before do
|
|
SiteSetting.solved_topics_auto_close_hours = 2
|
|
DiscourseSolved.accept_answer!(p1, user)
|
|
end
|
|
|
|
it "should unmark the post as solved" do
|
|
expect do post "/solution/unaccept.json", params: { id: p1.id } end.to change {
|
|
topic.reload.public_topic_timer
|
|
}.to(nil)
|
|
|
|
expect(response.status).to eq(200)
|
|
p1.reload
|
|
|
|
expect(p1.custom_fields["is_accepted_answer"]).to eq(nil)
|
|
expect(p1.topic.custom_fields["accepted_answer_post_id"]).to eq(nil)
|
|
end
|
|
end
|
|
|
|
it "triggers a webhook" do
|
|
Fabricate(:solved_web_hook)
|
|
post "/solution/unaccept.json", params: { id: p1.id }
|
|
|
|
job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first
|
|
|
|
expect(job_args["event_name"]).to eq("unaccepted_solution")
|
|
payload = JSON.parse(job_args["payload"])
|
|
expect(payload["id"]).to eq(p1.id)
|
|
end
|
|
end
|
|
|
|
context "with group moderators" do
|
|
fab!(:group_user)
|
|
let(:user_gm) { group_user.user }
|
|
let(:group) { group_user.group }
|
|
|
|
before do
|
|
SiteSetting.enable_category_group_moderation = true
|
|
p1.topic.category.update!(reviewable_by_group_id: group.id)
|
|
sign_in(user_gm)
|
|
end
|
|
|
|
it "can accept a solution" do
|
|
post "/solution/accept.json", params: { id: p1.id }
|
|
expect(response.status).to eq(200)
|
|
end
|
|
end
|
|
|
|
context "with discourse-assign installed", if: defined?(DiscourseAssign) do
|
|
let(:admin) { Fabricate(:admin) }
|
|
fab!(:group)
|
|
before do
|
|
SiteSetting.solved_enabled = true
|
|
SiteSetting.assign_enabled = true
|
|
SiteSetting.enable_assign_status = true
|
|
SiteSetting.assign_allowed_on_groups = "#{group.id}"
|
|
SiteSetting.assigns_public = true
|
|
SiteSetting.assignment_status_on_solve = "Done"
|
|
SiteSetting.assignment_status_on_unsolve = "New"
|
|
SiteSetting.ignore_solved_topics_in_assigned_reminder = false
|
|
group.add(p1.acting_user)
|
|
group.add(user)
|
|
|
|
sign_in(user)
|
|
end
|
|
describe "updating assignment status on solve when assignment_status_on_solve is set" do
|
|
it "update all assignments to this status when a post is accepted" do
|
|
assigner = Assigner.new(p1.topic, user)
|
|
result = assigner.assign(user)
|
|
expect(result[:success]).to eq(true)
|
|
|
|
expect(p1.topic.assignment.status).to eq("New")
|
|
DiscourseSolved.accept_answer!(p1, user)
|
|
|
|
expect(p1.reload.custom_fields["is_accepted_answer"]).to eq("true")
|
|
expect(p1.topic.assignment.reload.status).to eq("Done")
|
|
end
|
|
|
|
it "update all assignments to this status when a post is unaccepted" do
|
|
assigner = Assigner.new(p1.topic, user)
|
|
result = assigner.assign(user)
|
|
expect(result[:success]).to eq(true)
|
|
|
|
DiscourseSolved.accept_answer!(p1, user)
|
|
|
|
expect(p1.reload.topic.assignment.reload.status).to eq("Done")
|
|
|
|
DiscourseSolved.unaccept_answer!(p1)
|
|
|
|
expect(p1.reload.custom_fields["is_accepted_answer"]).to eq(nil)
|
|
expect(p1.reload.topic.assignment.reload.status).to eq("New")
|
|
end
|
|
|
|
it "does not update the assignee when a post is accepted" do
|
|
user_1 = Fabricate(:user)
|
|
user_2 = Fabricate(:user)
|
|
user_3 = Fabricate(:user)
|
|
group.add(user_1)
|
|
group.add(user_2)
|
|
group.add(user_3)
|
|
|
|
topic_question = Fabricate(:topic, user: user_1)
|
|
post_question = Fabricate(:post, topic: topic_question, user: user_1)
|
|
|
|
user_2_response = Fabricate(:post, topic: topic_question, user: user_2)
|
|
assigner = Assigner.new(topic_question, user_2)
|
|
result = assigner.assign(user_2)
|
|
expect(result[:success]).to eq(true)
|
|
|
|
post_response = Fabricate(:post, topic: topic_question, user: user_3)
|
|
Assigner.new(post_response, user_3).assign(user_3)
|
|
|
|
DiscourseSolved.accept_answer!(post_response, user_1)
|
|
|
|
expect(topic_question.assignment.assigned_to_id).to eq(user_2.id)
|
|
expect(post_response.assignment.assigned_to_id).to eq(user_3.id)
|
|
DiscourseSolved.unaccept_answer!(post_response)
|
|
|
|
expect(topic_question.assignment.assigned_to_id).to eq(user_2.id)
|
|
expect(post_response.assignment.assigned_to_id).to eq(user_3.id)
|
|
end
|
|
|
|
describe "assigned topic reminder"
|
|
it "excludes solved topics when ignore_solved_topics_in_assigned_reminder is false" do
|
|
other_topic = Fabricate(:topic, title: "Topic that should be there")
|
|
post = Fabricate(:post, topic: other_topic, user: user)
|
|
|
|
other_topic2 = Fabricate(:topic, title: "Topic that should be there2")
|
|
post2 = Fabricate(:post, topic: other_topic2, user: user)
|
|
|
|
Assigner.new(post.topic, user).assign(user)
|
|
Assigner.new(post2.topic, user).assign(user)
|
|
|
|
reminder = PendingAssignsReminder.new
|
|
topics = reminder.send(:assigned_topics, user, order: :asc)
|
|
expect(topics.to_a.length).to eq(2)
|
|
|
|
DiscourseSolved.accept_answer!(post2, Discourse.system_user)
|
|
topics = reminder.send(:assigned_topics, user, order: :asc)
|
|
expect(topics.to_a.length).to eq(2)
|
|
expect(topics).to include(other_topic2)
|
|
|
|
SiteSetting.ignore_solved_topics_in_assigned_reminder = true
|
|
topics = reminder.send(:assigned_topics, user, order: :asc)
|
|
expect(topics.to_a.length).to eq(1)
|
|
expect(topics).not_to include(other_topic2)
|
|
expect(topics).to include(other_topic)
|
|
end
|
|
end
|
|
end
|
|
end
|