FIX: delete solution with post (#256)

Ensures we remove the solution when the post marked as the solution is deleted.

DEV: Added `IS_ACCEPTED_ANSWER_CUSTOM_FIELD` constant.
DEV: Refactored the `PostSerializer` for better readability.
PERF: Improved the `TopicViewSerializer`'s performance by looking up the `accepted_answer_post_info` from the stream first.

Internal ref. dev/112251
This commit is contained in:
Régis Hanol 2023-10-13 19:06:03 +02:00 committed by GitHub
parent 29bf44807f
commit b269689ddf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 60 additions and 41 deletions

View File

@ -6,7 +6,7 @@ module TopicAnswerMixin
end end
def has_accepted_answer def has_accepted_answer
object.custom_fields["accepted_answer_post_id"] ? true : false object.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present?
end end
def include_has_accepted_answer? def include_has_accepted_answer?

View File

@ -77,6 +77,7 @@ SQL
AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD = "solved_auto_close_topic_timer_id" AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD = "solved_auto_close_topic_timer_id"
ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD = "accepted_answer_post_id" ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD = "accepted_answer_post_id"
ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD = "enable_accepted_answers" ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD = "enable_accepted_answers"
IS_ACCEPTED_ANSWER_CUSTOM_FIELD = "is_accepted_answer"
def self.accept_answer!(post, acting_user, topic: nil) def self.accept_answer!(post, acting_user, topic: nil)
topic ||= post.topic topic ||= post.topic
@ -86,7 +87,7 @@ SQL
if accepted_id > 0 if accepted_id > 0
if p2 = Post.find_by(id: accepted_id) if p2 = Post.find_by(id: accepted_id)
p2.custom_fields.delete("is_accepted_answer") p2.custom_fields.delete(IS_ACCEPTED_ANSWER_CUSTOM_FIELD)
p2.save! p2.save!
if defined?(UserAction::SOLVED) if defined?(UserAction::SOLVED)
@ -95,7 +96,7 @@ SQL
end end
end end
post.custom_fields["is_accepted_answer"] = "true" post.custom_fields[IS_ACCEPTED_ANSWER_CUSTOM_FIELD] = "true"
topic.custom_fields[ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD] = post.id topic.custom_fields[ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD] = post.id
if defined?(UserAction::SOLVED) if defined?(UserAction::SOLVED)
@ -173,7 +174,7 @@ SQL
topic ||= post.topic topic ||= post.topic
DistributedMutex.synchronize("discourse_solved_toggle_answer_#{topic.id}") do DistributedMutex.synchronize("discourse_solved_toggle_answer_#{topic.id}") do
post.custom_fields.delete("is_accepted_answer") post.custom_fields.delete(IS_ACCEPTED_ANSWER_CUSTOM_FIELD)
topic.custom_fields.delete(ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD) topic.custom_fields.delete(ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD)
if timer_id = topic.custom_fields[AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD] if timer_id = topic.custom_fields[AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD]
@ -240,6 +241,7 @@ SQL
guardian.ensure_can_accept_answer!(topic, post) guardian.ensure_can_accept_answer!(topic, post)
DiscourseSolved.unaccept_answer!(post, topic: topic) DiscourseSolved.unaccept_answer!(post, topic: topic)
render json: success_json render json: success_json
end end
@ -257,12 +259,18 @@ SQL
Discourse::Application.routes.append { mount ::DiscourseSolved::Engine, at: "solution" } Discourse::Application.routes.append { mount ::DiscourseSolved::Engine, at: "solution" }
on(:post_destroyed) do |post|
if post.custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
::DiscourseSolved.unaccept_answer!(post)
end
end
add_api_key_scope( add_api_key_scope(
:solved, :solved,
{ answer: { actions: %w[discourse_solved/answer#accept discourse_solved/answer#unaccept] } }, { answer: { actions: %w[discourse_solved/answer#accept discourse_solved/answer#unaccept] } },
) )
topic_view_post_custom_fields_allowlister { ["is_accepted_answer"] } topic_view_post_custom_fields_allowlister { [::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] }
def get_schema_text(post) def get_schema_text(post)
post.excerpt(nil, keep_onebox_body: true).presence || post.excerpt(nil, keep_onebox_body: true).presence ||
@ -450,29 +458,27 @@ SQL
end end
def accepted_answer_post_info def accepted_answer_post_info
# TODO: we may already have it in the stream ... so bypass query here post_info =
postInfo = if post = object.posts.find { |p| p.post_number == accepted_answer_post_id }
Post [post.post_number, post.user.username, post.cooked, post.user.name]
.where(id: accepted_answer_post_id, topic_id: object.topic.id) else
.joins(:user) Post
.pluck("post_number", "username", "cooked", "name") .where(id: accepted_answer_post_id, topic_id: object.topic.id)
.first .joins(:user)
.pluck("post_number", "username", "cooked", "name")
.first
end
if postInfo if post_info
postInfo[2] = if SiteSetting.solved_quote_length > 0 post_info[2] = if SiteSetting.solved_quote_length > 0
PrettyText.excerpt(postInfo[2], SiteSetting.solved_quote_length, keep_emoji_images: true) PrettyText.excerpt(post_info[2], SiteSetting.solved_quote_length, keep_emoji_images: true)
else else
nil nil
end end
postInfo[3] = ( post_info[3] = nil if !SiteSetting.enable_names || !SiteSetting.display_name_on_posts
if SiteSetting.enable_names && SiteSetting.display_name_on_posts
postInfo[3] post_info
else
nil
end
)
postInfo
end end
end end
@ -543,32 +549,28 @@ SQL
end end
require_dependency "post_serializer" require_dependency "post_serializer"
class ::PostSerializer class ::PostSerializer
attributes :can_accept_answer, :can_unaccept_answer, :accepted_answer, :topic_accepted_answer attributes :can_accept_answer, :can_unaccept_answer, :accepted_answer, :topic_accepted_answer
def can_accept_answer def can_accept_answer
if topic = (topic_view && topic_view.topic) || object.topic scope.can_accept_answer?(topic, object) && object.post_number > 1 && !accepted_answer
return scope.can_accept_answer?(topic, object) && object.post_number > 1 && !accepted_answer
end
false
end end
def can_unaccept_answer def can_unaccept_answer
if topic = (topic_view && topic_view.topic) || object.topic scope.can_accept_answer?(topic, object) && accepted_answer
scope.can_accept_answer?(topic, object) &&
(post_custom_fields["is_accepted_answer"] == "true")
end
end end
def accepted_answer def accepted_answer
post_custom_fields["is_accepted_answer"] == "true" post_custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
end end
def topic_accepted_answer def topic_accepted_answer
if topic = (topic_view && topic_view.topic) || object.topic topic&.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present?
topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present? end
end
def topic
topic_view&.topic || object.topic
end end
end end
@ -682,7 +684,7 @@ SQL
end end
if CategoryList.respond_to?(:preloaded_topic_custom_fields) if CategoryList.respond_to?(:preloaded_topic_custom_fields)
CategoryList.preloaded_topic_custom_fields << "accepted_answer_post_id" CategoryList.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
end end
on(:filter_auto_bump_topics) { |_category, filters| filters.push(->(r) { r.where(<<~SQL) }) } on(:filter_auto_bump_topics) { |_category, filters| filters.push(->(r) { r.where(<<~SQL) }) }

View File

@ -181,7 +181,7 @@ RSpec.describe "Managing Posts solved status" do
expect(topic.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close]) expect(topic.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
expect(topic.custom_fields[DiscourseSolved::AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD].to_i).to eq( expect(topic.custom_fields["solved_auto_close_topic_timer_id"].to_i).to eq(
topic.public_topic_timer.id, topic.public_topic_timer.id,
) )
@ -207,9 +207,9 @@ RSpec.describe "Managing Posts solved status" do
expect(topic_2.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close]) expect(topic_2.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
expect( expect(topic_2.custom_fields["solved_auto_close_topic_timer_id"].to_i).to eq(
topic_2.custom_fields[DiscourseSolved::AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD].to_i, topic_2.public_topic_timer.id,
).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.execute_at).to eq_time(Time.zone.now + 4.hours)
@ -268,6 +268,23 @@ RSpec.describe "Managing Posts solved status" do
expect(p1.custom_fields["is_accepted_answer"]).to eq("true") expect(p1.custom_fields["is_accepted_answer"]).to eq("true")
end 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 it "does not allow you to accept a whisper" do
whisper = Fabricate(:post, topic: topic, post_type: Post.types[:whisper]) whisper = Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
sign_in(Fabricate(:admin)) sign_in(Fabricate(:admin))