diff --git a/app/lib/first_accepted_post_solution_validator.rb b/app/lib/first_accepted_post_solution_validator.rb new file mode 100644 index 0000000..898e213 --- /dev/null +++ b/app/lib/first_accepted_post_solution_validator.rb @@ -0,0 +1,19 @@ +# 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' + + if TrustLevel.compare(post&.user&.trust_level, trust_level.to_i) + return false + end + + if !UserAction.where(user_id: post&.user_id, action_type: UserAction::SOLVED).exists? + return true + end + + false + end +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 9dd018e..90b548b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -42,3 +42,17 @@ en: solved_event: name: "Solved Event" details: "When a user marks a post as the accepted or unaccepted answer." + + discourse_automation: + triggerables: + first_accepted_solution: + max_trust_level: + tl1: < TL1 + tl2: < TL2 + tl3: < TL3 + tl4: < TL4 + any: Any + fields: + maximum_trust_level: + label: Trust Level + description: Users under this Trust Level will trigger this automation diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index cb635f4..18b9e5b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -33,6 +33,12 @@ en: name: "Tech Support" description: "10 Accepted answers" + discourse_automation: + triggerables: + first_accepted_solution: + title: First accepted solution + doc: Triggers when a user got a solution accepted for the first time. + education: topic_is_solved: | ### This topic has been solved diff --git a/plugin.rb b/plugin.rb index 1af9bec..c4557c7 100644 --- a/plugin.rb +++ b/plugin.rb @@ -24,6 +24,7 @@ after_initialize do SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s [ + '../app/lib/first_accepted_post_solution_validator.rb', '../app/serializers/concerns/topic_answer_mixin.rb' ].each { |path| load File.expand_path(path, __FILE__) } @@ -752,4 +753,45 @@ SQL prepend AddSolvedToTopicPostersSummary end end + + if defined?(DiscourseAutomation) + if respond_to?(:add_triggerable_to_scriptable) + 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 + + TRUST_LEVELS = [ + { 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' }, + ] + + 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: TRUST_LEVELS }, required: true + end + end + end end diff --git a/spec/lib/first_accepted_post_solution_validator_spec.rb b/spec/lib/first_accepted_post_solution_validator_spec.rb new file mode 100644 index 0000000..39d07b3 --- /dev/null +++ b/spec/lib/first_accepted_post_solution_validator_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe FirstAcceptedPostSolutionValidator do + fab!(:user_tl1) { Fabricate(:user, trust_level: TrustLevel[1]) } + + context 'user is under max trust level' do + context 'has no post accepted yet' do + it 'validates the post' do + post_1 = create_post(user: user_tl1) + expect(described_class.check(post_1, trust_level: TrustLevel[2])).to eq(true) + end + end + + context 'has already had accepted posts' do + before do + accepted_post = create_post(user: user_tl1) + DiscourseSolved.accept_answer!(accepted_post, Discourse.system_user) + end + + it 'doesn’t validate the post' do + post_1 = create_post(user: user_tl1) + expect(described_class.check(post_1, trust_level: TrustLevel[2])).to eq(false) + end + end + end + + context 'user is above or equal max trust level' do + context 'has no post accepted yet' do + it 'doesn’t validate the post' do + post_1 = create_post(user: user_tl1) + expect(described_class.check(post_1, trust_level: TrustLevel[1])).to eq(false) + end + end + + context 'has already had accepted posts' do + before do + accepted_post = create_post(user: user_tl1) + DiscourseSolved.accept_answer!(accepted_post, Discourse.system_user) + end + + it 'doesn’t validate the post' do + post_1 = create_post(user: user_tl1) + expect(described_class.check(post_1, trust_level: TrustLevel[1])).to eq(false) + end + end + end + + context 'using any trust level' do + it 'validates the post' do + post_1 = create_post(user: user_tl1) + expect(described_class.check(post_1, trust_level: 'any')).to eq(true) + end + end + + context 'user is system' do + it 'doesn’t validate the post' do + post_1 = create_post(user: Discourse.system_user) + expect(described_class.check(post_1, trust_level: 'any')).to eq(false) + end + end + + context 'post is a PM' do + it 'doesn’t validate the post' do + post_1 = create_post(user: user_tl1, target_usernames: [user_tl1.username], archetype: Archetype.private_message) + expect(described_class.check(post_1, trust_level: 'any')).to eq(false) + end + end +end