FEATURE: prioritize_solved_topics_in_search to prioritize solved topics (#236)
Many consumers of Discourse solved may want solved topics to show up more prominently in search. New setting `prioritize_solved_topics_in_search` (default off) allows bumping these topics to the top. Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
This commit is contained in:
parent
daf2a823e2
commit
b4a740382f
|
@ -14,6 +14,7 @@ en:
|
||||||
accept_solutions_topic_author: "Allow the topic author to accept a solution."
|
accept_solutions_topic_author: "Allow the topic author to accept a solution."
|
||||||
solved_add_schema_markup: "Add QAPage schema markup to HTML."
|
solved_add_schema_markup: "Add QAPage schema markup to HTML."
|
||||||
enable_solved_tags: "Tags that will allow users to select solutions."
|
enable_solved_tags: "Tags that will allow users to select solutions."
|
||||||
|
prioritize_solved_topics_in_search: "Prioritize solved topics in search results."
|
||||||
|
|
||||||
reports:
|
reports:
|
||||||
accepted_solutions:
|
accepted_solutions:
|
||||||
|
|
|
@ -34,6 +34,7 @@ plugins:
|
||||||
- "never"
|
- "never"
|
||||||
- "always"
|
- "always"
|
||||||
- "answered only"
|
- "answered only"
|
||||||
|
prioritize_solved_topics_in_search: false
|
||||||
enable_solved_tags:
|
enable_solved_tags:
|
||||||
type: tag_list
|
type: tag_list
|
||||||
default: ""
|
default: ""
|
||||||
|
|
60
plugin.rb
60
plugin.rb
|
@ -15,7 +15,7 @@ if respond_to?(:register_svg_icon)
|
||||||
register_svg_icon "far fa-square"
|
register_svg_icon "far fa-square"
|
||||||
end
|
end
|
||||||
|
|
||||||
PLUGIN_NAME = "discourse_solved".freeze
|
PLUGIN_NAME = "discourse_solved"
|
||||||
|
|
||||||
register_asset "stylesheets/solutions.scss"
|
register_asset "stylesheets/solutions.scss"
|
||||||
register_asset "stylesheets/mobile/solutions.scss", :mobile
|
register_asset "stylesheets/mobile/solutions.scss", :mobile
|
||||||
|
@ -74,13 +74,14 @@ SQL
|
||||||
isolate_namespace DiscourseSolved
|
isolate_namespace DiscourseSolved
|
||||||
end
|
end
|
||||||
|
|
||||||
AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD = "solved_auto_close_topic_timer_id".freeze
|
AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD = "solved_auto_close_topic_timer_id"
|
||||||
|
ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD = "accepted_answer_post_id"
|
||||||
|
|
||||||
def self.accept_answer!(post, acting_user, topic: nil)
|
def self.accept_answer!(post, acting_user, topic: nil)
|
||||||
topic ||= post.topic
|
topic ||= post.topic
|
||||||
|
|
||||||
DistributedMutex.synchronize("discourse_solved_toggle_answer_#{topic.id}") do
|
DistributedMutex.synchronize("discourse_solved_toggle_answer_#{topic.id}") do
|
||||||
accepted_id = topic.custom_fields["accepted_answer_post_id"].to_i
|
accepted_id = topic.custom_fields[ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].to_i
|
||||||
|
|
||||||
if accepted_id > 0
|
if accepted_id > 0
|
||||||
if p2 = Post.find_by(id: accepted_id)
|
if p2 = Post.find_by(id: accepted_id)
|
||||||
|
@ -94,7 +95,7 @@ SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
post.custom_fields["is_accepted_answer"] = "true"
|
post.custom_fields["is_accepted_answer"] = "true"
|
||||||
topic.custom_fields["accepted_answer_post_id"] = post.id
|
topic.custom_fields[ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD] = post.id
|
||||||
|
|
||||||
if defined?(UserAction::SOLVED)
|
if defined?(UserAction::SOLVED)
|
||||||
UserAction.log_action!(
|
UserAction.log_action!(
|
||||||
|
@ -172,7 +173,7 @@ SQL
|
||||||
|
|
||||||
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")
|
||||||
topic.custom_fields.delete("accepted_answer_post_id")
|
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]
|
||||||
topic_timer = TopicTimer.find_by(id: timer_id)
|
topic_timer = TopicTimer.find_by(id: timer_id)
|
||||||
|
@ -299,7 +300,10 @@ SQL
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if accepted_answer = Post.find_by(id: topic.custom_fields["accepted_answer_post_id"])
|
if accepted_answer =
|
||||||
|
Post.find_by(
|
||||||
|
id: topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD],
|
||||||
|
)
|
||||||
question_json["answerCount"] = 1
|
question_json["answerCount"] = 1
|
||||||
question_json[:acceptedAnswer] = {
|
question_json[:acceptedAnswer] = {
|
||||||
"@type" => "Answer",
|
"@type" => "Answer",
|
||||||
|
@ -343,7 +347,8 @@ SQL
|
||||||
Report.add_report("accepted_solutions") do |report|
|
Report.add_report("accepted_solutions") do |report|
|
||||||
report.data = []
|
report.data = []
|
||||||
|
|
||||||
accepted_solutions = TopicCustomField.where(name: "accepted_answer_post_id")
|
accepted_solutions =
|
||||||
|
TopicCustomField.where(name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD)
|
||||||
|
|
||||||
category_id, include_subcategories = report.add_category_filter
|
category_id, include_subcategories = report.add_category_filter
|
||||||
if category_id
|
if category_id
|
||||||
|
@ -375,6 +380,25 @@ SQL
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if respond_to?(:register_modifier)
|
||||||
|
register_modifier(:search_rank_sort_priorities) do |priorities, search|
|
||||||
|
if SiteSetting.prioritize_solved_topics_in_search
|
||||||
|
condition = <<~SQL
|
||||||
|
EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1 FROM topic_custom_fields f
|
||||||
|
WHERE topics.id = f.topic_id
|
||||||
|
AND f.name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
|
||||||
|
)
|
||||||
|
SQL
|
||||||
|
|
||||||
|
priorities.push([condition, 1.1])
|
||||||
|
else
|
||||||
|
priorities
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if defined?(UserAction::SOLVED)
|
if defined?(UserAction::SOLVED)
|
||||||
require_dependency "user_summary"
|
require_dependency "user_summary"
|
||||||
class ::UserSummary
|
class ::UserSummary
|
||||||
|
@ -444,7 +468,7 @@ SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def accepted_answer_post_id
|
def accepted_answer_post_id
|
||||||
id = object.topic.custom_fields["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
|
# a bit messy but race conditions can give us an array here, avoid
|
||||||
begin
|
begin
|
||||||
id && id.to_i
|
id && id.to_i
|
||||||
|
@ -533,7 +557,7 @@ SQL
|
||||||
|
|
||||||
def topic_accepted_answer
|
def topic_accepted_answer
|
||||||
if topic = (topic_view && topic_view.topic) || object.topic
|
if topic = (topic_view && topic_view.topic) || object.topic
|
||||||
topic.custom_fields["accepted_answer_post_id"].present?
|
topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -547,7 +571,7 @@ SQL
|
||||||
"topics.id IN (
|
"topics.id IN (
|
||||||
SELECT tc.topic_id
|
SELECT tc.topic_id
|
||||||
FROM topic_custom_fields tc
|
FROM topic_custom_fields tc
|
||||||
WHERE tc.name = 'accepted_answer_post_id' AND
|
WHERE tc.name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}' AND
|
||||||
tc.value IS NOT NULL
|
tc.value IS NOT NULL
|
||||||
)",
|
)",
|
||||||
)
|
)
|
||||||
|
@ -558,7 +582,7 @@ SQL
|
||||||
"topics.id NOT IN (
|
"topics.id NOT IN (
|
||||||
SELECT tc.topic_id
|
SELECT tc.topic_id
|
||||||
FROM topic_custom_fields tc
|
FROM topic_custom_fields tc
|
||||||
WHERE tc.name = 'accepted_answer_post_id' AND
|
WHERE tc.name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}' AND
|
||||||
tc.value IS NOT NULL
|
tc.value IS NOT NULL
|
||||||
)",
|
)",
|
||||||
)
|
)
|
||||||
|
@ -575,7 +599,7 @@ SQL
|
||||||
"topics.id IN (
|
"topics.id IN (
|
||||||
SELECT tc.topic_id
|
SELECT tc.topic_id
|
||||||
FROM topic_custom_fields tc
|
FROM topic_custom_fields tc
|
||||||
WHERE tc.name = 'accepted_answer_post_id' AND
|
WHERE tc.name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}' AND
|
||||||
tc.value IS NOT NULL
|
tc.value IS NOT NULL
|
||||||
)",
|
)",
|
||||||
)
|
)
|
||||||
|
@ -585,7 +609,7 @@ SQL
|
||||||
"topics.id NOT IN (
|
"topics.id NOT IN (
|
||||||
SELECT tc.topic_id
|
SELECT tc.topic_id
|
||||||
FROM topic_custom_fields tc
|
FROM topic_custom_fields tc
|
||||||
WHERE tc.name = 'accepted_answer_post_id' AND
|
WHERE tc.name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}' AND
|
||||||
tc.value IS NOT NULL
|
tc.value IS NOT NULL
|
||||||
)",
|
)",
|
||||||
)
|
)
|
||||||
|
@ -620,13 +644,13 @@ SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
if TopicList.respond_to? :preloaded_custom_fields
|
if TopicList.respond_to? :preloaded_custom_fields
|
||||||
TopicList.preloaded_custom_fields << "accepted_answer_post_id"
|
TopicList.preloaded_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
|
||||||
end
|
end
|
||||||
if Site.respond_to? :preloaded_category_custom_fields
|
if Site.respond_to? :preloaded_category_custom_fields
|
||||||
Site.preloaded_category_custom_fields << "enable_accepted_answers"
|
Site.preloaded_category_custom_fields << "enable_accepted_answers"
|
||||||
end
|
end
|
||||||
if Search.respond_to? :preloaded_topic_custom_fields
|
if Search.respond_to? :preloaded_topic_custom_fields
|
||||||
Search.preloaded_topic_custom_fields << "accepted_answer_post_id"
|
Search.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
|
||||||
end
|
end
|
||||||
|
|
||||||
if CategoryList.respond_to?(:preloaded_topic_custom_fields)
|
if CategoryList.respond_to?(:preloaded_topic_custom_fields)
|
||||||
|
@ -637,7 +661,7 @@ SQL
|
||||||
NOT EXISTS(
|
NOT EXISTS(
|
||||||
SELECT 1 FROM topic_custom_fields
|
SELECT 1 FROM topic_custom_fields
|
||||||
WHERE topic_id = topics.id
|
WHERE topic_id = topics.id
|
||||||
AND name = 'accepted_answer_post_id'
|
AND name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
|
||||||
AND value IS NOT NULL
|
AND value IS NOT NULL
|
||||||
)
|
)
|
||||||
SQL
|
SQL
|
||||||
|
@ -722,7 +746,7 @@ SQL
|
||||||
add_to_class(:composer_messages_finder, :check_topic_is_solved) do
|
add_to_class(:composer_messages_finder, :check_topic_is_solved) do
|
||||||
return if !SiteSetting.solved_enabled || SiteSetting.disable_solved_education_message
|
return if !SiteSetting.solved_enabled || SiteSetting.disable_solved_education_message
|
||||||
return if !replying? || @topic.blank? || @topic.private_message?
|
return if !replying? || @topic.blank? || @topic.private_message?
|
||||||
return if @topic.custom_fields["accepted_answer_post_id"].blank?
|
return if @topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].blank?
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "solved_topic",
|
id: "solved_topic",
|
||||||
|
@ -749,7 +773,7 @@ SQL
|
||||||
answer_post_ids =
|
answer_post_ids =
|
||||||
TopicCustomField
|
TopicCustomField
|
||||||
.select("value::INTEGER")
|
.select("value::INTEGER")
|
||||||
.where(name: "accepted_answer_post_id")
|
.where(name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD)
|
||||||
.where(topic_id: topics.map(&:id))
|
.where(topic_id: topics.map(&:id))
|
||||||
answer_user_ids = Post.where(id: answer_post_ids).pluck(:topic_id, :user_id).to_h
|
answer_user_ids = Post.where(id: answer_post_ids).pluck(:topic_id, :user_id).to_h
|
||||||
topics.each { |topic| topic.accepted_answer_user_id = answer_user_ids[topic.id] }
|
topics.each { |topic| topic.accepted_answer_user_id = answer_user_ids[topic.id] }
|
||||||
|
|
|
@ -9,6 +9,38 @@ RSpec.describe "Managing Posts solved status" do
|
||||||
|
|
||||||
before { SiteSetting.allow_solved_on_all_topics = true }
|
before { SiteSetting.allow_solved_on_all_topics = true }
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
describe "auto bump" do
|
describe "auto bump" do
|
||||||
it "does not automatically bump solved topics" do
|
it "does not automatically bump solved topics" do
|
||||||
category = Fabricate(:category_with_definition)
|
category = Fabricate(:category_with_definition)
|
||||||
|
|
Loading…
Reference in New Issue