From 5b68628dac02ff952a38bbb826211382a61042ba Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Tue, 25 Feb 2020 17:35:09 -0700 Subject: [PATCH 1/7] FEATURE: Publish WebHook event when solving/unsolving This feature will publish a post edit webhook event whenever a solution is accepted or unaccepted. I went ahead and used the existing post-edit webhook because all the post custom fields for the solved plugin are already included in the post-edit serializer. --- plugin.rb | 11 +++++++++++ spec/integration/solved_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/plugin.rb b/plugin.rb index 77d96015..6cc9296b 100644 --- a/plugin.rb +++ b/plugin.rb @@ -138,6 +138,11 @@ def self.accept_answer!(post, acting_user, topic: nil) topic.save! post.save! + if WebHook.active_web_hooks(:post).exists? + payload = WebHook.generate_payload(:post, post) + WebHook.enqueue_post_hooks(:post_edited, post, payload) + end + DiscourseEvent.trigger(:accepted_solution, post) end @@ -172,6 +177,12 @@ def self.unaccept_answer!(post, topic: nil) ) notification.destroy! if notification + + if WebHook.active_web_hooks(:post).exists? + payload = WebHook.generate_payload(:post, post) + WebHook.enqueue_post_hooks(:post_edited, post, payload) + end + DiscourseEvent.trigger(:unaccepted_solution, post) end end diff --git a/spec/integration/solved_spec.rb b/spec/integration/solved_spec.rb index fda575a0..3830d2e3 100644 --- a/spec/integration/solved_spec.rb +++ b/spec/integration/solved_spec.rb @@ -98,6 +98,17 @@ post "/solution/accept.json", params: { id: whisper.id } expect(response.status).to eq(403) end + + it 'triggers a webhook' do + Fabricate(:web_hook) + post "/solution/accept.json", params: { id: p1.id } + + job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first + + expect(job_args["event_name"]).to eq("post_edited") + payload = JSON.parse(job_args["payload"]) + expect(payload["id"]).to eq(p1.id) + end end describe '#unaccept' do @@ -122,6 +133,17 @@ expect(p1.custom_fields["is_accepted_answer"]).to eq(nil) expect(p1.topic.custom_fields["accepted_answer_post_id"]).to eq(nil) end + + it 'triggers a webhook' do + Fabricate(:web_hook) + post "/solution/unaccept.json", params: { id: p1.id } + + job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first + + expect(job_args["event_name"]).to eq("post_edited") + payload = JSON.parse(job_args["payload"]) + expect(payload["id"]).to eq(p1.id) + end end end end From 6707ee28641bb73a62182d88960ed274498f8ae6 Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Thu, 27 Feb 2020 12:10:18 -0700 Subject: [PATCH 2/7] Create Solved Event Webhook This commit adds a solved event webhook that will only trigger when an answer has been marked as accepted or unaccepted. It uses 100 as the webhook ID. This way any new webhooks in core can keep using lower numbers like 11, 12, 13, but plugins can use 101, 102, etc. --- config/locales/client.en.yml | 6 +++++ db/fixtures/002_web_hook_event_types.rb | 6 +++++ plugin.rb | 28 ++++++++++++++++++---- spec/fabricators/solved_hook_fabricator.rb | 9 +++++++ spec/integration/solved_spec.rb | 22 +++++++++-------- 5 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 db/fixtures/002_web_hook_event_types.rb create mode 100644 spec/fabricators/solved_hook_fabricator.rb diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 2bf9f6e6..cc399ac1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -27,3 +27,9 @@ en: topic_statuses: solved: help: "This topic has a solution" + + admin: + web_hooks: + solved_event: + name: "Solved Event" + details: "When a user marks a post as the accepted or unaccepted answer." diff --git a/db/fixtures/002_web_hook_event_types.rb b/db/fixtures/002_web_hook_event_types.rb new file mode 100644 index 00000000..bef0893d --- /dev/null +++ b/db/fixtures/002_web_hook_event_types.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +WebHookEventType.seed do |b| + b.id = WebHookEventType::SOLVED + b.name = "solved" +end diff --git a/plugin.rb b/plugin.rb index 6cc9296b..67f6879d 100644 --- a/plugin.rb +++ b/plugin.rb @@ -20,6 +20,11 @@ register_asset 'stylesheets/mobile/solutions.scss', :mobile after_initialize do + + class ::WebHookEventType + SOLVED = 100.freeze + end + SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s [ @@ -138,9 +143,9 @@ def self.accept_answer!(post, acting_user, topic: nil) topic.save! post.save! - if WebHook.active_web_hooks(:post).exists? + if WebHook.active_web_hooks(:solved).exists? payload = WebHook.generate_payload(:post, post) - WebHook.enqueue_post_hooks(:post_edited, post, payload) + WebHook.enqueue_solved_hooks(:accepted_solution, post, payload) end DiscourseEvent.trigger(:accepted_solution, post) @@ -178,9 +183,9 @@ def self.unaccept_answer!(post, topic: nil) notification.destroy! if notification - if WebHook.active_web_hooks(:post).exists? + if WebHook.active_web_hooks(:solved).exists? payload = WebHook.generate_payload(:post, post) - WebHook.enqueue_post_hooks(:post_edited, post, payload) + WebHook.enqueue_solved_hooks(:unaccepted_solution, post, payload) end DiscourseEvent.trigger(:unaccepted_solution, post) @@ -359,6 +364,21 @@ def solved_count end end + class ::WebHook + def self.enqueue_solved_hooks(event, post, payload = nil) + if active_web_hooks('solved').exists? && post.present? + payload ||= WebHook.generate_payload(:post, post) + + WebHook.enqueue_hooks(:solved, event, + id: post.id, + category_id: post.topic&.category_id, + tag_ids: post.topic&.tags&.pluck(:id), + payload: payload + ) + end + end + end + require_dependency 'topic_view_serializer' class ::TopicViewSerializer attributes :accepted_answer diff --git a/spec/fabricators/solved_hook_fabricator.rb b/spec/fabricators/solved_hook_fabricator.rb new file mode 100644 index 00000000..641fef8f --- /dev/null +++ b/spec/fabricators/solved_hook_fabricator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +Fabricator(:solved_web_hook, from: :web_hook) do + transient solved_hook: WebHookEventType.find_by(name: 'solved') + + after_build do |web_hook, transients| + web_hook.web_hook_event_types = [transients[:solved_hook]] + end +end diff --git a/spec/integration/solved_spec.rb b/spec/integration/solved_spec.rb index 3830d2e3..e39a87eb 100644 --- a/spec/integration/solved_spec.rb +++ b/spec/integration/solved_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'rails_helper' +require_relative '../fabricators/solved_hook_fabricator.rb' RSpec.describe "Managing Posts solved status" do let(:topic) { Fabricate(:topic) } @@ -100,12 +101,12 @@ end it 'triggers a webhook' do - Fabricate(:web_hook) + Fabricate(:solved_web_hook) post "/solution/accept.json", params: { id: p1.id } job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first - expect(job_args["event_name"]).to eq("post_edited") + expect(job_args["event_name"]).to eq("accepted_solution") payload = JSON.parse(job_args["payload"]) expect(payload["id"]).to eq(p1.id) end @@ -134,16 +135,17 @@ expect(p1.topic.custom_fields["accepted_answer_post_id"]).to eq(nil) end - it 'triggers a webhook' do - Fabricate(:web_hook) - post "/solution/unaccept.json", params: { id: p1.id } + end - job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first + it 'triggers a webhook' do + Fabricate(:solved_web_hook) + post "/solution/unaccept.json", params: { id: p1.id } - expect(job_args["event_name"]).to eq("post_edited") - payload = JSON.parse(job_args["payload"]) - expect(payload["id"]).to eq(p1.id) - end + job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first + + expect(job_args["event_name"]).to eq("unaccepted_solution") + payload = JSON.parse(job_args["payload"]) + expect(payload["id"]).to eq(p1.id) end end end From 57d77d2bf52d4ae0e9dc0502a29a13902d4e3586 Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Wed, 4 Mar 2020 15:28:49 -0700 Subject: [PATCH 3/7] Removed functionality that was added to core This [PR][1] to discourse core adds what what removed in this commit. It is better to have this logic in core so that it is discoverable and future webhooks won't end up accidentally using the same ID. [1]: https://github.com/discourse/discourse/pull/9110 --- db/fixtures/002_web_hook_event_types.rb | 6 ------ plugin.rb | 4 ---- 2 files changed, 10 deletions(-) delete mode 100644 db/fixtures/002_web_hook_event_types.rb diff --git a/db/fixtures/002_web_hook_event_types.rb b/db/fixtures/002_web_hook_event_types.rb deleted file mode 100644 index bef0893d..00000000 --- a/db/fixtures/002_web_hook_event_types.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -WebHookEventType.seed do |b| - b.id = WebHookEventType::SOLVED - b.name = "solved" -end diff --git a/plugin.rb b/plugin.rb index 67f6879d..6929ce99 100644 --- a/plugin.rb +++ b/plugin.rb @@ -21,10 +21,6 @@ after_initialize do - class ::WebHookEventType - SOLVED = 100.freeze - end - SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s [ From f4aea44db8e1a0af3140a7391c0c65cca49a7e8f Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 6 Mar 2020 08:57:38 +0530 Subject: [PATCH 4/7] UX: Add "solved" status filter in advanced search page. And rename `in:solved` to `status:solved`. --- .../initializers/extend-for-solved-button.js.es6 | 11 +++++++++++ config/locales/client.en.yml | 5 +++++ plugin.rb | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/assets/javascripts/discourse/initializers/extend-for-solved-button.js.es6 b/assets/javascripts/discourse/initializers/extend-for-solved-button.js.es6 index 5219f598..a3de8ca8 100644 --- a/assets/javascripts/discourse/initializers/extend-for-solved-button.js.es6 +++ b/assets/javascripts/discourse/initializers/extend-for-solved-button.js.es6 @@ -9,6 +9,7 @@ import PostCooked from "discourse/widgets/post-cooked"; import { formatUsername } from "discourse/lib/utilities"; import { iconHTML } from "discourse-common/lib/icon-library"; import { iconNode } from "discourse-common/lib/icon-library"; +import SearchAdvancedOptions from "discourse/components/search-advanced-options"; function clearAccepted(topic) { const posts = topic.get("postStream.posts"); @@ -262,6 +263,16 @@ export default { }) }); + SearchAdvancedOptions.reopen({ + didInsertElement() { + this._super(); + this.statusOptions.push({ + name: I18n.t("search.advanced.statuses.solved"), + value: "solved" + }); + } + }); + withPluginApi("0.1", initializeWithApi); withPluginApi("0.8.10", api => { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 2bf9f6e6..b7bf1d26 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -27,3 +27,8 @@ en: topic_statuses: solved: help: "This topic has a solution" + + search: + advanced: + statuses: + solved: "are solved" diff --git a/plugin.rb b/plugin.rb index 77d96015..19af807a 100644 --- a/plugin.rb +++ b/plugin.rb @@ -465,7 +465,7 @@ def accepted_answer #TODO Remove when plugin is 1.0 if Search.respond_to? :advanced_filter - Search.advanced_filter(/in:solved/) do |posts| + Search.advanced_filter(/status:solved/) do |posts| posts.where("topics.id IN ( SELECT tc.topic_id FROM topic_custom_fields tc @@ -475,7 +475,7 @@ def accepted_answer end - Search.advanced_filter(/in:unsolved/) do |posts| + Search.advanced_filter(/status:unsolved/) do |posts| posts.where("topics.id NOT IN ( SELECT tc.topic_id FROM topic_custom_fields tc From 6deba13fcc7dba5770cb94d111a93db64cdfe2d0 Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Tue, 25 Feb 2020 17:35:09 -0700 Subject: [PATCH 5/7] FEATURE: Publish WebHook event when solving/unsolving This feature will publish a post edit webhook event whenever a solution is accepted or unaccepted. I went ahead and used the existing post-edit webhook because all the post custom fields for the solved plugin are already included in the post-edit serializer. --- plugin.rb | 11 +++++++++++ spec/integration/solved_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/plugin.rb b/plugin.rb index 19af807a..c9659b30 100644 --- a/plugin.rb +++ b/plugin.rb @@ -138,6 +138,11 @@ def self.accept_answer!(post, acting_user, topic: nil) topic.save! post.save! + if WebHook.active_web_hooks(:post).exists? + payload = WebHook.generate_payload(:post, post) + WebHook.enqueue_post_hooks(:post_edited, post, payload) + end + DiscourseEvent.trigger(:accepted_solution, post) end @@ -172,6 +177,12 @@ def self.unaccept_answer!(post, topic: nil) ) notification.destroy! if notification + + if WebHook.active_web_hooks(:post).exists? + payload = WebHook.generate_payload(:post, post) + WebHook.enqueue_post_hooks(:post_edited, post, payload) + end + DiscourseEvent.trigger(:unaccepted_solution, post) end end diff --git a/spec/integration/solved_spec.rb b/spec/integration/solved_spec.rb index fda575a0..3830d2e3 100644 --- a/spec/integration/solved_spec.rb +++ b/spec/integration/solved_spec.rb @@ -98,6 +98,17 @@ post "/solution/accept.json", params: { id: whisper.id } expect(response.status).to eq(403) end + + it 'triggers a webhook' do + Fabricate(:web_hook) + post "/solution/accept.json", params: { id: p1.id } + + job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first + + expect(job_args["event_name"]).to eq("post_edited") + payload = JSON.parse(job_args["payload"]) + expect(payload["id"]).to eq(p1.id) + end end describe '#unaccept' do @@ -122,6 +133,17 @@ expect(p1.custom_fields["is_accepted_answer"]).to eq(nil) expect(p1.topic.custom_fields["accepted_answer_post_id"]).to eq(nil) end + + it 'triggers a webhook' do + Fabricate(:web_hook) + post "/solution/unaccept.json", params: { id: p1.id } + + job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first + + expect(job_args["event_name"]).to eq("post_edited") + payload = JSON.parse(job_args["payload"]) + expect(payload["id"]).to eq(p1.id) + end end end end From 122cbdd9fb7842f9d489211d85d27f00461cb98c Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Thu, 27 Feb 2020 12:10:18 -0700 Subject: [PATCH 6/7] Create Solved Event Webhook This commit adds a solved event webhook that will only trigger when an answer has been marked as accepted or unaccepted. It uses 100 as the webhook ID. This way any new webhooks in core can keep using lower numbers like 11, 12, 13, but plugins can use 101, 102, etc. --- config/locales/client.en.yml | 6 +++++ db/fixtures/002_web_hook_event_types.rb | 6 +++++ plugin.rb | 28 ++++++++++++++++++---- spec/fabricators/solved_hook_fabricator.rb | 9 +++++++ spec/integration/solved_spec.rb | 22 +++++++++-------- 5 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 db/fixtures/002_web_hook_event_types.rb create mode 100644 spec/fabricators/solved_hook_fabricator.rb diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index b7bf1d26..4361381a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -32,3 +32,9 @@ en: advanced: statuses: solved: "are solved" + + admin: + web_hooks: + solved_event: + name: "Solved Event" + details: "When a user marks a post as the accepted or unaccepted answer." diff --git a/db/fixtures/002_web_hook_event_types.rb b/db/fixtures/002_web_hook_event_types.rb new file mode 100644 index 00000000..bef0893d --- /dev/null +++ b/db/fixtures/002_web_hook_event_types.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +WebHookEventType.seed do |b| + b.id = WebHookEventType::SOLVED + b.name = "solved" +end diff --git a/plugin.rb b/plugin.rb index c9659b30..4b423c13 100644 --- a/plugin.rb +++ b/plugin.rb @@ -20,6 +20,11 @@ register_asset 'stylesheets/mobile/solutions.scss', :mobile after_initialize do + + class ::WebHookEventType + SOLVED = 100.freeze + end + SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s [ @@ -138,9 +143,9 @@ def self.accept_answer!(post, acting_user, topic: nil) topic.save! post.save! - if WebHook.active_web_hooks(:post).exists? + if WebHook.active_web_hooks(:solved).exists? payload = WebHook.generate_payload(:post, post) - WebHook.enqueue_post_hooks(:post_edited, post, payload) + WebHook.enqueue_solved_hooks(:accepted_solution, post, payload) end DiscourseEvent.trigger(:accepted_solution, post) @@ -178,9 +183,9 @@ def self.unaccept_answer!(post, topic: nil) notification.destroy! if notification - if WebHook.active_web_hooks(:post).exists? + if WebHook.active_web_hooks(:solved).exists? payload = WebHook.generate_payload(:post, post) - WebHook.enqueue_post_hooks(:post_edited, post, payload) + WebHook.enqueue_solved_hooks(:unaccepted_solution, post, payload) end DiscourseEvent.trigger(:unaccepted_solution, post) @@ -359,6 +364,21 @@ def solved_count end end + class ::WebHook + def self.enqueue_solved_hooks(event, post, payload = nil) + if active_web_hooks('solved').exists? && post.present? + payload ||= WebHook.generate_payload(:post, post) + + WebHook.enqueue_hooks(:solved, event, + id: post.id, + category_id: post.topic&.category_id, + tag_ids: post.topic&.tags&.pluck(:id), + payload: payload + ) + end + end + end + require_dependency 'topic_view_serializer' class ::TopicViewSerializer attributes :accepted_answer diff --git a/spec/fabricators/solved_hook_fabricator.rb b/spec/fabricators/solved_hook_fabricator.rb new file mode 100644 index 00000000..641fef8f --- /dev/null +++ b/spec/fabricators/solved_hook_fabricator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +Fabricator(:solved_web_hook, from: :web_hook) do + transient solved_hook: WebHookEventType.find_by(name: 'solved') + + after_build do |web_hook, transients| + web_hook.web_hook_event_types = [transients[:solved_hook]] + end +end diff --git a/spec/integration/solved_spec.rb b/spec/integration/solved_spec.rb index 3830d2e3..e39a87eb 100644 --- a/spec/integration/solved_spec.rb +++ b/spec/integration/solved_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'rails_helper' +require_relative '../fabricators/solved_hook_fabricator.rb' RSpec.describe "Managing Posts solved status" do let(:topic) { Fabricate(:topic) } @@ -100,12 +101,12 @@ end it 'triggers a webhook' do - Fabricate(:web_hook) + Fabricate(:solved_web_hook) post "/solution/accept.json", params: { id: p1.id } job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first - expect(job_args["event_name"]).to eq("post_edited") + expect(job_args["event_name"]).to eq("accepted_solution") payload = JSON.parse(job_args["payload"]) expect(payload["id"]).to eq(p1.id) end @@ -134,16 +135,17 @@ expect(p1.topic.custom_fields["accepted_answer_post_id"]).to eq(nil) end - it 'triggers a webhook' do - Fabricate(:web_hook) - post "/solution/unaccept.json", params: { id: p1.id } + end - job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first + it 'triggers a webhook' do + Fabricate(:solved_web_hook) + post "/solution/unaccept.json", params: { id: p1.id } - expect(job_args["event_name"]).to eq("post_edited") - payload = JSON.parse(job_args["payload"]) - expect(payload["id"]).to eq(p1.id) - end + job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first + + expect(job_args["event_name"]).to eq("unaccepted_solution") + payload = JSON.parse(job_args["payload"]) + expect(payload["id"]).to eq(p1.id) end end end From 1026f37af777010295ee111af5e574ed347a14df Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Wed, 4 Mar 2020 15:28:49 -0700 Subject: [PATCH 7/7] Removed functionality that was added to core This [PR][1] to discourse core adds what what removed in this commit. It is better to have this logic in core so that it is discoverable and future webhooks won't end up accidentally using the same ID. [1]: https://github.com/discourse/discourse/pull/9110 --- db/fixtures/002_web_hook_event_types.rb | 6 ------ plugin.rb | 4 ---- 2 files changed, 10 deletions(-) delete mode 100644 db/fixtures/002_web_hook_event_types.rb diff --git a/db/fixtures/002_web_hook_event_types.rb b/db/fixtures/002_web_hook_event_types.rb deleted file mode 100644 index bef0893d..00000000 --- a/db/fixtures/002_web_hook_event_types.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -WebHookEventType.seed do |b| - b.id = WebHookEventType::SOLVED - b.name = "solved" -end diff --git a/plugin.rb b/plugin.rb index 4b423c13..26c23789 100644 --- a/plugin.rb +++ b/plugin.rb @@ -21,10 +21,6 @@ after_initialize do - class ::WebHookEventType - SOLVED = 100.freeze - end - SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s [