Extract sending new follower notification to a separate job (#2030)
This commit is contained in:
parent
c92c77396c
commit
f0e9c0cfef
11 changed files with 304 additions and 39 deletions
1
Gemfile
1
Gemfile
|
|
@ -38,6 +38,7 @@ gem "delayed_job_active_record", "~> 4.1"
|
|||
gem "delayed_job_web", "~> 1.4"
|
||||
gem "devise", "~> 4.6"
|
||||
gem "draper", "~> 3.0"
|
||||
gem "dry-struct", "~> 0.6"
|
||||
gem "email_validator", "~> 2.0"
|
||||
gem "emoji_regex", "~> 1.0"
|
||||
gem "envied", "~> 0.9"
|
||||
|
|
|
|||
27
Gemfile.lock
27
Gemfile.lock
|
|
@ -269,6 +269,32 @@ GEM
|
|||
activemodel-serializers-xml (~> 1.0)
|
||||
activesupport (~> 5.0)
|
||||
request_store (~> 1.0)
|
||||
dry-configurable (0.8.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 0.4, >= 0.4.7)
|
||||
dry-container (0.7.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-configurable (~> 0.1, >= 0.1.3)
|
||||
dry-core (0.4.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-equalizer (0.2.2)
|
||||
dry-inflector (0.1.2)
|
||||
dry-logic (0.5.0)
|
||||
dry-container (~> 0.2, >= 0.2.6)
|
||||
dry-core (~> 0.2)
|
||||
dry-equalizer (~> 0.2)
|
||||
dry-struct (0.6.0)
|
||||
dry-core (~> 0.4, >= 0.4.3)
|
||||
dry-equalizer (~> 0.2)
|
||||
dry-types (~> 0.13)
|
||||
ice_nine (~> 0.11)
|
||||
dry-types (0.14.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-container (~> 0.3)
|
||||
dry-core (~> 0.4, >= 0.4.4)
|
||||
dry-equalizer (~> 0.2)
|
||||
dry-inflector (~> 0.1, >= 0.1.2)
|
||||
dry-logic (~> 0.5, >= 0.5)
|
||||
em-websocket (0.5.1)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
|
|
@ -981,6 +1007,7 @@ DEPENDENCIES
|
|||
derailed_benchmarks (~> 1.3)
|
||||
devise (~> 4.6)
|
||||
draper (~> 3.0)
|
||||
dry-struct (~> 0.6)
|
||||
email_validator (~> 2.0)
|
||||
emoji_regex (~> 1.0)
|
||||
envied (~> 0.9)
|
||||
|
|
|
|||
9
app/jobs/notifications/new_follower_job.rb
Normal file
9
app/jobs/notifications/new_follower_job.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
module Notifications
|
||||
class NewFollowerJob < ApplicationJob
|
||||
queue_as :send_new_follower_notification
|
||||
|
||||
def perform(follow_data, is_read = false, service = Notifications::NewFollower::Send)
|
||||
service.call(follow_data, is_read)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -14,31 +14,14 @@ class Notification < ApplicationRecord
|
|||
def send_new_follower_notification(follow, is_read = false)
|
||||
return unless Follow.need_new_follower_notification_for?(follow.followable_type)
|
||||
|
||||
recent_follows = Follow.where(followable_type: follow.followable_type, followable_id: follow.followable_id).where("created_at > ?", 24.hours.ago).order("created_at DESC")
|
||||
|
||||
notification_params = { action: "Follow" }
|
||||
if follow.followable_type == "User"
|
||||
notification_params[:user_id] = follow.followable_id
|
||||
elsif follow.followable_type == "Organization"
|
||||
notification_params[:organization_id] = follow.followable_id
|
||||
end
|
||||
|
||||
followers = User.where(id: recent_follows.pluck(:follower_id))
|
||||
aggregated_siblings = followers.map { |follower| user_data(follower) }
|
||||
if aggregated_siblings.size.zero?
|
||||
Notification.find_or_create_by(notification_params).destroy
|
||||
else
|
||||
json_data = { user: user_data(follow.follower), aggregated_siblings: aggregated_siblings }
|
||||
notification = Notification.find_or_create_by(notification_params)
|
||||
notification.notifiable_id = recent_follows.first.id
|
||||
notification.notifiable_type = "Follow"
|
||||
notification.json_data = json_data
|
||||
notification.notified_at = Time.current
|
||||
notification.read = is_read
|
||||
notification.save!
|
||||
end
|
||||
follow_data = follow.attributes.slice("follower_id", "followable_id", "followable_type").symbolize_keys
|
||||
Notifications::NewFollowerJob.perform_later(follow_data, is_read)
|
||||
end
|
||||
|
||||
def send_new_follower_notification_without_delay(follow, is_read = false)
|
||||
follow_data = follow.attributes.slice("follower_id", "followable_id", "followable_type").symbolize_keys
|
||||
Notifications::NewFollowerJob.perform_now(follow_data, is_read)
|
||||
end
|
||||
handle_asynchronously :send_new_follower_notification
|
||||
|
||||
def send_to_followers(notifiable, action = nil)
|
||||
# for now, arguments are always: notifiable = article, action = "Published"
|
||||
|
|
@ -322,16 +305,7 @@ class Notification < ApplicationRecord
|
|||
private
|
||||
|
||||
def user_data(user)
|
||||
{
|
||||
id: user.id,
|
||||
class: { name: "User" },
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
path: user.path,
|
||||
profile_image_90: user.profile_image_90,
|
||||
comments_count: user.comments_count,
|
||||
created_at: user.created_at
|
||||
}
|
||||
Notifications.user_data(user)
|
||||
end
|
||||
|
||||
def organization_data(organization)
|
||||
|
|
|
|||
14
app/services/notifications.rb
Normal file
14
app/services/notifications.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
module Notifications
|
||||
def self.user_data(user)
|
||||
{
|
||||
id: user.id,
|
||||
class: { name: "User" },
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
path: user.path,
|
||||
profile_image_90: user.profile_image_90,
|
||||
comments_count: user.comments_count,
|
||||
created_at: user.created_at
|
||||
}
|
||||
end
|
||||
end
|
||||
16
app/services/notifications/new_follower/follow_data.rb
Normal file
16
app/services/notifications/new_follower/follow_data.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
require "dry-types"
|
||||
require "dry-struct"
|
||||
|
||||
module Notifications
|
||||
module NewFollower
|
||||
module Types
|
||||
include Dry::Types.module
|
||||
end
|
||||
|
||||
class FollowData < Dry::Struct
|
||||
attribute :followable_id, Types::Strict::Integer
|
||||
attribute :followable_type, Types::Strict::String.enum("User", "Organization")
|
||||
attribute :follower_id, Types::Strict::Integer
|
||||
end
|
||||
end
|
||||
end
|
||||
60
app/services/notifications/new_follower/send.rb
Normal file
60
app/services/notifications/new_follower/send.rb
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# send notifications about the new followers
|
||||
module Notifications
|
||||
module NewFollower
|
||||
class Send
|
||||
# @param follow_data [Hash]
|
||||
# * :followable_id [Integer]
|
||||
# * :followable_type [String] - "User" or "Organization"
|
||||
# * :follower_id [Integer] - user id
|
||||
def initialize(follow_data, is_read = false)
|
||||
follow_data = follow_data.is_a?(FollowData) ? follow_data : FollowData.new(follow_data)
|
||||
@followable_id = follow_data.followable_id # fetch(:followable_id)
|
||||
@followable_type = follow_data.followable_type # fetch(:followable_type)
|
||||
@follower_id = follow_data.follower_id # fetch(:follower_id)
|
||||
@is_read = is_read
|
||||
end
|
||||
|
||||
delegate :user_data, to: Notifications
|
||||
|
||||
def self.call(*args)
|
||||
new(*args).call
|
||||
end
|
||||
|
||||
def call
|
||||
recent_follows = Follow.where(followable_type: followable_type, followable_id: followable_id).
|
||||
where("created_at > ?", 24.hours.ago).order("created_at DESC")
|
||||
|
||||
notification_params = { action: "Follow" }
|
||||
if followable_type == "User"
|
||||
notification_params[:user_id] = followable_id
|
||||
elsif followable_type == "Organization"
|
||||
notification_params[:organization_id] = followable_id
|
||||
end
|
||||
|
||||
followers = User.where(id: recent_follows.pluck(:follower_id))
|
||||
aggregated_siblings = followers.map { |f| user_data(f) }
|
||||
if aggregated_siblings.size.zero?
|
||||
notification = Notification.find_by(notification_params)&.destroy
|
||||
else
|
||||
json_data = { user: user_data(follower), aggregated_siblings: aggregated_siblings }
|
||||
notification = Notification.find_or_create_by(notification_params)
|
||||
notification.notifiable_id = recent_follows.first.id
|
||||
notification.notifiable_type = "Follow"
|
||||
notification.json_data = json_data
|
||||
notification.notified_at = Time.current
|
||||
notification.read = is_read
|
||||
notification.save!
|
||||
end
|
||||
notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :followable_id, :followable_type, :follower_id, :is_read
|
||||
|
||||
def follower
|
||||
User.find(follower_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
"git add"
|
||||
],
|
||||
"{app,spec}/**/*.rb": [
|
||||
"bin/rubocop --require rubocop-rspec --safe-auto-correct",
|
||||
"bundle exec rubocop --require rubocop-rspec --safe-auto-correct",
|
||||
"git add"
|
||||
],
|
||||
"app/**/*.html.erb": [
|
||||
|
|
|
|||
24
spec/jobs/notifications/new_follower_job_spec.rb
Normal file
24
spec/jobs/notifications/new_follower_job_spec.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Notifications::NewFollowerJob, type: :job do
|
||||
let(:follow_data) { { followable_type: "User", followable_id: 1, follower_id: 2 } }
|
||||
|
||||
describe "#perform_later" do
|
||||
it "enqueues the job" do
|
||||
ActiveJob::Base.queue_adapter = :test
|
||||
expect do
|
||||
described_class.perform_later(follow_data, true)
|
||||
end.to have_enqueued_job.with(follow_data, true).on_queue("send_new_follower_notification")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#perform_now" do
|
||||
it "calls the service" do
|
||||
new_follower_service = double
|
||||
allow(new_follower_service).to receive(:call)
|
||||
|
||||
described_class.perform_now(follow_data, false, new_follower_service)
|
||||
expect(new_follower_service).to have_received(:call).with(follow_data, false).once
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -29,9 +29,8 @@ RSpec.describe Notification, type: :model do
|
|||
|
||||
describe "#send_new_follower_notification" do
|
||||
before do
|
||||
run_background_jobs_immediately do
|
||||
Notification.send_new_follower_notification(follow_instance)
|
||||
end
|
||||
ActiveJob::Base.queue_adapter = :inline
|
||||
Notification.send_new_follower_notification(follow_instance)
|
||||
end
|
||||
|
||||
it "sets the notifiable_at column upon creation" do
|
||||
|
|
@ -71,7 +70,8 @@ RSpec.describe Notification, type: :model do
|
|||
context "when a user unfollows another user" do
|
||||
it "destroys the follow notification" do
|
||||
follow_instance = user.stop_following(user2)
|
||||
run_background_jobs_immediately { Notification.send_new_follower_notification(follow_instance) }
|
||||
ActiveJob::Base.queue_adapter = :inline
|
||||
Notification.send_new_follower_notification(follow_instance)
|
||||
expect(Notification.count).to eq 0
|
||||
end
|
||||
end
|
||||
|
|
|
|||
140
spec/services/notifications/new_follower/send_spec.rb
Normal file
140
spec/services/notifications/new_follower/send_spec.rb
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Notifications::NewFollower::Send, type: :service do
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:user3) { create(:user) }
|
||||
let(:organization) { create(:organization) }
|
||||
let(:article) { create(:article, user_id: user.id, page_views_count: 4000, positive_reactions_count: 70) }
|
||||
let!(:follow) { user.follow(user2) }
|
||||
|
||||
def follow_data(follow)
|
||||
{
|
||||
followable_id: follow.followable_id,
|
||||
followable_type: follow.followable_type,
|
||||
follower_id: follow.follower_id
|
||||
}
|
||||
end
|
||||
|
||||
context "when trying to pass tag follow data" do
|
||||
it "raises an exception" do
|
||||
tag = create(:tag)
|
||||
tag_follow = user.follow(tag)
|
||||
expect do
|
||||
described_class.call(follow_data(tag_follow))
|
||||
end.to raise_error(Dry::Struct::Error)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user follows another user" do
|
||||
it "creates a notification" do
|
||||
expect do
|
||||
described_class.call(follow_data(follow))
|
||||
end.to change(Notification, :count).by(1)
|
||||
end
|
||||
|
||||
it "creates a notification with data" do
|
||||
notification = described_class.call(follow_data(follow))
|
||||
expect(notification.notifiable).to eq(follow)
|
||||
expect(notification.notified_at).not_to be_nil
|
||||
expect(notification.read).to be_falsey
|
||||
expect(notification.json_data["user"]["id"]).to eq(user.id)
|
||||
end
|
||||
|
||||
it "creates a read notification" do
|
||||
notification = described_class.call(follow_data(follow), true)
|
||||
expect(notification.read).to be_truthy
|
||||
end
|
||||
|
||||
context "when destroyed follow" do
|
||||
let(:unfollow) { user.stop_following(user2) }
|
||||
|
||||
it "does not create a notification" do
|
||||
expect do
|
||||
described_class.call(follow_data(unfollow))
|
||||
end.not_to change(Notification, :count)
|
||||
end
|
||||
|
||||
it "destroys notification if it exists" do
|
||||
create(:notification, action: "Follow", user: user2, notifiable: follow, notified_at: Time.now - 1.year)
|
||||
expect do
|
||||
described_class.call(follow_data(unfollow))
|
||||
end.to change(Notification, :count).by(-1)
|
||||
end
|
||||
|
||||
it "destroys the correct notification" do
|
||||
notification = create(:notification, action: "Follow", user: user2, notifiable: follow, notified_at: Time.now - 1.year)
|
||||
described_class.call(follow_data(unfollow))
|
||||
expect(Notification.where(id: notification.id)).not_to exist
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when 2 user follows another user" do
|
||||
let!(:follow2) { user3.follow user2 }
|
||||
let(:follower_data) do
|
||||
{
|
||||
"id" => user3.id,
|
||||
"class" => { "name" => "User" },
|
||||
"name" => user3.name,
|
||||
"username" => user3.username,
|
||||
"path" => user3.path,
|
||||
"profile_image_90" => user3.profile_image_90,
|
||||
"comments_count" => user3.comments_count
|
||||
}
|
||||
end
|
||||
|
||||
it "creates a notification" do
|
||||
expect do
|
||||
described_class.call(follow_data(follow2))
|
||||
end.to change(Notification, :count).by(1)
|
||||
end
|
||||
|
||||
it "creates a notification with data" do
|
||||
notification = described_class.call(follow_data(follow2))
|
||||
expect(notification.notifiable).to eq(follow2)
|
||||
expect(notification.notified_at).not_to be_nil
|
||||
expect(notification.json_data["aggregated_siblings"].map { |j| j["id"] }.sort).to eq([user.id, user3.id].sort)
|
||||
end
|
||||
|
||||
it "creates a notification with user data" do
|
||||
notification = described_class.call(follow_data(follow2))
|
||||
expect(notification.json_data["user"]["name"]).to eq(user3.name)
|
||||
expect(notification.json_data["user"]["username"]).to eq(user3.username)
|
||||
expect(notification.json_data["user"]["id"]).to eq(user3.id)
|
||||
expect(notification.json_data["user"]["class"]).to eq("name" => "User")
|
||||
end
|
||||
|
||||
context "when notification exists" do
|
||||
before do
|
||||
create(:notification, user: user2, action: "Follow", notifiable: follow, notified_at: Time.now - 1.year)
|
||||
end
|
||||
|
||||
it "does not create a notification" do
|
||||
expect do
|
||||
described_class.call(follow_data(follow2))
|
||||
end.not_to change(Notification, :count)
|
||||
end
|
||||
|
||||
it "updates a notification" do
|
||||
notification = described_class.call(follow_data(follow2))
|
||||
expect(notification.notifiable).to eq(follow2)
|
||||
expect(notification.notified_at).to be >= Time.now - 1.day
|
||||
end
|
||||
end
|
||||
|
||||
context "when destroyed follow and notification exists" do
|
||||
let(:unfollow) { user.stop_following(user2) }
|
||||
|
||||
before do
|
||||
create(:notification, action: "Follow", user: user2, notifiable: follow, notified_at: Time.now - 1.year)
|
||||
end
|
||||
|
||||
it "does not destroy a notification" do
|
||||
expect do
|
||||
described_class.call(follow_data(unfollow))
|
||||
end.not_to change(Notification, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue