mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-11-10 17:36:33 +08:00
Implement webhook service and scheduling jobs [SCI-5801, SCI-5802]
This commit is contained in:
parent
6cf9ea5bc0
commit
4ee50f87d5
8 changed files with 283 additions and 2 deletions
19
app/jobs/activities/dispatch_webhooks_jobs.rb
Normal file
19
app/jobs/activities/dispatch_webhooks_jobs.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Activities
|
||||
class DispatchWebhooksJobs < ApplicationJob
|
||||
queue_as :high_priority
|
||||
|
||||
def perform(activity)
|
||||
webhooks =
|
||||
Webhook.where(
|
||||
activity_filter_id:
|
||||
Activities::ActivityFilterMatchingService.new(activity).activity_filters.select(:id)
|
||||
)
|
||||
|
||||
webhooks.each do |webhook|
|
||||
Activities::SendWebhookJob.perform_later(webhook, activity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
app/jobs/activities/send_webhook_job.rb
Normal file
11
app/jobs/activities/send_webhook_job.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Activities
|
||||
class SendWebhookJob < ApplicationJob
|
||||
queue_as :high_priority
|
||||
|
||||
def perform(webhook, activity)
|
||||
Activities::WebhookService.new(webhook, activity).send_webhook
|
||||
end
|
||||
end
|
||||
end
|
|
@ -63,6 +63,8 @@ class Activity < ApplicationRecord
|
|||
breadcrumbs: {}
|
||||
)
|
||||
|
||||
after_create :dispatch_webhooks
|
||||
|
||||
def self.activity_types_list
|
||||
activity_list = type_ofs.map do |key, value|
|
||||
[
|
||||
|
@ -151,4 +153,8 @@ class Activity < ApplicationRecord
|
|||
def activity_version
|
||||
errors.add(:activity, 'wrong combination of associations') if (experiment_id || my_module_id) && subject
|
||||
end
|
||||
|
||||
def dispatch_webhooks
|
||||
Activities::DispatchWebhooksJobs.perform_later(self)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,8 @@ class Webhook < ApplicationRecord
|
|||
validates :url, presence: true
|
||||
validate :valid_url
|
||||
|
||||
scope :active, -> { where(active: true) }
|
||||
|
||||
private
|
||||
|
||||
def valid_url
|
||||
|
|
56
app/services/activities/webhook_service.rb
Normal file
56
app/services/activities/webhook_service.rb
Normal file
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Activities
|
||||
class WebhookService
|
||||
DISABLE_WEBHOOK_ERROR_THRESHOLD = 10
|
||||
|
||||
def initialize(webhook, activity)
|
||||
@webhook = webhook
|
||||
@activity = activity
|
||||
end
|
||||
|
||||
def send_webhook
|
||||
raise "Cannot send inactive webhook." unless @webhook.active?
|
||||
|
||||
response = HTTParty.send(
|
||||
@webhook.method,
|
||||
@webhook.url,
|
||||
{
|
||||
headers: { 'Content-Type' => 'application/json' },
|
||||
body: activity_payload
|
||||
}
|
||||
)
|
||||
|
||||
unless response.success?
|
||||
log_error!("#{response.status}: #{response.message}")
|
||||
end
|
||||
rescue Net::ReadTimeout, SocketError => error
|
||||
log_error!(error)
|
||||
ensure
|
||||
disable_webhook_if_broken!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def activity_payload
|
||||
@activity.values.merge(
|
||||
type: @activity.type_of,
|
||||
created_at: @activity.created_at
|
||||
)
|
||||
end
|
||||
|
||||
def log_error!(message)
|
||||
error_count = @webhook.error_count + 1
|
||||
|
||||
@webhook.update(
|
||||
error_count: error_count,
|
||||
last_error: message
|
||||
)
|
||||
end
|
||||
|
||||
def disable_webhook_if_broken!
|
||||
return if @webhook.error_count < DISABLE_WEBHOOK_ERROR_THRESHOLD
|
||||
@webhook.update(active: false)
|
||||
end
|
||||
end
|
||||
end
|
6
db/migrate/20210616071836_add_error_info_to_webhooks.rb
Normal file
6
db/migrate/20210616071836_add_error_info_to_webhooks.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class AddErrorInfoToWebhooks < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :webhooks, :error_count, :integer, default: 0, null: false
|
||||
add_column :webhooks, :last_error, :text
|
||||
end
|
||||
end
|
|
@ -2801,7 +2801,9 @@ CREATE TABLE public.webhooks (
|
|||
url character varying NOT NULL,
|
||||
method integer NOT NULL,
|
||||
created_at timestamp(6) without time zone NOT NULL,
|
||||
updated_at timestamp(6) without time zone NOT NULL
|
||||
updated_at timestamp(6) without time zone NOT NULL,
|
||||
error_count integer DEFAULT 0 NOT NULL,
|
||||
last_error text
|
||||
);
|
||||
|
||||
|
||||
|
@ -7348,6 +7350,7 @@ INSERT INTO "schema_migrations" (version) VALUES
|
|||
('20210410100006'),
|
||||
('20210506125657'),
|
||||
('20210531114633'),
|
||||
('20210603152345');
|
||||
('20210603152345'),
|
||||
('20210616071836');
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Activities::ActivityFilterMatchingService do
|
||||
let(:user) { create :user }
|
||||
let(:user_2) { create :user }
|
||||
let(:team) { create :team, :with_members }
|
||||
let(:team_2) { create :team }
|
||||
let(:project) do
|
||||
create :project, team: team, user_projects: []
|
||||
end
|
||||
let(:project_2) do
|
||||
create :project, team: team, user_projects: []
|
||||
end
|
||||
let(:activity) { create :activity }
|
||||
|
||||
it 'matches activity filters by activity date' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "date filter",
|
||||
filter: {"to_date"=>"2021-1-2", "from_date"=>"2021-1-1"}
|
||||
)
|
||||
|
||||
non_matching_filter = ActivityFilter.create(
|
||||
name: "date filter",
|
||||
filter: {"to_date"=>"2021-12-2", "from_date"=>"2021-12-1"}
|
||||
)
|
||||
|
||||
activity.update_column(:created_at, Date.parse("2021-1-1").to_time)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter)
|
||||
end
|
||||
|
||||
it 'matches activity filters by activity user' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "user filter 1",
|
||||
filter: {"users" => [user.id.to_s], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
non_matching_filter = ActivityFilter.create(
|
||||
name: "user filter 2",
|
||||
filter: {"users" => [user_2.id.to_s], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
activity.update_column(:owner_id, user.id)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter)
|
||||
end
|
||||
|
||||
it 'matches activity filters by activity type' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "type filter 1",
|
||||
filter: {"types" => ["163"], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
non_matching_filter = ActivityFilter.create(
|
||||
name: "type filter 2",
|
||||
filter: {"types" => ["0"], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
activity.update_column(:type_of, 163)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter)
|
||||
end
|
||||
|
||||
it 'matches activity filters by activity team' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "team filter 1",
|
||||
filter: {"teams" => [team.id.to_s], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
non_matching_filter = ActivityFilter.create(
|
||||
name: "team filter 2",
|
||||
filter: {"teams" => [team_2.id.to_s], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
activity.update_column(:team_id, team.id)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter)
|
||||
end
|
||||
|
||||
it 'matches activity filters by activity subject' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "subject filter 1",
|
||||
filter: {"subjects" => { "Project" => [project.id.to_s] }, "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
non_matching_filter = ActivityFilter.create(
|
||||
name: "subject filter 2",
|
||||
filter: {"subjects" => { "Project" => [project_2.id.to_s] }, "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
activity.update_columns(subject_type: "Project", subject_id: project.id)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter)
|
||||
end
|
||||
|
||||
it 'matches activity filters by a combination of filters' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "mixed filter 1",
|
||||
filter: {
|
||||
"subjects" => { "Project" => [project.id.to_s] },
|
||||
"to_date"=>"2021-1-2",
|
||||
"from_date"=>"2021-1-1",
|
||||
"teams"=>[team.id.to_s],
|
||||
"users"=>[user.id.to_s],
|
||||
"types"=>["163"]
|
||||
}
|
||||
)
|
||||
|
||||
activity.update_columns(
|
||||
created_at: Date.parse("2021-1-1").to_time,
|
||||
owner_id: user.id,
|
||||
type_of: 163,
|
||||
subject_type: "Project",
|
||||
subject_id: project.id,
|
||||
team_id: team.id
|
||||
)
|
||||
|
||||
non_matching_filter_1 = ActivityFilter.create(
|
||||
name: "mixed filter 1",
|
||||
filter: {
|
||||
"subjects" => { "Project" => [project.id.to_s] },
|
||||
"to_date"=>"2021-10-2",
|
||||
"from_date"=>"2021-10-1",
|
||||
"teams"=>[team.id.to_s],
|
||||
"users"=>[user.id.to_s],
|
||||
"types"=>["163"]
|
||||
}
|
||||
)
|
||||
|
||||
non_matching_filter_2 = ActivityFilter.create(
|
||||
name: "mixed filter 2",
|
||||
filter: {
|
||||
"subjects" => { "Project" => [project.id.to_s] },
|
||||
"to_date"=>"2021-1-2",
|
||||
"from_date"=>"2021-1-1",
|
||||
"teams"=>[team_2.id.to_s],
|
||||
"users"=>[user.id.to_s],
|
||||
"types"=>["163"]
|
||||
}
|
||||
)
|
||||
|
||||
non_matching_filter_3 = ActivityFilter.create(
|
||||
name: "mixed filter 3",
|
||||
filter: {
|
||||
"subjects" => { "Project" => [project_2.id.to_s] },
|
||||
"to_date"=>"2021-1-2",
|
||||
"from_date"=>"2021-1-1",
|
||||
"teams"=>[team.id.to_s],
|
||||
"users"=>[user.id.to_s],
|
||||
"types"=>["163"]
|
||||
}
|
||||
)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter_1)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter_2)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter_3)
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue