DEV: Autoload and segregate features to prep for migration (#341)

This commit autoloads plugin files, and also extracts features into their own modules.
- `plugin.rb` is smaller
- external plugins like discourse-automation and discourse-assign have their own entrypoints
- solved filters as well
This commit is contained in:
Natalie Tay 2025-03-21 11:45:19 +08:00 committed by GitHub
parent 4e3521fd25
commit e82c6ae1ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 358 additions and 355 deletions

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
class FirstAcceptedPostSolutionValidator
def self.check(post, trust_level:)
return false if post.archetype != Archetype.default
return false if !post&.user&.human?
return true if trust_level == "any"
return false if TrustLevel.compare(post&.user&.trust_level, trust_level.to_i)
if !UserAction.where(user_id: post&.user_id, action_type: UserAction::SOLVED).exists?
return true
end
false
end
end

View File

@ -1,40 +0,0 @@
# frozen_string_literal: true
module DiscourseSolved
class PluginInitializer
attr_reader :plugin
def initialize(plugin)
@plugin = plugin
end
def apply_plugin_api
# this method should be overridden by subclasses
raise NotImplementedError
end
end
class AssignsReminderForTopicsQuery < PluginInitializer
def apply_plugin_api
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),
)
end
end
end
class AssignedCountForUserQuery < PluginInitializer
def apply_plugin_api
plugin.register_modifier(:assigned_count_for_user_query) do |query, user|
next query if !SiteSetting.ignore_solved_topics_in_assigned_reminder
next query if SiteSetting.assignment_status_on_solve.blank?
query.where.not(status: SiteSetting.assignment_status_on_solve)
end
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module DiscourseSolved
module TopicAnswerMixin
def self.included(klass)
klass.attributes :has_accepted_answer, :can_have_answer
end
def has_accepted_answer
object.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present?
end
def include_has_accepted_answer?
SiteSetting.solved_enabled
end
def can_have_answer
return true if SiteSetting.allow_solved_on_all_topics
return false if object.closed || object.archived
scope.allow_accepted_answers?(object.category_id, object.tags.map(&:name))
end
def include_can_have_answer?
SiteSetting.solved_enabled && SiteSetting.empty_box_on_unsolved
end
end
end

View File

@ -1,25 +0,0 @@
# frozen_string_literal: true
module TopicAnswerMixin
def self.included(klass)
klass.attributes :has_accepted_answer, :can_have_answer
end
def has_accepted_answer
object.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present?
end
def include_has_accepted_answer?
SiteSetting.solved_enabled
end
def can_have_answer
return true if SiteSetting.allow_solved_on_all_topics
return false if object.closed || object.archived
scope.allow_accepted_answers?(object.category_id, object.tags.map(&:name))
end
def include_can_have_answer?
SiteSetting.solved_enabled && SiteSetting.empty_box_on_unsolved
end
end

