Extract sending new follower notification to a separate job (#2030)

This commit is contained in:
Anna Buianova 2019-03-20 19:51:09 +03:00 committed by Mac Siri
parent c92c77396c
commit f0e9c0cfef
11 changed files with 304 additions and 39 deletions

View file

@ -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"

View file

@ -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)

View 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

View file

@ -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)

View 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

View 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

View 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

View file

@ -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": [

View 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

View file

@ -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

View 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