Remove observers and sweepers
They was extracted from a plugin. See https://github.com/rails/rails-observers [Rafael Mendonça França + Steve Klabnik]
This commit is contained in:
parent
e38d310912
commit
ccecab3ba9
|
@ -6,10 +6,10 @@ module ActionController
|
|||
# \Caching is a cheap way of speeding up slow applications by keeping the result of
|
||||
# calculations, renderings, and database calls around for subsequent requests.
|
||||
#
|
||||
# You can read more about each approach and the sweeping assistance by clicking the
|
||||
# You can read more about each approach and the by clicking the
|
||||
# modules below.
|
||||
#
|
||||
# Note: To turn off all caching and sweeping, set
|
||||
# Note: To turn off all caching, set
|
||||
# config.action_controller.perform_caching = false.
|
||||
#
|
||||
# == \Caching stores
|
||||
|
@ -30,8 +30,6 @@ module ActionController
|
|||
|
||||
eager_autoload do
|
||||
autoload :Fragments
|
||||
autoload :Sweeper, 'action_controller/caching/sweeping'
|
||||
autoload :Sweeping, 'action_controller/caching/sweeping'
|
||||
end
|
||||
|
||||
module ConfigMethods
|
||||
|
@ -54,7 +52,6 @@ module ActionController
|
|||
|
||||
include ConfigMethods
|
||||
include Fragments
|
||||
include Sweeping if defined?(ActiveRecord)
|
||||
|
||||
included do
|
||||
extend ConfigMethods
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
module ActionController
|
||||
module Caching
|
||||
# Sweepers are the terminators of the caching world and responsible for expiring
|
||||
# caches when Active Record objects change. They do this by being half-observers,
|
||||
# half-filters and implementing callbacks for both roles.
|
||||
#
|
||||
# class ListSweeper < ActionController::Caching::Sweeper
|
||||
# observe List, Item
|
||||
#
|
||||
# def after_save(record)
|
||||
# list = record.is_a?(List) ? record : record.list
|
||||
# expire_page(controller: 'lists', action: %w( show public feed ), id: list.id)
|
||||
# expire_action(controller: 'lists', action: 'all')
|
||||
# list.shares.each { |share| expire_page(controller: 'lists', action: 'show', id: share.url_key) }
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The sweeper is assigned in the controllers that wish to have its job performed using
|
||||
# the +cache_sweeper+ class method:
|
||||
#
|
||||
# class ListsController < ApplicationController
|
||||
# caches_action :index, :show, :public, :feed
|
||||
# cache_sweeper :list_sweeper, only: [ :edit, :destroy, :share ]
|
||||
# end
|
||||
#
|
||||
# In the example above, four actions are cached and three actions are responsible for expiring those caches.
|
||||
#
|
||||
# You can also name an explicit class in the declaration of a sweeper, which is needed
|
||||
# if the sweeper is in a module:
|
||||
#
|
||||
# class ListsController < ApplicationController
|
||||
# caches_action :index, :show, :public, :feed
|
||||
# cache_sweeper OpenBar::Sweeper, only: [ :edit, :destroy, :share ]
|
||||
# end
|
||||
module Sweeping
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods # :nodoc:
|
||||
def cache_sweeper(*sweepers)
|
||||
configuration = sweepers.extract_options!
|
||||
|
||||
sweepers.each do |sweeper|
|
||||
ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base)
|
||||
sweeper_instance = (sweeper.is_a?(Symbol) ? Object.const_get(sweeper.to_s.classify) : sweeper).instance
|
||||
|
||||
if sweeper_instance.is_a?(Sweeper)
|
||||
around_filter(sweeper_instance, :only => configuration[:only])
|
||||
else
|
||||
after_filter(sweeper_instance, :only => configuration[:only])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
|
||||
class Sweeper < ActiveRecord::Observer # :nodoc:
|
||||
attr_accessor :controller
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
@controller = nil
|
||||
end
|
||||
|
||||
def before(controller)
|
||||
self.controller = controller
|
||||
callback(:before) if controller.perform_caching
|
||||
true # before method from sweeper should always return true
|
||||
end
|
||||
|
||||
def after(controller)
|
||||
self.controller = controller
|
||||
callback(:after) if controller.perform_caching
|
||||
end
|
||||
|
||||
def around(controller)
|
||||
before(controller)
|
||||
yield
|
||||
after(controller)
|
||||
ensure
|
||||
clean_up
|
||||
end
|
||||
|
||||
protected
|
||||
# gets the action cache path for the given options.
|
||||
def action_path_for(options)
|
||||
Actions::ActionCachePath.new(controller, options).path
|
||||
end
|
||||
|
||||
# Retrieve instance variables set in the controller.
|
||||
def assigns(key)
|
||||
controller.instance_variable_get("@#{key}")
|
||||
end
|
||||
|
||||
private
|
||||
def clean_up
|
||||
# Clean up, so that the controller can be collected after this request
|
||||
self.controller = nil
|
||||
end
|
||||
|
||||
def callback(timing)
|
||||
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
|
||||
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
|
||||
|
||||
__send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
|
||||
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
|
||||
end
|
||||
|
||||
def method_missing(method, *arguments, &block)
|
||||
return super unless @controller
|
||||
@controller.__send__(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -25,7 +25,6 @@ require 'active_support/dependencies'
|
|||
require 'active_model'
|
||||
require 'active_record'
|
||||
require 'action_controller/caching'
|
||||
require 'action_controller/caching/sweeping'
|
||||
|
||||
require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late
|
||||
|
||||
|
|
|
@ -499,18 +499,6 @@ class FilterTest < ActionController::TestCase
|
|||
|
||||
end
|
||||
|
||||
class ::AppSweeper < ActionController::Caching::Sweeper; end
|
||||
class SweeperTestController < ActionController::Base
|
||||
cache_sweeper :app_sweeper
|
||||
def show
|
||||
render :text => 'hello world'
|
||||
end
|
||||
|
||||
def error
|
||||
raise StandardError.new
|
||||
end
|
||||
end
|
||||
|
||||
class ImplicitActionsController < ActionController::Base
|
||||
before_filter :find_only, :only => :edit
|
||||
before_filter :find_except, :except => :edit
|
||||
|
@ -526,35 +514,6 @@ class FilterTest < ActionController::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_sweeper_should_not_ignore_no_method_error
|
||||
sweeper = ActionController::Caching::Sweeper.send(:new)
|
||||
assert_raise NoMethodError do
|
||||
sweeper.send_not_defined
|
||||
end
|
||||
end
|
||||
|
||||
def test_sweeper_should_not_block_rendering
|
||||
response = test_process(SweeperTestController)
|
||||
assert_equal 'hello world', response.body
|
||||
end
|
||||
|
||||
def test_sweeper_should_clean_up_if_exception_is_raised
|
||||
assert_raise StandardError do
|
||||
test_process(SweeperTestController, 'error')
|
||||
end
|
||||
assert_nil AppSweeper.instance.controller
|
||||
end
|
||||
|
||||
def test_before_method_of_sweeper_should_always_return_true
|
||||
sweeper = ActionController::Caching::Sweeper.send(:new)
|
||||
assert sweeper.before(TestController.new)
|
||||
end
|
||||
|
||||
def test_after_method_of_sweeper_should_always_return_nil
|
||||
sweeper = ActionController::Caching::Sweeper.send(:new)
|
||||
assert_nil sweeper.after(TestController.new)
|
||||
end
|
||||
|
||||
def test_non_yielding_around_filters_not_returning_false_do_not_raise
|
||||
controller = NonYieldingAroundFilterController.new
|
||||
controller.instance_variable_set "@filter_return_value", true
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
require 'abstract_unit'
|
||||
|
||||
|
||||
class SweeperTest < ActionController::TestCase
|
||||
|
||||
class ::AppSweeper < ActionController::Caching::Sweeper; end
|
||||
|
||||
def test_sweeper_should_not_ignore_unknown_method_calls
|
||||
sweeper = ActionController::Caching::Sweeper.send(:new)
|
||||
assert_raise NameError do
|
||||
sweeper.instance_eval do
|
||||
some_method_that_doesnt_exist
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -40,8 +40,6 @@ module ActiveModel
|
|||
autoload :DeprecatedMassAssignmentSecurity
|
||||
autoload :Name, 'active_model/naming'
|
||||
autoload :Naming
|
||||
autoload :Observer, 'active_model/observing'
|
||||
autoload :Observing
|
||||
autoload :SecurePassword
|
||||
autoload :Serialization
|
||||
autoload :TestCase
|
||||
|
|
|
@ -1,152 +0,0 @@
|
|||
require 'set'
|
||||
|
||||
module ActiveModel
|
||||
# Stores the enabled/disabled state of individual observers for
|
||||
# a particular model class.
|
||||
class ObserverArray < Array
|
||||
attr_reader :model_class
|
||||
def initialize(model_class, *args) #:nodoc:
|
||||
@model_class = model_class
|
||||
super(*args)
|
||||
end
|
||||
|
||||
# Returns +true+ if the given observer is disabled for the model class,
|
||||
# +false+ otherwise.
|
||||
def disabled_for?(observer) #:nodoc:
|
||||
disabled_observers.include?(observer.class)
|
||||
end
|
||||
|
||||
# Disables one or more observers. This supports multiple forms:
|
||||
#
|
||||
# ORM.observers.disable :all
|
||||
# # => disables all observers for all models subclassed from
|
||||
# # an ORM base class that includes ActiveModel::Observing
|
||||
# # e.g. ActiveRecord::Base
|
||||
#
|
||||
# ORM.observers.disable :user_observer
|
||||
# # => disables the UserObserver
|
||||
#
|
||||
# User.observers.disable AuditTrail
|
||||
# # => disables the AuditTrail observer for User notifications.
|
||||
# # Other models will still notify the AuditTrail observer.
|
||||
#
|
||||
# ORM.observers.disable :observer_1, :observer_2
|
||||
# # => disables Observer1 and Observer2 for all models.
|
||||
#
|
||||
# User.observers.disable :all do
|
||||
# # all user observers are disabled for
|
||||
# # just the duration of the block
|
||||
# end
|
||||
def disable(*observers, &block)
|
||||
set_enablement(false, observers, &block)
|
||||
end
|
||||
|
||||
# Enables one or more observers. This supports multiple forms:
|
||||
#
|
||||
# ORM.observers.enable :all
|
||||
# # => enables all observers for all models subclassed from
|
||||
# # an ORM base class that includes ActiveModel::Observing
|
||||
# # e.g. ActiveRecord::Base
|
||||
#
|
||||
# ORM.observers.enable :user_observer
|
||||
# # => enables the UserObserver
|
||||
#
|
||||
# User.observers.enable AuditTrail
|
||||
# # => enables the AuditTrail observer for User notifications.
|
||||
# # Other models will not be affected (i.e. they will not
|
||||
# # trigger notifications to AuditTrail if previously disabled)
|
||||
#
|
||||
# ORM.observers.enable :observer_1, :observer_2
|
||||
# # => enables Observer1 and Observer2 for all models.
|
||||
#
|
||||
# User.observers.enable :all do
|
||||
# # all user observers are enabled for
|
||||
# # just the duration of the block
|
||||
# end
|
||||
#
|
||||
# Note: all observers are enabled by default. This method is only
|
||||
# useful when you have previously disabled one or more observers.
|
||||
def enable(*observers, &block)
|
||||
set_enablement(true, observers, &block)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def disabled_observers #:nodoc:
|
||||
@disabled_observers ||= Set.new
|
||||
end
|
||||
|
||||
def observer_class_for(observer) #:nodoc:
|
||||
return observer if observer.is_a?(Class)
|
||||
|
||||
if observer.respond_to?(:to_sym) # string/symbol
|
||||
observer.to_s.camelize.constantize
|
||||
else
|
||||
raise ArgumentError, "#{observer} was not a class or a " +
|
||||
"lowercase, underscored class name as expected."
|
||||
end
|
||||
end
|
||||
|
||||
def start_transaction #:nodoc:
|
||||
disabled_observer_stack.push(disabled_observers.dup)
|
||||
each_subclass_array do |array|
|
||||
array.start_transaction
|
||||
end
|
||||
end
|
||||
|
||||
def disabled_observer_stack #:nodoc:
|
||||
@disabled_observer_stack ||= []
|
||||
end
|
||||
|
||||
def end_transaction #:nodoc:
|
||||
@disabled_observers = disabled_observer_stack.pop
|
||||
each_subclass_array do |array|
|
||||
array.end_transaction
|
||||
end
|
||||
end
|
||||
|
||||
def transaction #:nodoc:
|
||||
start_transaction
|
||||
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
end_transaction
|
||||
end
|
||||
end
|
||||
|
||||
def each_subclass_array #:nodoc:
|
||||
model_class.descendants.each do |subclass|
|
||||
yield subclass.observers
|
||||
end
|
||||
end
|
||||
|
||||
def set_enablement(enabled, observers) #:nodoc:
|
||||
if block_given?
|
||||
transaction do
|
||||
set_enablement(enabled, observers)
|
||||
yield
|
||||
end
|
||||
else
|
||||
observers = ActiveModel::Observer.descendants if observers == [:all]
|
||||
observers.each do |obs|
|
||||
klass = observer_class_for(obs)
|
||||
|
||||
unless klass < ActiveModel::Observer
|
||||
raise ArgumentError.new("#{obs} does not refer to a valid observer")
|
||||
end
|
||||
|
||||
if enabled
|
||||
disabled_observers.delete(klass)
|
||||
else
|
||||
disabled_observers << klass
|
||||
end
|
||||
end
|
||||
|
||||
each_subclass_array do |array|
|
||||
array.set_enablement(enabled, observers)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,373 +0,0 @@
|
|||
require 'singleton'
|
||||
require 'active_model/observer_array'
|
||||
require 'active_support/core_ext/module/aliasing'
|
||||
require 'active_support/core_ext/module/remove_method'
|
||||
require 'active_support/core_ext/string/inflections'
|
||||
require 'active_support/core_ext/enumerable'
|
||||
require 'active_support/core_ext/object/try'
|
||||
|
||||
module ActiveModel
|
||||
# == Active \Model Observers Activation
|
||||
module Observing
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
extend ActiveSupport::DescendantsTracker
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Activates the observers assigned.
|
||||
#
|
||||
# class ORM
|
||||
# include ActiveModel::Observing
|
||||
# end
|
||||
#
|
||||
# # Calls PersonObserver.instance
|
||||
# ORM.observers = :person_observer
|
||||
#
|
||||
# # Calls Cacher.instance and GarbageCollector.instance
|
||||
# ORM.observers = :cacher, :garbage_collector
|
||||
#
|
||||
# # Same as above, just using explicit class references
|
||||
# ORM.observers = Cacher, GarbageCollector
|
||||
#
|
||||
# Note: Setting this does not instantiate the observers yet.
|
||||
# <tt>instantiate_observers</tt> is called during startup, and before
|
||||
# each development request.
|
||||
def observers=(*values)
|
||||
observers.replace(values.flatten)
|
||||
end
|
||||
|
||||
# Gets an array of observers observing this model. The array also provides
|
||||
# +enable+ and +disable+ methods that allow you to selectively enable and
|
||||
# disable observers (see ActiveModel::ObserverArray.enable and
|
||||
# ActiveModel::ObserverArray.disable for more on this).
|
||||
#
|
||||
# class ORM
|
||||
# include ActiveModel::Observing
|
||||
# end
|
||||
#
|
||||
# ORM.observers = :cacher, :garbage_collector
|
||||
# ORM.observers # => [:cacher, :garbage_collector]
|
||||
# ORM.observers.class # => ActiveModel::ObserverArray
|
||||
def observers
|
||||
@observers ||= ObserverArray.new(self)
|
||||
end
|
||||
|
||||
# Returns the current observer instances.
|
||||
#
|
||||
# class Foo
|
||||
# include ActiveModel::Observing
|
||||
#
|
||||
# attr_accessor :status
|
||||
# end
|
||||
#
|
||||
# class FooObserver < ActiveModel::Observer
|
||||
# def on_spec(record, *args)
|
||||
# record.status = true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Foo.observers = FooObserver
|
||||
# Foo.instantiate_observers
|
||||
#
|
||||
# Foo.observer_instances # => [#<FooObserver:0x007fc212c40820>]
|
||||
def observer_instances
|
||||
@observer_instances ||= []
|
||||
end
|
||||
|
||||
# Instantiate the global observers.
|
||||
#
|
||||
# class Foo
|
||||
# include ActiveModel::Observing
|
||||
#
|
||||
# attr_accessor :status
|
||||
# end
|
||||
#
|
||||
# class FooObserver < ActiveModel::Observer
|
||||
# def on_spec(record, *args)
|
||||
# record.status = true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Foo.observers = FooObserver
|
||||
#
|
||||
# foo = Foo.new
|
||||
# foo.status = false
|
||||
# foo.notify_observers(:on_spec)
|
||||
# foo.status # => false
|
||||
#
|
||||
# Foo.instantiate_observers # => [FooObserver]
|
||||
#
|
||||
# foo = Foo.new
|
||||
# foo.status = false
|
||||
# foo.notify_observers(:on_spec)
|
||||
# foo.status # => true
|
||||
def instantiate_observers
|
||||
observers.each { |o| instantiate_observer(o) }
|
||||
end
|
||||
|
||||
# Add a new observer to the pool. The new observer needs to respond to
|
||||
# <tt>update</tt>, otherwise it raises an +ArgumentError+ exception.
|
||||
#
|
||||
# class Foo
|
||||
# include ActiveModel::Observing
|
||||
# end
|
||||
#
|
||||
# class FooObserver < ActiveModel::Observer
|
||||
# end
|
||||
#
|
||||
# Foo.add_observer(FooObserver.instance)
|
||||
#
|
||||
# Foo.observers_instance
|
||||
# # => [#<FooObserver:0x007fccf55d9390>]
|
||||
def add_observer(observer)
|
||||
unless observer.respond_to? :update
|
||||
raise ArgumentError, "observer needs to respond to 'update'"
|
||||
end
|
||||
observer_instances << observer
|
||||
end
|
||||
|
||||
# Fires notifications to model's observers.
|
||||
#
|
||||
# def save
|
||||
# notify_observers(:before_save)
|
||||
# ...
|
||||
# notify_observers(:after_save)
|
||||
# end
|
||||
#
|
||||
# Custom notifications can be sent in a similar fashion:
|
||||
#
|
||||
# notify_observers(:custom_notification, :foo)
|
||||
#
|
||||
# This will call <tt>custom_notification</tt>, passing as arguments
|
||||
# the current object and <tt>:foo</tt>.
|
||||
def notify_observers(*args)
|
||||
observer_instances.each { |observer| observer.update(*args) }
|
||||
end
|
||||
|
||||
# Returns the total number of instantiated observers.
|
||||
#
|
||||
# class Foo
|
||||
# include ActiveModel::Observing
|
||||
#
|
||||
# attr_accessor :status
|
||||
# end
|
||||
#
|
||||
# class FooObserver < ActiveModel::Observer
|
||||
# def on_spec(record, *args)
|
||||
# record.status = true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Foo.observers = FooObserver
|
||||
# Foo.observers_count # => 0
|
||||
# Foo.instantiate_observers
|
||||
# Foo.observers_count # => 1
|
||||
def observers_count
|
||||
observer_instances.size
|
||||
end
|
||||
|
||||
# <tt>count_observers</tt> is deprecated. Use #observers_count.
|
||||
def count_observers
|
||||
msg = "count_observers is deprecated in favor of observers_count"
|
||||
ActiveSupport::Deprecation.warn msg
|
||||
observers_count
|
||||
end
|
||||
|
||||
protected
|
||||
def instantiate_observer(observer) #:nodoc:
|
||||
# string/symbol
|
||||
if observer.respond_to?(:to_sym)
|
||||
observer = observer.to_s.camelize.constantize
|
||||
end
|
||||
if observer.respond_to?(:instance)
|
||||
observer.instance
|
||||
else
|
||||
raise ArgumentError,
|
||||
"#{observer} must be a lowercase, underscored class name (or " +
|
||||
"the class itself) responding to the method :instance. " +
|
||||
"Example: Person.observers = :big_brother # calls " +
|
||||
"BigBrother.instance"
|
||||
end
|
||||
end
|
||||
|
||||
# Notify observers when the observed class is subclassed.
|
||||
def inherited(subclass) #:nodoc:
|
||||
super
|
||||
notify_observers :observed_class_inherited, subclass
|
||||
end
|
||||
end
|
||||
|
||||
# Notify a change to the list of observers.
|
||||
#
|
||||
# class Foo
|
||||
# include ActiveModel::Observing
|
||||
#
|
||||
# attr_accessor :status
|
||||
# end
|
||||
#
|
||||
# class FooObserver < ActiveModel::Observer
|
||||
# def on_spec(record, *args)
|
||||
# record.status = true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Foo.observers = FooObserver
|
||||
# Foo.instantiate_observers # => [FooObserver]
|
||||
#
|
||||
# foo = Foo.new
|
||||
# foo.status = false
|
||||
# foo.notify_observers(:on_spec)
|
||||
# foo.status # => true
|
||||
#
|
||||
# See ActiveModel::Observing::ClassMethods.notify_observers for more
|
||||
# information.
|
||||
def notify_observers(method, *extra_args)
|
||||
self.class.notify_observers(method, self, *extra_args)
|
||||
end
|
||||
end
|
||||
|
||||
# == Active \Model Observers
|
||||
#
|
||||
# Observer classes respond to life cycle callbacks to implement trigger-like
|
||||
# behavior outside the original class. This is a great way to reduce the
|
||||
# clutter that normally comes when the model class is burdened with
|
||||
# functionality that doesn't pertain to the core responsibility of the
|
||||
# class.
|
||||
#
|
||||
# class CommentObserver < ActiveModel::Observer
|
||||
# def after_save(comment)
|
||||
# Notifications.comment('admin@do.com', 'New comment was posted', comment).deliver
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This Observer sends an email when a <tt>Comment#save</tt> is finished.
|
||||
#
|
||||
# class ContactObserver < ActiveModel::Observer
|
||||
# def after_create(contact)
|
||||
# contact.logger.info('New contact added!')
|
||||
# end
|
||||
#
|
||||
# def after_destroy(contact)
|
||||
# contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This Observer uses logger to log when specific callbacks are triggered.
|
||||
#
|
||||
# == \Observing a class that can't be inferred
|
||||
#
|
||||
# Observers will by default be mapped to the class with which they share a
|
||||
# name. So <tt>CommentObserver</tt> will be tied to observing <tt>Comment</tt>,
|
||||
# <tt>ProductManagerObserver</tt> to <tt>ProductManager</tt>, and so on. If
|
||||
# you want to name your observer differently than the class you're interested
|
||||
# in observing, you can use the <tt>Observer.observe</tt> class method which
|
||||
# takes either the concrete class (<tt>Product</tt>) or a symbol for that
|
||||
# class (<tt>:product</tt>):
|
||||
#
|
||||
# class AuditObserver < ActiveModel::Observer
|
||||
# observe :account
|
||||
#
|
||||
# def after_update(account)
|
||||
# AuditTrail.new(account, 'UPDATED')
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# If the audit observer needs to watch more than one kind of object, this can
|
||||
# be specified with multiple arguments:
|
||||
#
|
||||
# class AuditObserver < ActiveModel::Observer
|
||||
# observe :account, :balance
|
||||
#
|
||||
# def after_update(record)
|
||||
# AuditTrail.new(record, 'UPDATED')
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The <tt>AuditObserver</tt> will now act on both updates to <tt>Account</tt>
|
||||
# and <tt>Balance</tt> by treating them both as records.
|
||||
#
|
||||
# If you're using an Observer in a Rails application with Active Record, be
|
||||
# sure to read about the necessary configuration in the documentation for
|
||||
# ActiveRecord::Observer.
|
||||
class Observer
|
||||
include Singleton
|
||||
extend ActiveSupport::DescendantsTracker
|
||||
|
||||
class << self
|
||||
# Attaches the observer to the supplied model classes.
|
||||
#
|
||||
# class AuditObserver < ActiveModel::Observer
|
||||
# observe :account, :balance
|
||||
# end
|
||||
#
|
||||
# AuditObserver.observed_classes # => [Account, Balance]
|
||||
def observe(*models)
|
||||
models.flatten!
|
||||
models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model }
|
||||
singleton_class.redefine_method(:observed_classes) { models }
|
||||
end
|
||||
|
||||
# Returns an array of Classes to observe.
|
||||
#
|
||||
# AccountObserver.observed_classes # => [Account]
|
||||
#
|
||||
# You can override this instead of using the +observe+ helper.
|
||||
#
|
||||
# class AuditObserver < ActiveModel::Observer
|
||||
# def self.observed_classes
|
||||
# [Account, Balance]
|
||||
# end
|
||||
# end
|
||||
def observed_classes
|
||||
Array(observed_class)
|
||||
end
|
||||
|
||||
# Returns the class observed by default. It's inferred from the observer's
|
||||
# class name.
|
||||
#
|
||||
# PersonObserver.observed_class # => Person
|
||||
# AccountObserver.observed_class # => Account
|
||||
def observed_class
|
||||
name[/(.*)Observer/, 1].try :constantize
|
||||
end
|
||||
end
|
||||
|
||||
# Start observing the declared classes and their subclasses.
|
||||
# Called automatically by the instance method.
|
||||
def initialize #:nodoc:
|
||||
observed_classes.each { |klass| add_observer!(klass) }
|
||||
end
|
||||
|
||||
def observed_classes #:nodoc:
|
||||
self.class.observed_classes
|
||||
end
|
||||
|
||||
# Send observed_method(object) if the method exists and
|
||||
# the observer is enabled for the given object's class.
|
||||
def update(observed_method, object, *extra_args, &block) #:nodoc:
|
||||
return if !respond_to?(observed_method) || disabled_for?(object)
|
||||
send(observed_method, object, *extra_args, &block)
|
||||
end
|
||||
|
||||
# Special method sent by the observed class when it is inherited.
|
||||
# Passes the new subclass.
|
||||
def observed_class_inherited(subclass) #:nodoc:
|
||||
self.class.observe(observed_classes + [subclass])
|
||||
add_observer!(subclass)
|
||||
end
|
||||
|
||||
protected
|
||||
def add_observer!(klass) #:nodoc:
|
||||
klass.add_observer(self)
|
||||
end
|
||||
|
||||
# Returns true if notifications are disabled for this object.
|
||||
def disabled_for?(object) #:nodoc:
|
||||
klass = object.class
|
||||
return false unless klass.respond_to?(:observers)
|
||||
klass.observers.disabled_for?(self)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,220 +0,0 @@
|
|||
require 'cases/helper'
|
||||
require 'models/observers'
|
||||
|
||||
class ObserverArrayTest < ActiveModel::TestCase
|
||||
def teardown
|
||||
ORM.observers.enable :all
|
||||
Budget.observers.enable :all
|
||||
Widget.observers.enable :all
|
||||
end
|
||||
|
||||
def assert_observer_notified(model_class, observer_class)
|
||||
observer_class.instance.before_save_invocations.clear
|
||||
model_instance = model_class.new
|
||||
model_instance.save
|
||||
assert_equal [model_instance], observer_class.instance.before_save_invocations
|
||||
end
|
||||
|
||||
def assert_observer_not_notified(model_class, observer_class)
|
||||
observer_class.instance.before_save_invocations.clear
|
||||
model_instance = model_class.new
|
||||
model_instance.save
|
||||
assert_equal [], observer_class.instance.before_save_invocations
|
||||
end
|
||||
|
||||
test "all observers are enabled by default" do
|
||||
assert_observer_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can disable individual observers using a class constant" do
|
||||
ORM.observers.disable WidgetObserver
|
||||
|
||||
assert_observer_not_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can enable individual observers using a class constant" do
|
||||
ORM.observers.disable :all
|
||||
ORM.observers.enable AuditTrail
|
||||
|
||||
assert_observer_not_notified Widget, WidgetObserver
|
||||
assert_observer_not_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can disable individual observers using a symbol" do
|
||||
ORM.observers.disable :budget_observer
|
||||
|
||||
assert_observer_notified Widget, WidgetObserver
|
||||
assert_observer_not_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can enable individual observers using a symbol" do
|
||||
ORM.observers.disable :all
|
||||
ORM.observers.enable :audit_trail
|
||||
|
||||
assert_observer_not_notified Widget, WidgetObserver
|
||||
assert_observer_not_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can disable multiple observers at a time" do
|
||||
ORM.observers.disable :widget_observer, :budget_observer
|
||||
|
||||
assert_observer_not_notified Widget, WidgetObserver
|
||||
assert_observer_not_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can enable multiple observers at a time" do
|
||||
ORM.observers.disable :all
|
||||
ORM.observers.enable :widget_observer, :budget_observer
|
||||
|
||||
assert_observer_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_not_notified Widget, AuditTrail
|
||||
assert_observer_not_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can disable all observers using :all" do
|
||||
ORM.observers.disable :all
|
||||
|
||||
assert_observer_not_notified Widget, WidgetObserver
|
||||
assert_observer_not_notified Budget, BudgetObserver
|
||||
assert_observer_not_notified Widget, AuditTrail
|
||||
assert_observer_not_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can enable all observers using :all" do
|
||||
ORM.observers.disable :all
|
||||
ORM.observers.enable :all
|
||||
|
||||
assert_observer_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can disable observers on individual models without affecting those observers on other models" do
|
||||
Widget.observers.disable :all
|
||||
|
||||
assert_observer_not_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_not_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can enable observers on individual models without affecting those observers on other models" do
|
||||
ORM.observers.disable :all
|
||||
Budget.observers.enable AuditTrail
|
||||
|
||||
assert_observer_not_notified Widget, WidgetObserver
|
||||
assert_observer_not_notified Budget, BudgetObserver
|
||||
assert_observer_not_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can disable observers for the duration of a block" do
|
||||
yielded = false
|
||||
ORM.observers.disable :budget_observer do
|
||||
yielded = true
|
||||
assert_observer_notified Widget, WidgetObserver
|
||||
assert_observer_not_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
assert yielded
|
||||
assert_observer_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can enable observers for the duration of a block" do
|
||||
yielded = false
|
||||
Widget.observers.disable :all
|
||||
|
||||
Widget.observers.enable :all do
|
||||
yielded = true
|
||||
assert_observer_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
assert yielded
|
||||
assert_observer_not_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_not_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "raises an appropriate error when a developer accidentally enables or disables the wrong class (i.e. Widget instead of WidgetObserver)" do
|
||||
assert_raise ArgumentError do
|
||||
ORM.observers.enable :widget
|
||||
end
|
||||
|
||||
assert_raise ArgumentError do
|
||||
ORM.observers.enable Widget
|
||||
end
|
||||
|
||||
assert_raise ArgumentError do
|
||||
ORM.observers.disable :widget
|
||||
end
|
||||
|
||||
assert_raise ArgumentError do
|
||||
ORM.observers.disable Widget
|
||||
end
|
||||
end
|
||||
|
||||
test "allows #enable at the superclass level to override #disable at the subclass level when called last" do
|
||||
Widget.observers.disable :all
|
||||
ORM.observers.enable :all
|
||||
|
||||
assert_observer_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "allows #disable at the superclass level to override #enable at the subclass level when called last" do
|
||||
Budget.observers.enable :audit_trail
|
||||
ORM.observers.disable :audit_trail
|
||||
|
||||
assert_observer_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_not_notified Widget, AuditTrail
|
||||
assert_observer_not_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
test "can use the block form at different levels of the hierarchy" do
|
||||
yielded = false
|
||||
Widget.observers.disable :all
|
||||
|
||||
ORM.observers.enable :all do
|
||||
yielded = true
|
||||
assert_observer_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
|
||||
assert yielded
|
||||
assert_observer_not_notified Widget, WidgetObserver
|
||||
assert_observer_notified Budget, BudgetObserver
|
||||
assert_observer_not_notified Widget, AuditTrail
|
||||
assert_observer_notified Budget, AuditTrail
|
||||
end
|
||||
end
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
require 'cases/helper'
|
||||
|
||||
class ObservedModel
|
||||
include ActiveModel::Observing
|
||||
|
||||
class Observer
|
||||
end
|
||||
end
|
||||
|
||||
class FooObserver < ActiveModel::Observer
|
||||
class << self
|
||||
public :new
|
||||
end
|
||||
|
||||
attr_accessor :stub
|
||||
|
||||
def on_spec(record, *args)
|
||||
stub.event_with(record, *args) if stub
|
||||
end
|
||||
|
||||
def around_save(record)
|
||||
yield :in_around_save
|
||||
end
|
||||
end
|
||||
|
||||
class Foo
|
||||
include ActiveModel::Observing
|
||||
end
|
||||
|
||||
class ObservingTest < ActiveModel::TestCase
|
||||
def setup
|
||||
ObservedModel.observers.clear
|
||||
end
|
||||
|
||||
test "initializes model with no cached observers" do
|
||||
assert ObservedModel.observers.empty?, "Not empty: #{ObservedModel.observers.inspect}"
|
||||
end
|
||||
|
||||
test "stores cached observers in an array" do
|
||||
ObservedModel.observers << :foo
|
||||
assert ObservedModel.observers.include?(:foo), ":foo not in #{ObservedModel.observers.inspect}"
|
||||
end
|
||||
|
||||
test "flattens array of assigned cached observers" do
|
||||
ObservedModel.observers = [[:foo], :bar]
|
||||
assert ObservedModel.observers.include?(:foo), ":foo not in #{ObservedModel.observers.inspect}"
|
||||
assert ObservedModel.observers.include?(:bar), ":bar not in #{ObservedModel.observers.inspect}"
|
||||
end
|
||||
|
||||
test "uses an ObserverArray so observers can be disabled" do
|
||||
ObservedModel.observers = [:foo, :bar]
|
||||
assert ObservedModel.observers.is_a?(ActiveModel::ObserverArray)
|
||||
end
|
||||
|
||||
test "instantiates observer names passed as strings" do
|
||||
ObservedModel.observers << 'foo_observer'
|
||||
FooObserver.expects(:instance)
|
||||
ObservedModel.instantiate_observers
|
||||
end
|
||||
|
||||
test "instantiates observer names passed as symbols" do
|
||||
ObservedModel.observers << :foo_observer
|
||||
FooObserver.expects(:instance)
|
||||
ObservedModel.instantiate_observers
|
||||
end
|
||||
|
||||
test "instantiates observer classes" do
|
||||
ObservedModel.observers << ObservedModel::Observer
|
||||
ObservedModel::Observer.expects(:instance)
|
||||
ObservedModel.instantiate_observers
|
||||
end
|
||||
|
||||
test "raises an appropriate error when a developer accidentally adds the wrong class (i.e. Widget instead of WidgetObserver)" do
|
||||
assert_raise ArgumentError do
|
||||
ObservedModel.observers = ['string']
|
||||
ObservedModel.instantiate_observers
|
||||
end
|
||||
assert_raise ArgumentError do
|
||||
ObservedModel.observers = [:string]
|
||||
ObservedModel.instantiate_observers
|
||||
end
|
||||
assert_raise ArgumentError do
|
||||
ObservedModel.observers = [String]
|
||||
ObservedModel.instantiate_observers
|
||||
end
|
||||
end
|
||||
|
||||
test "passes observers to subclasses" do
|
||||
FooObserver.instance
|
||||
bar = Class.new(Foo)
|
||||
assert_equal Foo.observers_count, bar.observers_count
|
||||
end
|
||||
end
|
||||
|
||||
class ObserverTest < ActiveModel::TestCase
|
||||
def setup
|
||||
ObservedModel.observers = :foo_observer
|
||||
FooObserver.singleton_class.instance_eval do
|
||||
alias_method :original_observed_classes, :observed_classes
|
||||
end
|
||||
end
|
||||
|
||||
def teardown
|
||||
FooObserver.singleton_class.instance_eval do
|
||||
undef_method :observed_classes
|
||||
alias_method :observed_classes, :original_observed_classes
|
||||
end
|
||||
end
|
||||
|
||||
test "guesses implicit observable model name" do
|
||||
assert_equal Foo, FooObserver.observed_class
|
||||
end
|
||||
|
||||
test "tracks implicit observable models" do
|
||||
instance = FooObserver.new
|
||||
assert_equal [Foo], instance.observed_classes
|
||||
end
|
||||
|
||||
test "tracks explicit observed model class" do
|
||||
FooObserver.observe ObservedModel
|
||||
instance = FooObserver.new
|
||||
assert_equal [ObservedModel], instance.observed_classes
|
||||
end
|
||||
|
||||
test "tracks explicit observed model as string" do
|
||||
FooObserver.observe 'observed_model'
|
||||
instance = FooObserver.new
|
||||
assert_equal [ObservedModel], instance.observed_classes
|
||||
end
|
||||
|
||||
test "tracks explicit observed model as symbol" do
|
||||
FooObserver.observe :observed_model
|
||||
instance = FooObserver.new
|
||||
assert_equal [ObservedModel], instance.observed_classes
|
||||
end
|
||||
|
||||
test "calls existing observer event" do
|
||||
foo = Foo.new
|
||||
FooObserver.instance.stub = stub
|
||||
FooObserver.instance.stub.expects(:event_with).with(foo)
|
||||
Foo.notify_observers(:on_spec, foo)
|
||||
end
|
||||
|
||||
test "calls existing observer event from the instance" do
|
||||
foo = Foo.new
|
||||
FooObserver.instance.stub = stub
|
||||
FooObserver.instance.stub.expects(:event_with).with(foo)
|
||||
foo.notify_observers(:on_spec)
|
||||
end
|
||||
|
||||
test "passes extra arguments" do
|
||||
foo = Foo.new
|
||||
FooObserver.instance.stub = stub
|
||||
FooObserver.instance.stub.expects(:event_with).with(foo, :bar)
|
||||
Foo.send(:notify_observers, :on_spec, foo, :bar)
|
||||
end
|
||||
|
||||
test "skips nonexistent observer event" do
|
||||
foo = Foo.new
|
||||
Foo.notify_observers(:whatever, foo)
|
||||
end
|
||||
|
||||
test "update passes a block on to the observer" do
|
||||
yielded_value = nil
|
||||
FooObserver.instance.update(:around_save, Foo.new) do |val|
|
||||
yielded_value = val
|
||||
end
|
||||
assert_equal :in_around_save, yielded_value
|
||||
end
|
||||
|
||||
test "observe redefines observed_classes class method" do
|
||||
class BarObserver < ActiveModel::Observer
|
||||
observe :foo
|
||||
end
|
||||
|
||||
assert_equal [Foo], BarObserver.observed_classes
|
||||
|
||||
BarObserver.observe(ObservedModel)
|
||||
assert_equal [ObservedModel], BarObserver.observed_classes
|
||||
end
|
||||
end
|
|
@ -1,27 +0,0 @@
|
|||
class ORM
|
||||
include ActiveModel::Observing
|
||||
|
||||
def save
|
||||
notify_observers :before_save
|
||||
end
|
||||
|
||||
class Observer < ActiveModel::Observer
|
||||
def before_save_invocations
|
||||
@before_save_invocations ||= []
|
||||
end
|
||||
|
||||
def before_save(record)
|
||||
before_save_invocations << record
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Widget < ORM; end
|
||||
class Budget < ORM; end
|
||||
class WidgetObserver < ORM::Observer; end
|
||||
class BudgetObserver < ORM::Observer; end
|
||||
class AuditTrail < ORM::Observer
|
||||
observe :widget, :budget
|
||||
end
|
||||
|
||||
ORM.instantiate_observers
|
|
@ -45,7 +45,6 @@ module ActiveRecord
|
|||
autoload :Migrator, 'active_record/migration'
|
||||
autoload :ModelSchema
|
||||
autoload :NestedAttributes
|
||||
autoload :Observer
|
||||
autoload :Persistence
|
||||
autoload :QueryCache
|
||||
autoload :Querying
|
||||
|
|
|
@ -320,7 +320,6 @@ module ActiveRecord #:nodoc:
|
|||
# So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
|
||||
# instances in the current object space.
|
||||
class Base
|
||||
extend ActiveModel::Observing::ClassMethods
|
||||
extend ActiveModel::Naming
|
||||
|
||||
extend ActiveSupport::Benchmarkable
|
||||
|
@ -348,7 +347,6 @@ module ActiveRecord #:nodoc:
|
|||
include Locking::Pessimistic
|
||||
include AttributeMethods
|
||||
include Callbacks
|
||||
include ActiveModel::Observing
|
||||
include Timestamp
|
||||
include Associations
|
||||
include ActiveModel::SecurePassword
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
|
||||
module ActiveRecord
|
||||
# = Active Record Observer
|
||||
#
|
||||
# Observer classes respond to life cycle callbacks to implement trigger-like
|
||||
# behavior outside the original class. This is a great way to reduce the
|
||||
# clutter that normally comes when the model class is burdened with
|
||||
# functionality that doesn't pertain to the core responsibility of the
|
||||
# class. Example:
|
||||
#
|
||||
# class CommentObserver < ActiveRecord::Observer
|
||||
# def after_save(comment)
|
||||
# Notifications.comment("admin@do.com", "New comment was posted", comment).deliver
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This Observer sends an email when a Comment#save is finished.
|
||||
#
|
||||
# class ContactObserver < ActiveRecord::Observer
|
||||
# def after_create(contact)
|
||||
# contact.logger.info('New contact added!')
|
||||
# end
|
||||
#
|
||||
# def after_destroy(contact)
|
||||
# contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This Observer uses logger to log when specific callbacks are triggered.
|
||||
#
|
||||
# == Observing a class that can't be inferred
|
||||
#
|
||||
# Observers will by default be mapped to the class with which they share a name. So CommentObserver will
|
||||
# be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
|
||||
# differently than the class you're interested in observing, you can use the Observer.observe class method which takes
|
||||
# either the concrete class (Product) or a symbol for that class (:product):
|
||||
#
|
||||
# class AuditObserver < ActiveRecord::Observer
|
||||
# observe :account
|
||||
#
|
||||
# def after_update(account)
|
||||
# AuditTrail.new(account, "UPDATED")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments:
|
||||
#
|
||||
# class AuditObserver < ActiveRecord::Observer
|
||||
# observe :account, :balance
|
||||
#
|
||||
# def after_update(record)
|
||||
# AuditTrail.new(record, "UPDATED")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
|
||||
#
|
||||
# == Available callback methods
|
||||
#
|
||||
# The observer can implement callback methods for each of the methods described in the Callbacks module.
|
||||
#
|
||||
# == Storing Observers in Rails
|
||||
#
|
||||
# If you're using Active Record within Rails, observer classes are usually stored in app/models with the
|
||||
# naming convention of app/models/audit_observer.rb.
|
||||
#
|
||||
# == Configuration
|
||||
#
|
||||
# In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration
|
||||
# setting in your <tt>config/application.rb</tt> file.
|
||||
#
|
||||
# config.active_record.observers = :comment_observer, :signup_observer
|
||||
#
|
||||
# Observers will not be invoked unless you define these in your application configuration.
|
||||
#
|
||||
# If you are using Active Record outside Rails, activate the observers explicitly in a configuration or
|
||||
# environment file:
|
||||
#
|
||||
# ActiveRecord::Base.add_observer CommentObserver.instance
|
||||
# ActiveRecord::Base.add_observer SignupObserver.instance
|
||||
#
|
||||
# == Loading
|
||||
#
|
||||
# Observers register themselves in the model class they observe, since it is the class that
|
||||
# notifies them of events when they occur. As a side-effect, when an observer is loaded its
|
||||
# corresponding model class is loaded.
|
||||
#
|
||||
# Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
|
||||
# application initializers. Now observers are loaded after application initializers,
|
||||
# so observed models can make use of extensions.
|
||||
#
|
||||
# If by any chance you are using observed models in the initialization you can still
|
||||
# load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
|
||||
# singletons and that call instantiates and registers them.
|
||||
#
|
||||
class Observer < ActiveModel::Observer
|
||||
|
||||
protected
|
||||
|
||||
def observed_classes
|
||||
klasses = super
|
||||
klasses + klasses.map { |klass| klass.descendants }.flatten
|
||||
end
|
||||
|
||||
def add_observer!(klass)
|
||||
super
|
||||
define_callbacks klass
|
||||
end
|
||||
|
||||
def define_callbacks(klass)
|
||||
observer = self
|
||||
observer_name = observer.class.name.underscore.gsub('/', '__')
|
||||
|
||||
ActiveRecord::Callbacks::CALLBACKS.each do |callback|
|
||||
next unless respond_to?(callback)
|
||||
callback_meth = :"_notify_#{observer_name}_for_#{callback}"
|
||||
unless klass.respond_to?(callback_meth)
|
||||
klass.send(:define_method, callback_meth) do |&block|
|
||||
observer.update(callback, self, &block)
|
||||
end
|
||||
klass.send(callback, callback_meth)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -102,7 +102,7 @@ module ActiveRecord
|
|||
# record's primary key, and no callbacks are executed.
|
||||
#
|
||||
# To enforce the object's +before_destroy+ and +after_destroy+
|
||||
# callbacks, Observer methods, or any <tt>:dependent</tt> association
|
||||
# callbacks or any <tt>:dependent</tt> association
|
||||
# options, use <tt>#destroy</tt>.
|
||||
def delete
|
||||
self.class.delete(id) if persisted?
|
||||
|
|
|
@ -168,15 +168,5 @@ module ActiveRecord
|
|||
path = app.paths["db"].first
|
||||
config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"]
|
||||
end
|
||||
|
||||
config.after_initialize do |app|
|
||||
ActiveSupport.on_load(:active_record) do
|
||||
instantiate_observers
|
||||
|
||||
ActionDispatch::Reloader.to_prepare do
|
||||
ActiveRecord::Base.instantiate_observers
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -315,11 +315,9 @@ module ActiveRecord
|
|||
|
||||
# Destroys the records matching +conditions+ by instantiating each
|
||||
# record and calling its +destroy+ method. Each object's callbacks are
|
||||
# executed (including <tt>:dependent</tt> association options and
|
||||
# +before_destroy+/+after_destroy+ Observer methods). Returns the
|
||||
# executed (including <tt>:dependent</tt> association options). Returns the
|
||||
# collection of objects that were destroyed; each will be frozen, to
|
||||
# reflect that no changes should be made (since they can't be
|
||||
# persisted).
|
||||
# reflect that no changes should be made (since they can't be persisted).
|
||||
#
|
||||
# Note: Instantiation, callback execution, and deletion of each
|
||||
# record can be time consuming when you're removing many records at
|
||||
|
@ -419,8 +417,7 @@ module ActiveRecord
|
|||
# Deletes the row with a primary key matching the +id+ argument, using a
|
||||
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
|
||||
# Record objects are not instantiated, so the object's callbacks are not
|
||||
# executed, including any <tt>:dependent</tt> association options or
|
||||
# Observer methods.
|
||||
# executed, including any <tt>:dependent</tt> association options.
|
||||
#
|
||||
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
|
||||
#
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
require 'rails/generators/active_record'
|
||||
|
||||
module ActiveRecord
|
||||
module Generators # :nodoc:
|
||||
class ObserverGenerator < Base # :nodoc:
|
||||
check_class_collision :suffix => "Observer"
|
||||
|
||||
def create_observer_file
|
||||
template 'observer.rb', File.join('app/models', class_path, "#{file_name}_observer.rb")
|
||||
end
|
||||
|
||||
hook_for :test_framework
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +0,0 @@
|
|||
<% module_namespacing do -%>
|
||||
class <%= class_name %>Observer < ActiveRecord::Observer
|
||||
end
|
||||
<% end -%>
|
|
@ -12,7 +12,7 @@ class Pirate # Just reopening it, not defining it
|
|||
after_update :check_changes
|
||||
|
||||
private
|
||||
# after_save/update in sweepers, observers, and the model itself
|
||||
# after_save/update and the model itself
|
||||
# can end up checking dirty status and acting on the results
|
||||
def check_changes
|
||||
if self.changed?
|
||||
|
|
|
@ -1,256 +0,0 @@
|
|||
require 'cases/helper'
|
||||
require 'models/topic'
|
||||
require 'models/developer'
|
||||
require 'models/reply'
|
||||
require 'models/minimalistic'
|
||||
require 'models/comment'
|
||||
|
||||
class SpecialDeveloper < Developer; end
|
||||
|
||||
class DeveloperObserver < ActiveRecord::Observer
|
||||
def calls
|
||||
@calls ||= []
|
||||
end
|
||||
|
||||
def before_save(developer)
|
||||
calls << developer
|
||||
end
|
||||
end
|
||||
|
||||
class SalaryChecker < ActiveRecord::Observer
|
||||
observe :special_developer
|
||||
attr_accessor :last_saved
|
||||
|
||||
def before_save(developer)
|
||||
return developer.salary > 80000
|
||||
end
|
||||
|
||||
module Implementation
|
||||
def after_save(developer)
|
||||
self.last_saved = developer
|
||||
end
|
||||
end
|
||||
include Implementation
|
||||
|
||||
end
|
||||
|
||||
class TopicaAuditor < ActiveRecord::Observer
|
||||
observe :topic
|
||||
|
||||
attr_reader :topic
|
||||
|
||||
def after_find(topic)
|
||||
@topic = topic
|
||||
end
|
||||
end
|
||||
|
||||
class TopicObserver < ActiveRecord::Observer
|
||||
attr_reader :topic
|
||||
|
||||
def after_find(topic)
|
||||
@topic = topic
|
||||
end
|
||||
|
||||
# Create an after_save callback, so a notify_observer hook is created
|
||||
# on :topic.
|
||||
def after_save(nothing)
|
||||
end
|
||||
end
|
||||
|
||||
class MinimalisticObserver < ActiveRecord::Observer
|
||||
attr_reader :minimalistic
|
||||
|
||||
def after_find(minimalistic)
|
||||
@minimalistic = minimalistic
|
||||
end
|
||||
end
|
||||
|
||||
class MultiObserver < ActiveRecord::Observer
|
||||
attr_reader :record
|
||||
|
||||
def self.observed_class() [ Topic, Developer ] end
|
||||
|
||||
cattr_reader :last_inherited
|
||||
@@last_inherited = nil
|
||||
|
||||
def observed_class_inherited_with_testing(subclass)
|
||||
observed_class_inherited_without_testing(subclass)
|
||||
@@last_inherited = subclass
|
||||
end
|
||||
|
||||
alias_method_chain :observed_class_inherited, :testing
|
||||
|
||||
def after_find(record)
|
||||
@record = record
|
||||
end
|
||||
end
|
||||
|
||||
class ValidatedComment < Comment
|
||||
attr_accessor :callers
|
||||
|
||||
before_validation :record_callers
|
||||
|
||||
after_validation do
|
||||
record_callers
|
||||
end
|
||||
|
||||
def record_callers
|
||||
callers << self.class if callers
|
||||
end
|
||||
end
|
||||
|
||||
class ValidatedCommentObserver < ActiveRecord::Observer
|
||||
attr_accessor :callers
|
||||
|
||||
def after_validation(model)
|
||||
callers << self.class if callers
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
class AroundTopic < Topic
|
||||
end
|
||||
|
||||
class AroundTopicObserver < ActiveRecord::Observer
|
||||
observe :around_topic
|
||||
def topic_ids
|
||||
@topic_ids ||= []
|
||||
end
|
||||
|
||||
def around_save(topic)
|
||||
topic_ids << topic.id
|
||||
yield(topic)
|
||||
topic_ids << topic.id
|
||||
end
|
||||
end
|
||||
|
||||
class LifecycleTest < ActiveRecord::TestCase
|
||||
fixtures :topics, :developers, :minimalistics
|
||||
|
||||
def test_before_destroy
|
||||
topic = Topic.find(1)
|
||||
assert_difference 'Topic.count', -(1 + topic.replies.size) do
|
||||
topic.destroy
|
||||
end
|
||||
end
|
||||
|
||||
def test_auto_observer
|
||||
topic_observer = TopicaAuditor.instance
|
||||
assert_nil TopicaAuditor.observed_class
|
||||
assert_equal [Topic], TopicaAuditor.observed_classes.to_a
|
||||
|
||||
topic = Topic.find(1)
|
||||
assert_equal topic.title, topic_observer.topic.title
|
||||
end
|
||||
|
||||
def test_inferred_auto_observer
|
||||
topic_observer = TopicObserver.instance
|
||||
assert_equal Topic, TopicObserver.observed_class
|
||||
|
||||
topic = Topic.find(1)
|
||||
assert_equal topic.title, topic_observer.topic.title
|
||||
end
|
||||
|
||||
def test_observing_two_classes
|
||||
multi_observer = MultiObserver.instance
|
||||
|
||||
topic = Topic.find(1)
|
||||
assert_equal topic.title, multi_observer.record.title
|
||||
|
||||
developer = Developer.find(1)
|
||||
assert_equal developer.name, multi_observer.record.name
|
||||
end
|
||||
|
||||
def test_observing_subclasses
|
||||
multi_observer = MultiObserver.instance
|
||||
|
||||
developer = SpecialDeveloper.find(1)
|
||||
assert_equal developer.name, multi_observer.record.name
|
||||
|
||||
klass = Class.new(Developer)
|
||||
assert_equal klass, multi_observer.last_inherited
|
||||
|
||||
developer = klass.find(1)
|
||||
assert_equal developer.name, multi_observer.record.name
|
||||
end
|
||||
|
||||
def test_after_find_can_be_observed_when_its_not_defined_on_the_model
|
||||
observer = MinimalisticObserver.instance
|
||||
assert_equal Minimalistic, MinimalisticObserver.observed_class
|
||||
|
||||
minimalistic = Minimalistic.find(1)
|
||||
assert_equal minimalistic, observer.minimalistic
|
||||
end
|
||||
|
||||
def test_after_find_can_be_observed_when_its_defined_on_the_model
|
||||
observer = TopicObserver.instance
|
||||
assert_equal Topic, TopicObserver.observed_class
|
||||
|
||||
topic = Topic.find(1)
|
||||
assert_equal topic, observer.topic
|
||||
end
|
||||
|
||||
def test_invalid_observer
|
||||
assert_raise(ArgumentError) { Topic.observers = Object.new; Topic.instantiate_observers }
|
||||
end
|
||||
|
||||
test "model callbacks fire before observers are notified" do
|
||||
callers = []
|
||||
|
||||
comment = ValidatedComment.new
|
||||
comment.callers = ValidatedCommentObserver.instance.callers = callers
|
||||
|
||||
comment.valid?
|
||||
assert_equal [ValidatedComment, ValidatedComment, ValidatedCommentObserver], callers,
|
||||
"model callbacks did not fire before observers were notified"
|
||||
end
|
||||
|
||||
test "able to save developer" do
|
||||
SalaryChecker.instance # activate
|
||||
developer = SpecialDeveloper.new :name => 'Roger', :salary => 100000
|
||||
assert developer.save, "developer with normal salary failed to save"
|
||||
end
|
||||
|
||||
test "unable to save developer with low salary" do
|
||||
SalaryChecker.instance # activate
|
||||
developer = SpecialDeveloper.new :name => 'Rookie', :salary => 50000
|
||||
assert !developer.save, "allowed to save a developer with too low salary"
|
||||
end
|
||||
|
||||
test "able to call methods defined with included module" do # https://rails.lighthouseapp.com/projects/8994/tickets/6065-activerecordobserver-is-not-aware-of-method-added-by-including-modules
|
||||
SalaryChecker.instance # activate
|
||||
developer = SpecialDeveloper.create! :name => 'Roger', :salary => 100000
|
||||
assert_equal developer, SalaryChecker.instance.last_saved
|
||||
end
|
||||
|
||||
test "around filter from observer should accept block" do
|
||||
observer = AroundTopicObserver.instance
|
||||
topic = AroundTopic.new
|
||||
topic.save
|
||||
assert_nil observer.topic_ids.first
|
||||
assert_not_nil observer.topic_ids.last
|
||||
end
|
||||
|
||||
test "able to disable observers" do
|
||||
observer = DeveloperObserver.instance # activate
|
||||
observer.calls.clear
|
||||
|
||||
ActiveRecord::Base.observers.disable DeveloperObserver do
|
||||
Developer.create! :name => 'Ancestor', :salary => 100000
|
||||
SpecialDeveloper.create! :name => 'Descendent', :salary => 100000
|
||||
end
|
||||
|
||||
assert_equal [], observer.calls
|
||||
end
|
||||
|
||||
def test_observer_is_called_once
|
||||
observer = DeveloperObserver.instance # activate
|
||||
observer.calls.clear
|
||||
|
||||
developer = Developer.create! :name => 'Ancestor', :salary => 100000
|
||||
special_developer = SpecialDeveloper.create! :name => 'Descendent', :salary => 100000
|
||||
|
||||
assert_equal [developer, special_developer], observer.calls
|
||||
end
|
||||
|
||||
end
|
|
@ -247,87 +247,6 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
|
||||
class TransactionObserverCallbacksTest < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
fixtures :topics
|
||||
|
||||
class TopicWithObserverAttached < ActiveRecord::Base
|
||||
self.table_name = :topics
|
||||
def history
|
||||
@history ||= []
|
||||
end
|
||||
end
|
||||
|
||||
class TopicWithObserverAttachedObserver < ActiveRecord::Observer
|
||||
def after_commit(record)
|
||||
record.history.push "after_commit"
|
||||
end
|
||||
|
||||
def after_rollback(record)
|
||||
record.history.push "after_rollback"
|
||||
end
|
||||
end
|
||||
|
||||
def test_after_commit_called
|
||||
assert TopicWithObserverAttachedObserver.instance, 'should have observer'
|
||||
|
||||
topic = TopicWithObserverAttached.new
|
||||
topic.save!
|
||||
|
||||
assert_equal %w{ after_commit }, topic.history
|
||||
end
|
||||
|
||||
def test_after_rollback_called
|
||||
assert TopicWithObserverAttachedObserver.instance, 'should have observer'
|
||||
|
||||
topic = TopicWithObserverAttached.new
|
||||
|
||||
Topic.transaction do
|
||||
topic.save!
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
||||
assert topic.id.nil?
|
||||
assert !topic.persisted?
|
||||
assert_equal %w{ after_rollback }, topic.history
|
||||
end
|
||||
|
||||
class TopicWithManualRollbackObserverAttached < ActiveRecord::Base
|
||||
self.table_name = :topics
|
||||
def history
|
||||
@history ||= []
|
||||
end
|
||||
end
|
||||
|
||||
class TopicWithManualRollbackObserverAttachedObserver < ActiveRecord::Observer
|
||||
def after_save(record)
|
||||
record.history.push "after_save"
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
def test_after_save_called_with_manual_rollback
|
||||
assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer'
|
||||
|
||||
topic = TopicWithManualRollbackObserverAttached.new
|
||||
|
||||
assert !topic.save
|
||||
assert_equal nil, topic.id
|
||||
assert !topic.persisted?
|
||||
assert_equal %w{ after_save }, topic.history
|
||||
end
|
||||
def test_after_save_called_with_manual_rollback_bang
|
||||
assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer'
|
||||
|
||||
topic = TopicWithManualRollbackObserverAttached.new
|
||||
|
||||
topic.save!
|
||||
assert_equal nil, topic.id
|
||||
assert !topic.persisted?
|
||||
assert_equal %w{ after_save }, topic.history
|
||||
end
|
||||
end
|
||||
|
||||
class SaveFromAfterCommitBlockTest < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
|
|
|
@ -18,9 +18,6 @@ module Blog
|
|||
# Custom directories with classes and modules you want to be autoloadable.
|
||||
# config.autoload_paths += %W(#{config.root}/extras)
|
||||
|
||||
# Activate observers that should always be running.
|
||||
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
|
||||
|
||||
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
|
||||
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
|
||||
# config.time_zone = 'Central Time (US & Canada)'
|
||||
|
|
|
@ -11,7 +11,6 @@ After reading this guide and trying out the presented concepts, we hope that you
|
|||
* Work with the error messages generated by the validation process
|
||||
* Create callback methods that respond to events in the object life cycle
|
||||
* Create special classes that encapsulate common behavior for your callbacks
|
||||
* Create Observers that respond to life cycle events outside of the original class
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
@ -20,7 +19,7 @@ The Object Life Cycle
|
|||
|
||||
During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object life cycle</em> so that you can control your application and its data.
|
||||
|
||||
Validations allow you to ensure that only valid data is stored in your database. Callbacks and observers allow you to trigger logic before or after an alteration of an object's state.
|
||||
Validations allow you to ensure that only valid data is stored in your database. Callbacks allow you to trigger logic before or after an alteration of an object's state.
|
||||
|
||||
Validations Overview
|
||||
--------------------
|
||||
|
@ -1272,70 +1271,6 @@ end
|
|||
|
||||
You can declare as many callbacks as you want inside your callback classes.
|
||||
|
||||
Observers
|
||||
---------
|
||||
|
||||
Observers are similar to callbacks, but with important differences. Whereas callbacks can pollute a model with code that isn't directly related to its purpose, observers allow you to add the same functionality without changing the code of the model. For example, it could be argued that a `User` model should not include code to send registration confirmation emails. Whenever you use callbacks with code that isn't directly related to your model, you may want to consider creating an observer instead.
|
||||
|
||||
### Creating Observers
|
||||
|
||||
For example, imagine a `User` model where we want to send an email every time a new user is created. Because sending emails is not directly related to our model's purpose, we should create an observer to contain the code implementing this functionality.
|
||||
|
||||
```bash
|
||||
$ rails generate observer User
|
||||
```
|
||||
|
||||
generates `app/models/user_observer.rb` containing the observer class `UserObserver`:
|
||||
|
||||
```ruby
|
||||
class UserObserver < ActiveRecord::Observer
|
||||
end
|
||||
```
|
||||
|
||||
You may now add methods to be called at the desired occasions:
|
||||
|
||||
```ruby
|
||||
class UserObserver < ActiveRecord::Observer
|
||||
def after_create(model)
|
||||
# code to send confirmation email...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
As with callback classes, the observer's methods receive the observed model as a parameter.
|
||||
|
||||
### Registering Observers
|
||||
|
||||
Observers are conventionally placed inside of your `app/models` directory and registered in your application's `config/application.rb` file. For example, the `UserObserver` above would be saved as `app/models/user_observer.rb` and registered in `config/application.rb` this way:
|
||||
|
||||
```ruby
|
||||
# Activate observers that should always be running.
|
||||
config.active_record.observers = :user_observer
|
||||
```
|
||||
|
||||
As usual, settings in `config/environments` take precedence over those in `config/application.rb`. So, if you prefer that an observer doesn't run in all environments, you can simply register it in a specific environment instead.
|
||||
|
||||
### Sharing Observers
|
||||
|
||||
By default, Rails will simply strip "Observer" from an observer's name to find the model it should observe. However, observers can also be used to add behavior to more than one model, and thus it is possible to explicitly specify the models that our observer should observe:
|
||||
|
||||
```ruby
|
||||
class MailerObserver < ActiveRecord::Observer
|
||||
observe :registration, :user
|
||||
|
||||
def after_create(model)
|
||||
# code to send confirmation email...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In this example, the `after_create` method will be called whenever a `Registration` or `User` is created. Note that this new `MailerObserver` would also need to be registered in `config/application.rb` in order to take effect:
|
||||
|
||||
```ruby
|
||||
# Activate observers that should always be running.
|
||||
config.active_record.observers = :mailer_observer
|
||||
```
|
||||
|
||||
Transaction Callbacks
|
||||
---------------------
|
||||
|
||||
|
|
|
@ -67,8 +67,6 @@ class ProductsController < ActionController
|
|||
end
|
||||
```
|
||||
|
||||
If you want a more complicated expiration scheme, you can use cache sweepers to expire cached objects when things change. This is covered in the section on Sweepers.
|
||||
|
||||
By default, page caching automatically gzips files (for example, to `products.html.gz` if user requests `/products`) to reduce the size of data transmitted (web servers are typically configured to use a moderate compression ratio as a compromise, but since precompilation happens once, compression ratio is maximum).
|
||||
|
||||
Nginx is able to serve compressed content directly from disk by enabling `gzip_static`:
|
||||
|
@ -176,102 +174,6 @@ This fragment is then available to all actions in the `ProductsController` using
|
|||
expire_fragment('all_available_products')
|
||||
```
|
||||
|
||||
### Sweepers
|
||||
|
||||
Cache sweeping is a mechanism which allows you to get around having a ton of `expire_{page,action,fragment}` calls in your code. It does this by moving all the work required to expire cached content into an `ActionController::Caching::Sweeper` subclass. This class is an observer and looks for changes to an Active Record object via callbacks, and when a change occurs it expires the caches associated with that object in an around or after filter.
|
||||
|
||||
TIP: Sweepers rely on the use of Active Record and Active Record Observers. The object you are observing must be an Active Record model.
|
||||
|
||||
Continuing with our Product controller example, we could rewrite it with a sweeper like this:
|
||||
|
||||
```ruby
|
||||
class ProductSweeper < ActionController::Caching::Sweeper
|
||||
observe Product # This sweeper is going to keep an eye on the Product model
|
||||
|
||||
# If our sweeper detects that a Product was created call this
|
||||
def after_create(product)
|
||||
expire_cache_for(product)
|
||||
end
|
||||
|
||||
# If our sweeper detects that a Product was updated call this
|
||||
def after_update(product)
|
||||
expire_cache_for(product)
|
||||
end
|
||||
|
||||
# If our sweeper detects that a Product was deleted call this
|
||||
def after_destroy(product)
|
||||
expire_cache_for(product)
|
||||
end
|
||||
|
||||
private
|
||||
def expire_cache_for(product)
|
||||
# Expire the index page now that we added a new product
|
||||
expire_page(controller: 'products', action: 'index')
|
||||
|
||||
# Expire a fragment
|
||||
expire_fragment('all_available_products')
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
You may notice that the actual product gets passed to the sweeper, so if we were caching the edit action for each product, we could add an expire method which specifies the page we want to expire:
|
||||
|
||||
```ruby
|
||||
expire_action(controller: 'products', action: 'edit', id: product.id)
|
||||
```
|
||||
|
||||
Then we add it to our controller to tell it to call the sweeper when certain actions are called. So, if we wanted to expire the cached content for the list and edit actions when the create action was called, we could do the following:
|
||||
|
||||
```ruby
|
||||
class ProductsController < ActionController
|
||||
|
||||
before_filter :authenticate
|
||||
caches_action :index
|
||||
cache_sweeper :product_sweeper
|
||||
|
||||
def index
|
||||
@products = Product.all
|
||||
end
|
||||
|
||||
end
|
||||
```
|
||||
|
||||
Sometimes it is necessary to disambiguate the controller when you call `expire_action`, such as when there are two identically named controllers in separate namespaces:
|
||||
|
||||
```ruby
|
||||
class ProductsController < ActionController
|
||||
caches_action :index
|
||||
|
||||
def index
|
||||
@products = Product.all
|
||||
end
|
||||
end
|
||||
|
||||
module Admin
|
||||
class ProductsController < ActionController
|
||||
cache_sweeper :product_sweeper
|
||||
|
||||
def new
|
||||
@product = Product.new
|
||||
end
|
||||
|
||||
def create
|
||||
@product = Product.create(params[:product])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ProductSweeper < ActionController::Caching::Sweeper
|
||||
observe Product
|
||||
|
||||
def after_create(product)
|
||||
expire_action(controller: '/products', action: 'index')
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Note the use of '/products' here rather than 'products'. If you wanted to expire an action cache for the `Admin::ProductsController`, you would use 'admin/products' instead.
|
||||
|
||||
### SQL Caching
|
||||
|
||||
Query caching is a Rails feature that caches the result set returned by each query so that if Rails encounters the same query again for that request, it will use the cached result set as opposed to running the query against the database again.
|
||||
|
|
|
@ -37,7 +37,7 @@ config.filter_parameters += [:password]
|
|||
This is a setting for Rails itself. If you want to pass settings to individual Rails components, you can do so via the same `config` object in `config/application.rb`:
|
||||
|
||||
```ruby
|
||||
config.active_record.observers = [:hotel_observer, :review_observer]
|
||||
config.active_record.schema_format = :ruby
|
||||
```
|
||||
|
||||
Rails will use that particular setting to configure Active Record.
|
||||
|
@ -614,7 +614,7 @@ Rails.application.config.before_initialize do
|
|||
end
|
||||
```
|
||||
|
||||
WARNING: Some parts of your application, notably observers and routing, are not yet set up at the point where the `after_initialize` block is called.
|
||||
WARNING: Some parts of your application, notably routing, are not yet set up at the point where the `after_initialize` block is called.
|
||||
|
||||
### `Rails::Railtie#initializer`
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ module Rails
|
|||
#
|
||||
# The <tt>Application</tt> class adds a couple more paths to this set. And as in your
|
||||
# <tt>Application</tt>, all folders under +app+ are automatically added to the load path.
|
||||
# If you have an <tt>app/observers</tt> folder for example, it will be added by default.
|
||||
# If you have an <tt>app/services/tt> folder for example, it will be added by default.
|
||||
#
|
||||
# == Endpoint
|
||||
#
|
||||
|
|
|
@ -172,13 +172,11 @@ module Rails
|
|||
"resource_route",
|
||||
"#{orm}:migration",
|
||||
"#{orm}:model",
|
||||
"#{orm}:observer",
|
||||
"#{test}:controller",
|
||||
"#{test}:helper",
|
||||
"#{test}:integration",
|
||||
"#{test}:mailer",
|
||||
"#{test}:model",
|
||||
"#{test}:observer",
|
||||
"#{test}:scaffold",
|
||||
"#{test}:view",
|
||||
"#{test}:performance",
|
||||
|
|
|
@ -78,7 +78,7 @@ module Rails
|
|||
# end
|
||||
#
|
||||
# environment(nil, env: "development") do
|
||||
# "config.active_record.observers = :cacher"
|
||||
# "config.autoload_paths += %W(#{config.root}/extras)"
|
||||
# end
|
||||
def environment(data=nil, options={}, &block)
|
||||
sentinel = /class [a-z_:]+ < Rails::Application/i
|
||||
|
|
|
@ -169,10 +169,10 @@ module Rails
|
|||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# check_class_collision suffix: "Observer"
|
||||
# check_class_collision suffix: "Decorator"
|
||||
#
|
||||
# If the generator is invoked with class name Admin, it will check for
|
||||
# the presence of "AdminObserver".
|
||||
# the presence of "AdminDecorator".
|
||||
#
|
||||
def self.check_class_collision(options={})
|
||||
define_method :check_class_collision do
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
Description:
|
||||
Stubs out a new observer. Pass the observer name, either CamelCased or
|
||||
under_scored, as an argument.
|
||||
|
||||
This generator only invokes your ORM and test framework generators.
|
||||
|
||||
Example:
|
||||
`rails generate observer Account`
|
||||
|
||||
For ActiveRecord and TestUnit it creates:
|
||||
Observer: app/models/account_observer.rb
|
||||
TestUnit: test/models/account_observer_test.rb
|
|
@ -1,7 +0,0 @@
|
|||
module Rails
|
||||
module Generators
|
||||
class ObserverGenerator < NamedBase # :nodoc:
|
||||
hook_for :orm, required: true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
require 'rails/generators/test_unit'
|
||||
|
||||
module TestUnit # :nodoc:
|
||||
module Generators # :nodoc:
|
||||
class ObserverGenerator < Base # :nodoc:
|
||||
check_class_collision suffix: "ObserverTest"
|
||||
|
||||
def create_test_files
|
||||
template 'unit_test.rb', File.join('test/models', class_path, "#{file_name}_observer_test.rb")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
require 'test_helper'
|
||||
|
||||
<% module_namespacing do -%>
|
||||
class <%= class_name %>ObserverTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
<% end -%>
|
|
@ -582,27 +582,6 @@ module ApplicationTests
|
|||
assert app.config.colorize_logging
|
||||
end
|
||||
|
||||
test "config.active_record.observers" do
|
||||
add_to_config <<-RUBY
|
||||
config.active_record.observers = :foo_observer
|
||||
RUBY
|
||||
|
||||
app_file 'app/models/foo.rb', <<-RUBY
|
||||
class Foo < ActiveRecord::Base
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file 'app/models/foo_observer.rb', <<-RUBY
|
||||
class FooObserver < ActiveRecord::Observer
|
||||
end
|
||||
RUBY
|
||||
|
||||
require "#{app_path}/config/environment"
|
||||
|
||||
ActiveRecord::Base
|
||||
assert defined?(FooObserver)
|
||||
end
|
||||
|
||||
test "config.session_store with :active_record_store with activerecord-session_store gem" do
|
||||
begin
|
||||
make_basic_app do |app|
|
||||
|
|
|
@ -95,21 +95,4 @@ class ConsoleTest < ActiveSupport::TestCase
|
|||
load_environment(true)
|
||||
assert value
|
||||
end
|
||||
|
||||
def test_active_record_does_not_panic_when_referencing_an_observed_constant
|
||||
add_to_config "config.active_record.observers = :user_observer"
|
||||
|
||||
app_file "app/models/user.rb", <<-MODEL
|
||||
class User < ActiveRecord::Base
|
||||
end
|
||||
MODEL
|
||||
|
||||
app_file "app/models/user_observer.rb", <<-MODEL
|
||||
class UserObserver < ActiveRecord::Observer
|
||||
end
|
||||
MODEL
|
||||
|
||||
load_environment
|
||||
assert_nothing_raised { User }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -249,28 +249,6 @@ module ApplicationTests
|
|||
assert !File.exists?(File.join(app_path, 'db', 'schema_cache.dump'))
|
||||
end
|
||||
|
||||
def test_load_activerecord_base_when_we_use_observers
|
||||
Dir.chdir(app_path) do
|
||||
`bundle exec rails g model user;
|
||||
bundle exec rake db:migrate;
|
||||
bundle exec rails g observer user;`
|
||||
|
||||
add_to_config "config.active_record.observers = :user_observer"
|
||||
|
||||
assert_equal "0", `bundle exec rails r "puts User.count"`.strip
|
||||
|
||||
app_file "lib/tasks/count_user.rake", <<-RUBY
|
||||
namespace :user do
|
||||
task count: :environment do
|
||||
puts User.count
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
assert_equal "0", `bundle exec rake user:count`.strip
|
||||
end
|
||||
end
|
||||
|
||||
def test_copy_templates
|
||||
Dir.chdir(app_path) do
|
||||
`bundle exec rake rails:templates:copy`
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
require 'generators/generators_test_helper'
|
||||
require 'rails/generators/rails/controller/controller_generator'
|
||||
require 'rails/generators/rails/model/model_generator'
|
||||
require 'rails/generators/rails/observer/observer_generator'
|
||||
require 'rails/generators/mailer/mailer_generator'
|
||||
require 'rails/generators/rails/scaffold/scaffold_generator'
|
||||
|
||||
|
@ -142,26 +141,6 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase
|
|||
end
|
||||
end
|
||||
|
||||
class NamespacedObserverGeneratorTest < NamespacedGeneratorTestCase
|
||||
arguments %w(account)
|
||||
tests Rails::Generators::ObserverGenerator
|
||||
|
||||
def test_invokes_default_orm
|
||||
run_generator
|
||||
assert_file "app/models/test_app/account_observer.rb", /module TestApp/, / class AccountObserver < ActiveRecord::Observer/
|
||||
end
|
||||
|
||||
def test_invokes_default_orm_with_class_path
|
||||
run_generator ["admin/account"]
|
||||
assert_file "app/models/test_app/admin/account_observer.rb", /module TestApp/, / class Admin::AccountObserver < ActiveRecord::Observer/
|
||||
end
|
||||
|
||||
def test_invokes_default_test_framework
|
||||
run_generator
|
||||
assert_file "test/models/test_app/account_observer_test.rb", /module TestApp/, / class AccountObserverTest < ActiveSupport::TestCase/
|
||||
end
|
||||
end
|
||||
|
||||
class NamespacedMailerGeneratorTest < NamespacedGeneratorTestCase
|
||||
arguments %w(notifier foo bar)
|
||||
tests Rails::Generators::MailerGenerator
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
require 'generators/generators_test_helper'
|
||||
require 'rails/generators/rails/observer/observer_generator'
|
||||
|
||||
class ObserverGeneratorTest < Rails::Generators::TestCase
|
||||
include GeneratorsTestHelper
|
||||
arguments %w(account)
|
||||
|
||||
def test_invokes_default_orm
|
||||
run_generator
|
||||
assert_file "app/models/account_observer.rb", /class AccountObserver < ActiveRecord::Observer/
|
||||
end
|
||||
|
||||
def test_invokes_default_orm_with_class_path
|
||||
run_generator ["admin/account"]
|
||||
assert_file "app/models/admin/account_observer.rb", /class Admin::AccountObserver < ActiveRecord::Observer/
|
||||
end
|
||||
|
||||
def test_invokes_default_test_framework
|
||||
run_generator
|
||||
assert_file "test/models/account_observer_test.rb", /class AccountObserverTest < ActiveSupport::TestCase/
|
||||
end
|
||||
|
||||
def test_logs_if_the_test_framework_cannot_be_found
|
||||
content = run_generator ["account", "--test-framework=rspec"]
|
||||
assert_match(/rspec \[not found\]/, content)
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue