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:
Rafael Mendonça França 2012-10-14 20:00:57 -03:00
parent e38d310912
commit ccecab3ba9
39 changed files with 14 additions and 1961 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,7 +45,6 @@ module ActiveRecord
autoload :Migrator, 'active_record/migration'
autoload :ModelSchema
autoload :NestedAttributes
autoload :Observer
autoload :Persistence
autoload :QueryCache
autoload :Querying

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
<% module_namespacing do -%>
class <%= class_name %>Observer < ActiveRecord::Observer
end
<% end -%>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
module Rails
module Generators
class ObserverGenerator < NamedBase # :nodoc:
hook_for :orm, required: true
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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