Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
34 changes: 34 additions & 0 deletions app/models/discourse_solved/solved_topic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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
validates :accepter_user_id, presence: true
end
end

# == Schema Information
#
# Table name: discourse_solved_solved_topics
#
# id :bigint not null, primary key
# topic_id :integer not null
# answer_post_id :integer not null
# accepter_user_id :integer not null
# topic_timer_id :integer
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_discourse_solved_solved_topics_on_answer_post_id (answer_post_id) UNIQUE
# index_discourse_solved_solved_topics_on_topic_id (topic_id) UNIQUE
#
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 = 15
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
24 changes: 24 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,24 @@
# 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,
unique: true,
if_exists: true
remove_index :discourse_solved_solved_topics,
:answer_post_id,
algorithm: :concurrently,
unique: true,
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
10 changes: 4 additions & 6 deletions lib/discourse_assign/entry_point.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

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),
)
query.where.not(id: DiscourseSolved::SolvedTopic.select(:topic_id))
end

plugin.register_modifier(:assigned_count_for_user_query) do |query, user|
Expand Down
8 changes: 4 additions & 4 deletions lib/discourse_dev/discourse_solved.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,24 @@ def self.populate(plugin)

solved_category =
DiscourseDev::Record.random(
Category.where(
::Category.where(
read_restricted: false,
id: records.pluck(:id),
parent_category_id: nil,
),
)
CategoryCustomField.create!(
::CategoryCustomField.create!(
category_id: solved_category.id,
name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD,
value: "true",
)
puts "discourse-solved enabled on category '#{solved_category.name}' (#{solved_category.id})."
elsif type == :topic
topics = Topic.where(id: records.pluck(:id))
topics = ::Topic.where(id: records.pluck(:id))

unless SiteSetting.allow_solved_on_all_topics
solved_category_id =
CategoryCustomField
::CategoryCustomField
.where(name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD, value: "true")
.first
.category_id
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