Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
15 changes: 15 additions & 0 deletions app/models/discourse_solved/solved_topic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module DiscourseSolved
class SolvedTopic < ActiveRecord::Base
self.table_name = "discourse_solved_solved_topics"

belongs_to :topic, class_name: "Topic"
belongs_to :answer_post, class_name: "Post", foreign_key: "answer_post_id"
belongs_to :accepter, class_name: "User", foreign_key: "accepter_user_id"
belongs_to :topic_timer

validates :topic_id, presence: true
validates :answer_post_id, presence: true
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def self.included(klass)
end

def has_accepted_answer
object.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present?
object&.solved.present?
end

def include_has_accepted_answer?
Expand Down
24 changes: 11 additions & 13 deletions db/fixtures/001_badges.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
first_solution_query = <<~SQL
SELECT post_id, user_id, created_at AS granted_at
FROM (
SELECT p.id AS post_id, p.user_id, pcf.created_at,
ROW_NUMBER() OVER (PARTITION BY p.user_id ORDER BY pcf.created_at) AS row_number
FROM post_custom_fields pcf
JOIN badge_posts p ON pcf.post_id = p.id
JOIN topics t ON p.topic_id = t.id
WHERE pcf.name = 'is_accepted_answer'
AND p.user_id <> t.user_id -- ignore topics solved by OP
AND (:backfill OR p.id IN (:post_ids))
SELECT p.id AS post_id, p.user_id, dsst.created_at,
ROW_NUMBER() OVER (PARTITION BY p.user_id ORDER BY dsst.created_at) AS row_number
FROM discourse_solved_solved_topics dsst
JOIN badge_posts p ON dsst.answer_post_id = p.id
JOIN topics t ON p.topic_id = t.id
WHERE p.user_id <> t.user_id -- ignore topics solved by OP
AND (:backfill OR p.id IN (:post_ids))
) x
WHERE row_number = 1
SQL
Expand All @@ -32,12 +31,11 @@

def solved_query_with_count(min_count)
<<~SQL
SELECT p.user_id, MAX(pcf.created_at) AS granted_at
FROM post_custom_fields pcf
JOIN badge_posts p ON pcf.post_id = p.id
SELECT p.user_id, MAX(dsst.created_at) AS granted_at
FROM discourse_solved_solved_topics dsst
JOIN badge_posts p ON dsst.answer_post_id = p.id
JOIN topics t ON p.topic_id = t.id
WHERE pcf.name = 'is_accepted_answer'
AND p.user_id <> t.user_id -- ignore topics solved by OP
WHERE p.user_id <> t.user_id -- ignore topics solved by OP
AND (:backfill OR p.id IN (:post_ids))
GROUP BY p.user_id
HAVING COUNT(*) >= #{min_count}
Expand Down
13 changes: 13 additions & 0 deletions db/migrate/20250318024824_create_discourse_solved_solved_topics.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true
#
class CreateDiscourseSolvedSolvedTopics < ActiveRecord::Migration[7.2]
def change
create_table :discourse_solved_solved_topics do |t|
t.integer :topic_id, null: false
t.integer :answer_post_id, null: false
t.integer :accepter_user_id, null: false
t.integer :topic_timer_id
t.timestamps
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true
#
class CopySolvedTopicCustomFieldToDiscourseSolvedSolvedTopics < ActiveRecord::Migration[7.2]
disable_ddl_transaction!

BATCH_SIZE = 5000

def up
last_id = 0
loop do
rows = DB.query(<<~SQL, last_id: last_id, batch_size: BATCH_SIZE)
INSERT INTO discourse_solved_solved_topics (
topic_id,
answer_post_id,
topic_timer_id,
accepter_user_id,
created_at,
updated_at
)
SELECT DISTINCT ON (tc.topic_id)
tc.topic_id,
CAST(tc.value AS INTEGER),
CAST(tc2.value AS INTEGER),
COALESCE(ua.acting_user_id, -1),
tc.created_at,
tc.updated_at
FROM topic_custom_fields tc
LEFT JOIN topic_custom_fields tc2
ON tc2.topic_id = tc.topic_id
AND tc2.name = 'solved_auto_close_topic_timer_id'
LEFT JOIN user_actions ua
ON ua.target_topic_id = tc.topic_id
AND ua.action_type = #{UserAction::SOLVED}
WHERE tc.name = 'accepted_answer_post_id'
AND tc.id > :last_id
ORDER BY tc.topic_id, ua.created_at DESC
LIMIT :batch_size
SQL

break if rows.length == 0
last_id += BATCH_SIZE
end
end

