Add ActiveRecord::Base.suppress

This commit is contained in:
Michael Ryan 2015-02-18 17:55:48 -05:00
parent 83be86933d
commit b9a1e9a4b2
8 changed files with 118 additions and 0 deletions

@ -1,3 +1,32 @@
* Add ActiveRecord::Base.suppress to prevent the receiver from being saved
during the given block.
For example, here's a pattern of creating notifications when new comments
are posted. (The notification may in turn trigger an email, a push
notification, or just appear in the UI somewhere):
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
after_create -> { Notification.create! comment: self,
recipients: commentable.recipients }
end
That's what you want the bulk of the time. New comment creates a new
Notification. But there may well be off cases, like copying a commentable
and its comments, where you don't want that. So you'd have a concern
something like this:
module Copyable
def copy_to(destination)
Notification.suppress do
# Copy logic that creates new comments that we do not want triggering
# notifications.
end
end
end
*Michael Ryan*
* `:time` option added for `#touch`
Fixes #18905.

@ -62,6 +62,7 @@ module ActiveRecord
autoload :Serialization
autoload :StatementCache
autoload :Store
autoload :Suppressor
autoload :TableMetadata
autoload :Timestamp
autoload :Transactions

@ -313,6 +313,7 @@ class Base
include Serialization
include Store
include SecureToken
include Suppressor
end
ActiveSupport.run_load_hooks(:active_record, Base)

@ -0,0 +1,55 @@
module ActiveRecord
# ActiveRecord::Suppressor prevents the receiver from being saved during
# a given block.
#
# For example, here's a pattern of creating notifications when new comments
# are posted. (The notification may in turn trigger an email, a push
# notification, or just appear in the UI somewhere):
#
# class Comment < ActiveRecord::Base
# belongs_to :commentable, polymorphic: true
# after_create -> { Notification.create! comment: self,
# recipients: commentable.recipients }
# end
#
# That's what you want the bulk of the time. New comment creates a new
# Notification. But there may well be off cases, like copying a commentable
# and its comments, where you don't want that. So you'd have a concern
# something like this:
#
# module Copyable
# def copy_to(destination)
# Notification.suppress do
# # Copy logic that creates new comments that we do not want
# # triggering notifications.
# end
# end
# end
module Suppressor
extend ActiveSupport::Concern
module ClassMethods
def suppress(&block)
SuppressorRegistry.suppressed[name] = true
yield
ensure
SuppressorRegistry.suppressed[name] = false
end
end
# Ignore saving events if we're in suppression mode.
def save!(*args)
SuppressorRegistry.suppressed[self.class.name] ? self : super
end
end
class SuppressorRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
attr_reader :suppressed
def initialize
@suppressed = {}
end
end
end

@ -0,0 +1,22 @@
require 'cases/helper'
require 'models/notification'
require 'models/user'
class SuppressorTest < ActiveRecord::TestCase
def test_suppresses_creation_of_record_generated_by_callback
assert_difference -> { User.count } do
assert_no_difference -> { Notification.count } do
Notification.suppress { UserWithNotification.create! }
end
end
end
def test_resumes_saving_after_suppression_complete
Notification.suppress { UserWithNotification.create! }
assert_difference -> { Notification.count } do
Notification.create!
end
end
end

@ -0,0 +1,2 @@
class Notification < ActiveRecord::Base
end

@ -2,3 +2,7 @@ class User < ActiveRecord::Base
has_secure_token
has_secure_token :auth_token
end
class UserWithNotification < User
after_create -> { Notification.create! message: "A new user has been created." }
end

@ -468,6 +468,10 @@ def except(adapter_names_to_exclude)
t.string :name
end
create_table :notifications, force: true do |t|
t.string :message
end
create_table :numeric_data, force: true do |t|
t.decimal :bank_balance, precision: 10, scale: 2
t.decimal :big_bank_balance, precision: 15, scale: 2