8
config/routes.rb Normal file
View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
DiscourseSolved::Engine.routes.draw do
post "/accept" => "answer#accept"
post "/unaccept" => "answer#unaccept"
end
Discourse::Application.routes.draw { mount ::DiscourseSolved::Engine, at: "solution" }

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module DiscourseAssign
class EntryPoint
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),
)
end
plugin.register_modifier(:assigned_count_for_user_query) do |query, user|
next query if !SiteSetting.ignore_solved_topics_in_assigned_reminder
next query if SiteSetting.assignment_status_on_solve.blank?
query.where.not(status: SiteSetting.assignment_status_on_solve)
end
plugin.on(:accepted_solution) do |post|
next if SiteSetting.assignment_status_on_solve.blank?
assignments = Assignment.includes(:target).where(topic: post.topic)
assignments.each do |assignment|
assigned_user = User.find_by(id: assignment.assigned_to_id)
Assigner.new(assignment.target, assigned_user).assign(
assigned_user,
status: SiteSetting.assignment_status_on_solve,
)
end
end
plugin.on(:unaccepted_solution) do |post|
next if SiteSetting.assignment_status_on_unsolve.blank?
assignments = Assignment.includes(:target).where(topic: post.topic)
assignments.each do |assignment|
assigned_user = User.find_by(id: assignment.assigned_to_id)
Assigner.new(assignment.target, assigned_user).assign(
assigned_user,
status: SiteSetting.assignment_status_on_unsolve,
)
end
end
end
end
end

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
module DiscourseAutomation
class EntryPoint
def self.inject(plugin)
plugin.on(:accepted_solution) do |post|
# testing directly automation is prone to issues
# we prefer to abstract logic in service object and test this
next if Rails.env.test?
name = "first_accepted_solution"
DiscourseAutomation::Automation
.where(trigger: name, enabled: true)
.find_each do |automation|
maximum_trust_level = automation.trigger_field("maximum_trust_level")&.dig("value")
if FirstAcceptedPostSolutionValidator.check(post, trust_level: maximum_trust_level)
automation.trigger!(
"kind" => name,
"accepted_post_id" => post.id,
"usernames" => [post.user.username],
"placeholders" => {
"post_url" => Discourse.base_url + post.url,
},
)
end
end
end
plugin.add_triggerable_to_scriptable(:first_accepted_solution, :send_pms)
DiscourseAutomation::Triggerable.add(:first_accepted_solution) do
placeholder :post_url
field :maximum_trust_level,
component: :choices,
extra: {
content: [
{
id: 1,
name:
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl1",
},
{
id: 2,
name:
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl2",
},
{
id: 3,
name:
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl3",
},
{
id: 4,
name:
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl4",
},
{
id: "any",
name:
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.any",
},
],
},
required: true
end
end
end
end

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
module DiscourseDev
class DiscourseSolved
def self.populate(plugin)
plugin.on(:after_populate_dev_records) do |records, type|
next unless SiteSetting.solved_enabled
if type == :category
next if SiteSetting.allow_solved_on_all_topics
solved_category =
DiscourseDev::Record.random(
Category.where(
read_restricted: false,
id: records.pluck(:id),
parent_category_id: nil,
),
)
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))
unless SiteSetting.allow_solved_on_all_topics
solved_category_id =
CategoryCustomField
.where(name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD, value: "true")
.first
.category_id
unless topics.exists?(category_id: solved_category_id)
topics.last.update(category_id: solved_category_id)
end
topics = topics.where(category_id: solved_category_id)
end
solved_topic = DiscourseDev::Record.random(topics)
post = nil
if solved_topic.posts_count > 1
post = DiscourseDev::Record.random(solved_topic.posts.where.not(post_number: 1))
else
post = DiscourseDev::Post.new(solved_topic, 1).create!
end
::DiscourseSolved.accept_answer!(post, post.topic.user, topic: post.topic)
end
end
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module ::DiscourseSolved
class Engine < ::Rails::Engine
engine_name PLUGIN_NAME
isolate_namespace DiscourseSolved
config.autoload_paths << File.join(config.root, "lib")
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module DiscourseSolved
class FirstAcceptedPostSolutionValidator
def self.check(post, trust_level:)
return false if post.archetype != Archetype.default
return false if !post&.user&.human?
return true if trust_level == "any"
return false if TrustLevel.compare(post&.user&.trust_level, trust_level.to_i)
if !UserAction.where(user_id: post&.user_id, action_type: UserAction::SOLVED).exists?
return true
end
false
end
end
end

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
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)
end
unsolved_callback = ->(scope) do
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
)
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
end
scope.where("topics.archetype <> ?", Archetype.private_message)
end
plugin.register_custom_filter_by_status("solved", &solved_callback)
plugin.register_custom_filter_by_status("unsolved", &unsolved_callback)
plugin.register_search_advanced_filter(/status:solved/, &solved_callback)
plugin.register_search_advanced_filter(/status:unsolved/, &unsolved_callback)
TopicQuery.add_custom_filter(:solved) do |results, topic_query|
if topic_query.options[:solved] == "yes"
solved_callback.call(results)
elsif topic_query.options[:solved] == "no"
unsolved_callback.call(results)
else
results
end
end
end
end
end

310
plugin.rb
View File

