Skip to content

Commit b269689

Browse files
authored
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
1 parent 29bf448 commit b269689

File tree

3 files changed

+61
-42
lines changed

3 files changed

+61
-42
lines changed

app/serializers/concerns/topic_answer_mixin.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def self.included(klass)
66
end
77

88
def has_accepted_answer
9-
object.custom_fields["accepted_answer_post_id"] ? true : false
9+
object.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present?
1010
end
1111

1212
def include_has_accepted_answer?

plugin.rb

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class Engine < ::Rails::Engine
7777
AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD = "solved_auto_close_topic_timer_id"
7878
ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD = "accepted_answer_post_id"
7979
ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD = "enable_accepted_answers"
80+
IS_ACCEPTED_ANSWER_CUSTOM_FIELD = "is_accepted_answer"
8081

8182
def self.accept_answer!(post, acting_user, topic: nil)
8283
topic ||= post.topic
@@ -86,7 +87,7 @@ def self.accept_answer!(post, acting_user, topic: nil)
8687

8788
if accepted_id > 0
8889
if p2 = Post.find_by(id: accepted_id)
89-
p2.custom_fields.delete("is_accepted_answer")
90+
p2.custom_fields.delete(IS_ACCEPTED_ANSWER_CUSTOM_FIELD)
9091
p2.save!
9192

9293
if defined?(UserAction::SOLVED)
@@ -95,7 +96,7 @@ def self.accept_answer!(post, acting_user, topic: nil)
9596
end
9697
end
9798

98-
post.custom_fields["is_accepted_answer"] = "true"
99+
post.custom_fields[IS_ACCEPTED_ANSWER_CUSTOM_FIELD] = "true"
99100
topic.custom_fields[ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD] = post.id
100101

101102
if defined?(UserAction::SOLVED)
@@ -173,7 +174,7 @@ def self.unaccept_answer!(post, topic: nil)
173174
topic ||= post.topic
174175

175176
DistributedMutex.synchronize("discourse_solved_toggle_answer_#{topic.id}") do
176-
post.custom_fields.delete("is_accepted_answer")
177+
post.custom_fields.delete(IS_ACCEPTED_ANSWER_CUSTOM_FIELD)
177178
topic.custom_fields.delete(ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD)
178179

179180
if timer_id = topic.custom_fields[AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD]
@@ -240,6 +241,7 @@ def unaccept
240241
guardian.ensure_can_accept_answer!(topic, post)
241242

242243
DiscourseSolved.unaccept_answer!(post, topic: topic)
244+
243245
render json: success_json
244246
end
245247

@@ -257,12 +259,18 @@ def limit_accepts
257259

258260
Discourse::Application.routes.append { mount ::DiscourseSolved::Engine, at: "solution" }
259261

262+
on(:post_destroyed) do |post|
263+
if post.custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
264+
::DiscourseSolved.unaccept_answer!(post)
265+
end
266+
end
267+
260268
add_api_key_scope(
261269
:solved,
262270
{ answer: { actions: %w[discourse_solved/answer#accept discourse_solved/answer#unaccept] } },
263271
)
264272

265-
topic_view_post_custom_fields_allowlister { ["is_accepted_answer"] }
273+
topic_view_post_custom_fields_allowlister { [::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] }
266274

267275
def get_schema_text(post)
268276
post.excerpt(nil, keep_onebox_body: true).presence ||
@@ -450,29 +458,27 @@ def accepted_answer
450458
end
451459

452460
def accepted_answer_post_info
453-
# TODO: we may already have it in the stream ... so bypass query here
454-
postInfo =
455-
Post
456-
.where(id: accepted_answer_post_id, topic_id: object.topic.id)
457-
.joins(:user)
458-
.pluck("post_number", "username", "cooked", "name")
459-
.first
460-
461-
if postInfo
462-
postInfo[2] = if SiteSetting.solved_quote_length > 0
463-
PrettyText.excerpt(postInfo[2], SiteSetting.solved_quote_length, keep_emoji_images: true)
461+
post_info =
462+
if post = object.posts.find { |p| p.post_number == accepted_answer_post_id }
463+
[post.post_number, post.user.username, post.cooked, post.user.name]
464+
else
465+
Post
466+
.where(id: accepted_answer_post_id, topic_id: object.topic.id)
467+
.joins(:user)
468+
.pluck("post_number", "username", "cooked", "name")
469+
.first
470+
end
471+
472+
if post_info
473+
post_info[2] = if SiteSetting.solved_quote_length > 0
474+
PrettyText.excerpt(post_info[2], SiteSetting.solved_quote_length, keep_emoji_images: true)
464475
else
465476
nil
466477
end
467478

468-
postInfo[3] = (
469-
if SiteSetting.enable_names && SiteSetting.display_name_on_posts
470-
postInfo[3]
471-
else
472-
nil
473-
end
474-
)
475-
postInfo
479+
post_info[3] = nil if !SiteSetting.enable_names || !SiteSetting.display_name_on_posts
480+
481+
post_info
476482
end
477483
end
478484

@@ -543,32 +549,28 @@ def can_accept_answer?(topic, post)
543549
end
544550

545551
require_dependency "post_serializer"
552+
546553
class ::PostSerializer
547554
attributes :can_accept_answer, :can_unaccept_answer, :accepted_answer, :topic_accepted_answer
548555

549556
def can_accept_answer
550-
if topic = (topic_view && topic_view.topic) || object.topic
551-
return scope.can_accept_answer?(topic, object) && object.post_number > 1 && !accepted_answer
552-
end
553-
554-
false
557+
scope.can_accept_answer?(topic, object) && object.post_number > 1 && !accepted_answer
555558
end
556559

557560
def can_unaccept_answer
558-
if topic = (topic_view && topic_view.topic) || object.topic
559-
scope.can_accept_answer?(topic, object) &&
560-
(post_custom_fields["is_accepted_answer"] == "true")
561-
end
561+
scope.can_accept_answer?(topic, object) && accepted_answer
562562
end
563563

564564
def accepted_answer
565-
post_custom_fields["is_accepted_answer"] == "true"
565+
post_custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
566566
end
567567

568568
def topic_accepted_answer
569-
if topic = (topic_view && topic_view.topic) || object.topic
570-
topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present?
571-
end
569+
topic&.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present?
570+
end
571+
572+
def topic
573+
topic_view&.topic || object.topic
572574
end
573575
end
574576

@@ -682,7 +684,7 @@ class ::ListableTopicSerializer
682684
end
683685

684686
if CategoryList.respond_to?(:preloaded_topic_custom_fields)
685-
CategoryList.preloaded_topic_custom_fields << "accepted_answer_post_id"
687+
CategoryList.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
686688
end
687689

688690
on(:filter_auto_bump_topics) { |_category, filters| filters.push(->(r) { r.where(<<~SQL) }) }

spec/integration/solved_spec.rb

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@
181181

182182
expect(topic.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
183183

184-
expect(topic.custom_fields[DiscourseSolved::AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD].to_i).to eq(
184+
expect(topic.custom_fields["solved_auto_close_topic_timer_id"].to_i).to eq(
185185
topic.public_topic_timer.id,
186186
)
187187

@@ -207,9 +207,9 @@
207207

208208
expect(topic_2.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
209209

210-
expect(
211-
topic_2.custom_fields[DiscourseSolved::AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD].to_i,
212-
).to eq(topic_2.public_topic_timer.id)
210+
expect(topic_2.custom_fields["solved_auto_close_topic_timer_id"].to_i).to eq(
211+
topic_2.public_topic_timer.id,
212+
)
213213

214214
expect(topic_2.public_topic_timer.execute_at).to eq_time(Time.zone.now + 4.hours)
215215

@@ -268,6 +268,23 @@
268268
expect(p1.custom_fields["is_accepted_answer"]).to eq("true")
269269
end
270270

271+
it "removes the solution when the post is deleted" do
272+
reply = Fabricate(:post, post_number: 2, topic: topic)
273+
274+
post "/solution/accept.json", params: { id: reply.id }
275+
expect(response.status).to eq(200)
276+
277+
reply.reload
278+
expect(reply.custom_fields["is_accepted_answer"]).to eq("true")
279+
expect(reply.topic.custom_fields["accepted_answer_post_id"].to_i).to eq(reply.id)
280+
281+
PostDestroyer.new(Discourse.system_user, reply).destroy
282+
283+
reply.reload
284+
expect(reply.custom_fields["is_accepted_answer"]).to eq(nil)
285+
expect(reply.topic.custom_fields["accepted_answer_post_id"]).to eq(nil)
286+
end
287+
271288
it "does not allow you to accept a whisper" do
272289
whisper = Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
273290
sign_in(Fabricate(:admin))

0 commit comments

Comments
 (0)