def down
raise ActiveRecord::IrreversibleMigration
end
end
22 changes: 22 additions & 0 deletions db/migrate/20250318025147_add_index_for_discourse_solved_topics.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true
#
class AddIndexForDiscourseSolvedTopics < ActiveRecord::Migration[7.2]
disable_ddl_transaction!

def change
remove_index :discourse_solved_solved_topics,
:topic_id,
algorithm: :concurrently,
if_exists: true
remove_index :discourse_solved_solved_topics,
:answer_post_id,
algorithm: :concurrently,
if_exists: true

add_index :discourse_solved_solved_topics, :topic_id, unique: true, algorithm: :concurrently
add_index :discourse_solved_solved_topics,
:answer_post_id,
unique: true,
algorithm: :concurrently
end
end
12 changes: 6 additions & 6 deletions lib/discourse_assign/entry_point.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

module DiscourseAssign
class EntryPoint
# TODO: These four plugin api usages should ideally be in the assign plugin, not the solved plugin.
# They have been moved here from plugin.rb as part of the custom fields migration.

def self.inject(plugin)
plugin.register_modifier(:assigns_reminder_assigned_topics_query) do |query|
next query if !SiteSetting.ignore_solved_topics_in_assigned_reminder
query.where.not(
id:
TopicCustomField.where(
name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
).pluck(:topic_id),
)
# TODO: this line was modified for the custom fields migration,
# but returning this huge array is not good at all.
query.where.not(id: DiscourseSolved::SolvedTopic.pluck(:topic_id))
end

plugin.register_modifier(:assigned_count_for_user_query) do |query, user|
Expand Down
5 changes: 1 addition & 4 deletions lib/discourse_solved/before_head_close.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ def html
},
}

if accepted_answer =
Post.find_by(
id: topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD],
)
if accepted_answer = topic.solved&.answer_post
question_json["answerCount"] = 1
question_json[:acceptedAnswer] = {
"@type" => "Answer",
Expand Down
50 changes: 21 additions & 29 deletions lib/discourse_solved/register_filters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,38 @@ module DiscourseSolved
class RegisterFilters
def self.register(plugin)
solved_callback = ->(scope) do
sql = <<~SQL
topics.id IN (
SELECT topic_id
FROM topic_custom_fields
WHERE name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
AND value IS NOT NULL
)
SQL

scope.where(sql).where("topics.archetype <> ?", Archetype.private_message)
scope.joins(
"INNER JOIN discourse_solved_solved_topics ON discourse_solved_solved_topics.topic_id = topics.id",
).where("topics.archetype <> ?", Archetype.private_message)
end

unsolved_callback = ->(scope) do
scope = scope.where <<~SQL
scope = scope.where(<<~SQL)
topics.id NOT IN (
SELECT topic_id
FROM topic_custom_fields
WHERE name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
AND value IS NOT NULL
FROM discourse_solved_solved_topics
)
SQL

if !SiteSetting.allow_solved_on_all_topics
tag_ids = Tag.where(name: SiteSetting.enable_solved_tags.split("|")).pluck(:id)

scope = scope.where <<~SQL, tag_ids
topics.id IN (
SELECT t.id
FROM topics t
JOIN category_custom_fields cc
ON t.category_id = cc.category_id
AND cc.name = '#{::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD}'
AND cc.value = 'true'
)
OR
topics.id IN (
SELECT topic_id
FROM topic_tags
WHERE tag_id IN (?)
)
SQL
topics.id IN (
SELECT t.id
FROM topics t
JOIN category_custom_fields cc
ON t.category_id = cc.category_id
AND cc.name = '#{::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD}'
AND cc.value = 'true'
)
OR
topics.id IN (
SELECT topic_id
FROM topic_tags
WHERE tag_id IN (?)
)
SQL
end

scope.where("topics.archetype <> ?", Archetype.private_message)
Expand Down
7 changes: 7 additions & 0 deletions lib/discourse_solved/topic_extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module DiscourseSolved::TopicExtension
extend ActiveSupport::Concern

prepended { has_one :solved, class_name: "DiscourseSolved::SolvedTopic", dependent: :destroy }
end
8 changes: 1 addition & 7 deletions lib/discourse_solved/topic_view_serializer_extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,6 @@ def accepted_answer_post_info
end

def accepted_answer_post_id
id = object.topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD]
# a bit messy but race conditions can give us an array here, avoid
begin
id && id.to_i
rescue StandardError
nil
end
object.topic.solved&.answer_post_id
end
end
2 changes: 1 addition & 1 deletion lib/discourse_solved/user_summary_extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ module DiscourseSolved::UserSummaryExtension
extend ActiveSupport::Concern

def solved_count
UserAction.where(user: @user).where(action_type: UserAction::SOLVED).count
DiscourseSolved::SolvedTopic.where(accepter: @user).count
end
end
Loading
Loading