@ -16,38 +16,19 @@ register_svg_icon "far-square"
register_asset "stylesheets/solutions.scss" register_asset "stylesheets/solutions.scss"
register_asset "stylesheets/mobile/solutions.scss", :mobile register_asset "stylesheets/mobile/solutions.scss", :mobile
module ::DiscourseSolved
PLUGIN_NAME = "discourse-solved"
AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD = "solved_auto_close_topic_timer_id"
ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD = "accepted_answer_post_id"
ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD = "enable_accepted_answers"
IS_ACCEPTED_ANSWER_CUSTOM_FIELD = "is_accepted_answer"
end
require_relative "lib/discourse_solved/engine.rb"
after_initialize do after_initialize do
module ::DiscourseSolved
PLUGIN_NAME = "discourse-solved"
AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD = "solved_auto_close_topic_timer_id"
ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD = "accepted_answer_post_id"
ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD = "enable_accepted_answers"
IS_ACCEPTED_ANSWER_CUSTOM_FIELD = "is_accepted_answer"
class Engine < ::Rails::Engine
engine_name PLUGIN_NAME
isolate_namespace DiscourseSolved
end
end
SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s
require_relative "app/controllers/answer_controller"
require_relative "app/lib/first_accepted_post_solution_validator"
require_relative "app/lib/accepted_answer_cache"
require_relative "app/lib/guardian_extensions"
require_relative "app/lib/before_head_close"
require_relative "app/lib/category_extension"
require_relative "app/lib/post_serializer_extension"
require_relative "app/lib/topic_posters_summary_extension"
require_relative "app/lib/topic_view_serializer_extension"
require_relative "app/lib/user_summary_extension"
require_relative "app/lib/web_hook_extension"
require_relative "app/serializers/concerns/topic_answer_mixin"
require_relative "app/lib/plugin_initializers/assigned_reminder_exclude_solved"
DiscourseSolved::AssignsReminderForTopicsQuery.new(self).apply_plugin_api
DiscourseSolved::AssignedCountForUserQuery.new(self).apply_plugin_api
module ::DiscourseSolved module ::DiscourseSolved
def self.accept_answer!(post, acting_user, topic: nil) def self.accept_answer!(post, acting_user, topic: nil)
topic ||= post.topic topic ||= post.topic
@ -183,7 +164,7 @@ after_initialize do
end end
end end
reloadable_patch do |plugin| reloadable_patch do
::Guardian.prepend(DiscourseSolved::GuardianExtensions) ::Guardian.prepend(DiscourseSolved::GuardianExtensions)
::WebHook.prepend(DiscourseSolved::WebHookExtension) ::WebHook.prepend(DiscourseSolved::WebHookExtension)
::TopicViewSerializer.prepend(DiscourseSolved::TopicViewSerializerExtension) ::TopicViewSerializer.prepend(DiscourseSolved::TopicViewSerializerExtension)
@ -199,7 +180,7 @@ after_initialize do
::SuggestedTopicSerializer, ::SuggestedTopicSerializer,
::UserSummarySerializer::TopicSerializer, ::UserSummarySerializer::TopicSerializer,
::ListableTopicSerializer, ::ListableTopicSerializer,
].each { |klass| klass.include(TopicAnswerMixin) } ].each { |klass| klass.include(DiscourseSolved::TopicAnswerMixin) }
end end
# we got to do a one time upgrade # we got to do a one time upgrade
@ -240,26 +221,17 @@ after_initialize do
end end
end end
DiscourseSolved::Engine.routes.draw do topic_view_post_custom_fields_allowlister { [::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] }
post "/accept" => "answer#accept" TopicList.preloaded_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
post "/unaccept" => "answer#unaccept" Site.preloaded_category_custom_fields << ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD
end Search.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
CategoryList.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
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 { [::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] }
register_html_builder("server:before-head-close-crawler") do |controller| register_html_builder("server:before-head-close-crawler") do |controller|
DiscourseSolved::BeforeHeadClose.new(controller).html DiscourseSolved::BeforeHeadClose.new(controller).html
end end
@ -308,14 +280,14 @@ after_initialize do
register_modifier(:search_rank_sort_priorities) do |priorities, _search| register_modifier(:search_rank_sort_priorities) do |priorities, _search|
if SiteSetting.prioritize_solved_topics_in_search if SiteSetting.prioritize_solved_topics_in_search
condition = <<~SQL condition = <<~SQL
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM topic_custom_fields FROM topic_custom_fields
WHERE topic_id = topics.id WHERE topic_id = topics.id
AND name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}' AND name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
AND value IS NOT NULL AND value IS NOT NULL
) )
SQL SQL
priorities.push([condition, 1.1]) priorities.push([condition, 1.1])
else else
@ -323,6 +295,19 @@ after_initialize do
end end
end end
register_modifier(:user_action_stream_builder) do |builder|
builder.where("t.deleted_at IS NULL").where("t.archetype <> ?", Archetype.private_message)
end
add_to_serializer(:user_card, :accepted_answers) do
UserAction
.where(user_id: object.id)
.where(action_type: UserAction::SOLVED)
.joins("JOIN topics ON topics.id = user_actions.target_topic_id")
.where("topics.archetype <> ?", Archetype.private_message)
.where("topics.deleted_at IS NULL")
.count
end
add_to_serializer(:user_summary, :solved_count) { object.solved_count } add_to_serializer(:user_summary, :solved_count) { object.solved_count }
add_to_serializer(:post, :can_accept_answer) do add_to_serializer(:post, :can_accept_answer) do
scope.can_accept_answer?(topic, object) && object.post_number > 1 && !accepted_answer scope.can_accept_answer?(topic, object) && object.post_number > 1 && !accepted_answer
@ -337,78 +322,12 @@ after_initialize do
topic&.custom_fields&.[](::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD).present? topic&.custom_fields&.[](::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD).present?
end end
solved_callback = ->(scope) do on(:post_destroyed) do |post|
sql = <<~SQL if post.custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
topics.id IN ( ::DiscourseSolved.unaccept_answer!(post)
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)
end
unsolved_callback = ->(scope) do
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
)
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
end
scope.where("topics.archetype <> ?", Archetype.private_message)
end
register_custom_filter_by_status("solved", &solved_callback)
register_custom_filter_by_status("unsolved", &unsolved_callback)
register_search_advanced_filter(/status:solved/, &solved_callback)
register_search_advanced_filter(/status:unsolved/, &unsolved_callback)
TopicQuery.add_custom_filter(:solved) do |results, topic_query|
if topic_query.options[:solved] == "yes"
solved_callback.call(results)
elsif topic_query.options[:solved] == "no"
unsolved_callback.call(results)
else
results
end end
end end
register_modifier(:user_action_stream_builder) do |builder|
builder.where("t.deleted_at IS NULL").where("t.archetype <> ?", Archetype.private_message)
end
TopicList.preloaded_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
Site.preloaded_category_custom_fields << ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD
Search.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
CategoryList.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
on(:filter_auto_bump_topics) do |_category, filters| on(:filter_auto_bump_topics) do |_category, filters|
filters.push( filters.push(
->(r) do ->(r) do
@ -437,52 +356,6 @@ after_initialize do
options[:refresh_stream] = true if old_allowed != new_allowed options[:refresh_stream] = true if old_allowed != new_allowed
end end
on(:after_populate_dev_records) do |records, type|
next unless SiteSetting.solved_enabled
if type == :category
next if SiteSetting.allow_solved_on_all_topics
solved_category =
DiscourseDev::Record.random(
Category.where(read_restricted: false, id: records.pluck(:id), parent_category_id: nil),
)
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))
unless SiteSetting.allow_solved_on_all_topics
solved_category_id =
CategoryCustomField
.where(name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD, value: "true")
.first
.category_id
unless topics.exists?(category_id: solved_category_id)
topics.last.update(category_id: solved_category_id)
end
topics = topics.where(category_id: solved_category_id)
end
solved_topic = DiscourseDev::Record.random(topics)
post = nil
if solved_topic.posts_count > 1
post = DiscourseDev::Record.random(solved_topic.posts.where.not(post_number: 1))
else
post = DiscourseDev::Post.new(solved_topic, 1).create!
end
DiscourseSolved.accept_answer!(post, post.topic.user, topic: post.topic)
end
end
query = <<~SQL query = <<~SQL
WITH x AS ( WITH x AS (
SELECT u.id user_id, COUNT(DISTINCT ua.id) AS solutions SELECT u.id user_id, COUNT(DISTINCT ua.id) AS solutions
@ -529,16 +402,6 @@ after_initialize do
} }
end end
add_to_serializer(:user_card, :accepted_answers) do
UserAction
.where(user_id: object.id)
.where(action_type: UserAction::SOLVED)
.joins("JOIN topics ON topics.id = user_actions.target_topic_id")
.where("topics.archetype <> ?", Archetype.private_message)
.where("topics.deleted_at IS NULL")
.count
end
register_topic_list_preload_user_ids do |topics, user_ids, topic_list| register_topic_list_preload_user_ids do |topics, user_ids, topic_list|
answer_post_ids = answer_post_ids =
TopicCustomField TopicCustomField
@ -550,92 +413,9 @@ after_initialize do
user_ids.concat(answer_user_ids.values) user_ids.concat(answer_user_ids.values)
end end
if defined?(DiscourseAutomation) DiscourseSolved::RegisterFilters.register(self)
on(:accepted_solution) do |post|
# testing directly automation is prone to issues
# we prefer to abstract logic in service object and test this
next if Rails.env.test?
name = "first_accepted_solution" DiscourseDev::DiscourseSolved.populate(self)
DiscourseAutomation::Automation DiscourseAutomation::EntryPoint.inject(self) if defined?(DiscourseAutomation)
.where(trigger: name, enabled: true) DiscourseAssign::EntryPoint.inject(self) if defined?(DiscourseAssign)
.find_each do |automation|
maximum_trust_level = automation.trigger_field("maximum_trust_level")&.dig("value")
if FirstAcceptedPostSolutionValidator.check(post, trust_level: maximum_trust_level)
automation.trigger!(
"kind" => name,
"accepted_post_id" => post.id,
"usernames" => [post.user.username],
"placeholders" => {
"post_url" => Discourse.base_url + post.url,
},
)
end
end
end
add_triggerable_to_scriptable(:first_accepted_solution, :send_pms)
DiscourseAutomation::Triggerable.add(:first_accepted_solution) do
placeholder :post_url
field :maximum_trust_level,
component: :choices,
extra: {
content: [
{
id: 1,
name:
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl1",
},
{
id: 2,
name:
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl2",
},
{
id: 3,
name:
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl3",
},
{
id: 4,
name:
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl4",
},
{
id: "any",
name:
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.any",
},
],
},
required: true
end
end
if defined?(DiscourseAssign)
on(:accepted_solution) do |post|
next if SiteSetting.assignment_status_on_solve.blank?
assignments = Assignment.includes(:target).where(topic: post.topic)
assignments.each do |assignment|
assigned_user = User.find_by(id: assignment.assigned_to_id)
Assigner.new(assignment.target, assigned_user).assign(
assigned_user,
status: SiteSetting.assignment_status_on_solve,
)
end
end
on(:unaccepted_solution) do |post|
next if SiteSetting.assignment_status_on_unsolve.blank?
assignments = Assignment.includes(:target).where(topic: post.topic)
assignments.each do |assignment|
assigned_user = User.find_by(id: assignment.assigned_to_id)
Assigner.new(assignment.target, assigned_user).assign(
assigned_user,
status: SiteSetting.assignment_status_on_unsolve,
)
end
end
end
end end

View File

@ -2,7 +2,7 @@
require "rails_helper" require "rails_helper"
describe FirstAcceptedPostSolutionValidator do describe DiscourseSolved::FirstAcceptedPostSolutionValidator do
fab!(:user_tl1) { Fabricate(:user, trust_level: TrustLevel[1], refresh_auto_groups: true) } fab!(:user_tl1) { Fabricate(:user, trust_level: TrustLevel[1], refresh_auto_groups: true) }
context "when user is under max trust level" do context "when user is under max trust level" do

View File

@ -2,7 +2,7 @@
require "rails_helper" require "rails_helper"
describe TopicAnswerMixin do describe DiscourseSolved::TopicAnswerMixin do
let(:topic) { Fabricate(:topic) } let(:topic) { Fabricate(:topic) }
let(:post) { Fabricate(:post, topic: topic) } let(:post) { Fabricate(:post, topic: topic) }
let(:guardian) { Guardian.new } let(:guardian) { Guardian.new }