├── .gitignore
├── .travis.yml
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── gemfiles
├── rails_4_2.gemfile
├── rails_5_0.gemfile
├── rails_5_1.gemfile
├── rails_5_2.gemfile
└── rails_master.gemfile
├── lib
├── generators
│ ├── active_record
│ │ └── observer
│ │ │ ├── observer_generator.rb
│ │ │ └── templates
│ │ │ └── observer.rb
│ ├── rails
│ │ └── observer
│ │ │ ├── USAGE
│ │ │ └── observer_generator.rb
│ └── test_unit
│ │ └── observer
│ │ ├── observer_generator.rb
│ │ └── templates
│ │ └── unit_test.rb
├── rails-observers.rb
└── rails
│ └── observers
│ ├── action_controller
│ ├── caching.rb
│ └── caching
│ │ ├── sweeper.rb
│ │ └── sweeping.rb
│ ├── active_model.rb
│ ├── active_model
│ ├── active_model.rb
│ ├── observer_array.rb
│ └── observing.rb
│ ├── active_resource
│ └── observing.rb
│ ├── activerecord
│ ├── active_record.rb
│ ├── base.rb
│ └── observer.rb
│ ├── railtie.rb
│ └── version.rb
├── rails-observers.gemspec
└── test
├── active_resource_observer_test.rb
├── configuration_test.rb
├── console_test.rb
├── fixtures
├── developers.yml
├── minimalistics.yml
└── topics.yml
├── generators
├── generators_test_helper.rb
├── namespaced_generators_test.rb
└── observer_generator_test.rb
├── helper.rb
├── isolation
└── abstract_unit.rb
├── lifecycle_test.rb
├── models
└── observers.rb
├── observer_array_test.rb
├── observing_test.rb
├── rake_test.rb
├── sweeper_test.rb
└── transaction_callbacks_test.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 | .ruby-version
19 | gemfiles/*.gemfile.lock
20 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | sudo: false
3 | cache: bundler
4 | before_install:
5 | - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
6 | - gem install bundler -v '< 2'
7 | rvm:
8 | - 2.2.10
9 | - 2.3.8
10 | - 2.4.6
11 | - 2.5.5
12 | - 2.6.3
13 | - ruby-head
14 | gemfile:
15 | - gemfiles/rails_4_2.gemfile
16 | - gemfiles/rails_5_0.gemfile
17 | - gemfiles/rails_5_1.gemfile
18 | - gemfiles/rails_5_2.gemfile
19 | - gemfiles/rails_master.gemfile
20 | - Gemfile
21 | matrix:
22 | exclude:
23 | - rvm: ruby-head
24 | gemfile: gemfiles/rails_4_2.gemfile
25 | - rvm: 2.6.3
26 | gemfile: gemfiles/rails_4_2.gemfile
27 | - rvm: 2.6.3
28 | gemfile: gemfiles/rails_5_0.gemfile
29 | - rvm: 2.6.3
30 | gemfile: gemfiles/rails_5_1.gemfile
31 | - rvm: 2.6.3
32 | gemfile: gemfiles/rails_5_2.gemfile
33 | - rvm: 2.5.5
34 | gemfile: gemfiles/rails_4_2.gemfile
35 | - rvm: 2.5.5
36 | gemfile: gemfiles/rails_5_0.gemfile
37 | - rvm: 2.2.10
38 | gemfile: gemfiles/rails_master.gemfile
39 | - rvm: 2.3.8
40 | gemfile: gemfiles/rails_master.gemfile
41 | - rvm: 2.4.6
42 | gemfile: gemfiles/rails_master.gemfile
43 | allow_failures:
44 | - rvm: ruby-head
45 | - gemfile: gemfiles/rails_master.gemfile
46 | fast_finish: true
47 | notifications:
48 | email: false
49 | irc:
50 | on_success: change
51 | on_failure: always
52 | channels:
53 | - "irc.freenode.org#rails-contrib"
54 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in active_record-observers.gemspec
4 | gemspec
5 | gem "minitest", "~> 5.8.4"
6 | gem 'rails', github: 'rails/rails', branch: '5-1-stable'
7 | gem 'activeresource', github: 'rails/activeresource'
8 |
9 | gem 'mocha', require: false
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2016 Steve Klabnik, Rafael Mendonça França
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/rails/rails-observers)
2 | # Rails::Observers
3 |
4 | Rails Observers (removed from core in Rails 4.0)
5 |
6 | ## Installation
7 |
8 | Add this line to your application's Gemfile:
9 | ```ruby
10 | gem 'rails-observers'
11 | ```
12 | And then execute:
13 |
14 | $ bundle
15 |
16 | Or install it yourself as:
17 |
18 | $ gem install rails-observers
19 |
20 | ## Usage
21 |
22 | This gem contains two observers:
23 |
24 | * Active Record Observer
25 | * Action Controller Sweeper
26 |
27 | ### Active Record Observer
28 |
29 | Observer classes respond to life cycle callbacks to implement trigger-like
30 | behavior outside the original class. This is a great way to reduce the
31 | clutter that normally comes when the model class is burdened with
32 | functionality that doesn't pertain to the core responsibility of the
33 | class. Observers are put in `app/models` (e.g.
34 | `app/models/comment_observer.rb`). Example:
35 |
36 | ```ruby
37 | class CommentObserver < ActiveRecord::Observer
38 | def after_save(comment)
39 | Notifications.comment("admin@do.com", "New comment was posted", comment).deliver
40 | end
41 | end
42 | ```
43 |
44 | This Observer sends an email when a Comment#save is finished.
45 |
46 | ```ruby
47 | class ContactObserver < ActiveRecord::Observer
48 | def after_create(contact)
49 | contact.logger.info('New contact added!')
50 | end
51 |
52 | def after_destroy(contact)
53 | contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
54 | end
55 | end
56 | ```
57 |
58 | This Observer uses logger to log when specific callbacks are triggered.
59 |
60 | The convention is to name observers after the class they observe. If you
61 | absolutely need to override this, or want to use one observer for several
62 | classes, use `observe`:
63 |
64 | ```ruby
65 | class NotificationsObserver < ActiveRecord::Observer
66 | observe :comment, :like
67 |
68 | def after_create(record)
69 | # notifiy users of new comment or like
70 | end
71 |
72 | end
73 | ```
74 |
75 | Please note that observers are called in the order that they are defined. This means that callbacks in an observer
76 | will always be called *after* callbacks defined in the model itself. Likewise, `has_one` and `has_many`
77 | use callbacks to enforce `dependent: :destroy`. Therefore, associated records will be destroyed before
78 | the observer's `before_destroy` is called.
79 |
80 | For an observer to be active, it must be registered first. This can be done by adding the following line into the `application.rb`:
81 |
82 | ```
83 | config.active_record.observers = :contact_observer
84 | ```
85 |
86 | Enable multiple observers using array:
87 |
88 | ```
89 | config.active_record.observers = [:contact_observer, :user_observer]
90 | ```
91 |
92 | Observers can also be registered on an environment-specific basis by simply using the corresponding environment's configuration file instead of `application.rb`.
93 |
94 | ### Action Controller Sweeper
95 |
96 | Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
97 | They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
98 |
99 | ```ruby
100 | class ListSweeper < ActionController::Caching::Sweeper
101 | observe List, Item
102 |
103 | def after_save(record)
104 | list = record.is_a?(List) ? record : record.list
105 | expire_page(controller: "lists", action: %w( show public feed ), id: list.id)
106 | expire_action(controller: "lists", action: "all")
107 | list.shares.each { |share| expire_page(controller: "lists", action: "show", id: share.url_key) }
108 | end
109 | end
110 | ```
111 |
112 | The sweeper is assigned in the controllers that wish to have its job performed using the `cache_sweeper` class method:
113 |
114 | ```ruby
115 | class ListsController < ApplicationController
116 | caches_action :index, :show, :public, :feed
117 | cache_sweeper :list_sweeper, only: [ :edit, :destroy, :share ]
118 | end
119 | ```
120 |
121 | In the example above, four actions are cached and three actions are responsible for expiring those caches.
122 |
123 | You can also name an explicit class in the declaration of a sweeper, which is needed if the sweeper is in a module:
124 |
125 | ```ruby
126 | class ListsController < ApplicationController
127 | caches_action :index, :show, :public, :feed
128 | cache_sweeper OpenBar::Sweeper, only: [ :edit, :destroy, :share ]
129 | end
130 | ```
131 |
132 | ## Contributing
133 |
134 | 1. Fork it
135 | 2. Create your feature branch (`git checkout -b my-new-feature`)
136 | 3. Commit your changes (`git commit -am 'Added some feature'`)
137 | 4. Push to the branch (`git push origin my-new-feature`)
138 | 5. Create new Pull Request
139 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | require "bundler/gem_tasks"
3 |
4 | require 'rake/testtask'
5 |
6 | Rake::TestTask.new("test:regular") do |t|
7 | t.libs = ["test"]
8 | t.pattern = "test/*_test.rb"
9 | t.ruby_opts = ['-w']
10 | end
11 |
12 | Rake::TestTask.new("test:generators") do |t|
13 | t.libs = ["test"]
14 | t.pattern = "test/generators/*_test.rb"
15 | t.ruby_opts = ['-w']
16 | end
17 |
18 | task :default => :test
19 | task :test => ['test:regular', 'test:generators']
20 |
--------------------------------------------------------------------------------
/gemfiles/rails_4_2.gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "mocha", require: false
4 | gem "minitest", "~> 5.8.4"
5 | gem "rails", "~> 4.2.0"
6 | gem 'sqlite3', '~> 1.3.6'
7 |
8 | gemspec path: "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails_5_0.gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem's dependencies in active_record-observers.gemspec
4 | gem "rails", github: "rails/rails", branch: "5-0-stable"
5 | gem 'sqlite3', '~> 1.3.6'
6 | gem "minitest", "~> 5.8.4"
7 | gem "activeresource", github: "rails/activeresource"
8 |
9 | gem "mocha", require: false
10 |
11 | gemspec path: "../"
12 |
--------------------------------------------------------------------------------
/gemfiles/rails_5_1.gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem's dependencies in active_record-observers.gemspec
4 | gem "rails", github: "rails/rails", branch: "5-1-stable"
5 | gem "minitest", "~> 5.8.4"
6 | gem "activeresource", github: "rails/activeresource"
7 |
8 | gem "mocha", require: false
9 |
10 | gemspec path: "../"
11 |
--------------------------------------------------------------------------------
/gemfiles/rails_5_2.gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem's dependencies in active_record-observers.gemspec
4 | gem "rails", github: "rails/rails", branch: "5-2-stable"
5 | gem "minitest", "~> 5.8.4"
6 | gem "activeresource", github: "rails/activeresource"
7 | gem "bootsnap"
8 |
9 | gem "mocha", require: false
10 |
11 | gemspec path: "../"
12 |
--------------------------------------------------------------------------------
/gemfiles/rails_master.gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem's dependencies in active_record-observers.gemspec
4 | gem "rails", github: "rails/rails"
5 | gem "minitest", "~> 5.8.4"
6 | gem "activeresource", github: "rails/activeresource"
7 |
8 | gem "mocha", require: false
9 |
10 | gemspec path: "../"
11 |
--------------------------------------------------------------------------------
/lib/generators/active_record/observer/observer_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators/active_record'
2 |
3 | module ActiveRecord
4 | module Generators
5 | class ObserverGenerator < Base
6 | check_class_collision :suffix => "Observer"
7 |
8 | source_root File.expand_path("../templates", __FILE__)
9 |
10 | def create_observer_file
11 | template 'observer.rb', File.join('app/models', class_path, "#{file_name}_observer.rb")
12 | end
13 |
14 | hook_for :test_framework
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/generators/active_record/observer/templates/observer.rb:
--------------------------------------------------------------------------------
1 | <% module_namespacing do -%>
2 | class <%= class_name %>Observer < ActiveRecord::Observer
3 | end
4 | <% end -%>
5 |
--------------------------------------------------------------------------------
/lib/generators/rails/observer/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Stubs out a new observer. Pass the observer name, either CamelCased or
3 | under_scored, as an argument.
4 |
5 | This generator only invokes your ORM and test framework generators.
6 |
7 | Example:
8 | `rails generate observer Account`
9 |
10 | For ActiveRecord and TestUnit it creates:
11 | Observer: app/models/account_observer.rb
12 | TestUnit: test/unit/account_observer_test.rb
13 |
--------------------------------------------------------------------------------
/lib/generators/rails/observer/observer_generator.rb:
--------------------------------------------------------------------------------
1 | module Rails
2 | module Generators
3 | class ObserverGenerator < NamedBase #metagenerator
4 | hook_for :orm, :required => true
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/generators/test_unit/observer/observer_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators/test_unit'
2 |
3 | module TestUnit
4 | module Generators
5 | class ObserverGenerator < Base
6 | check_class_collision :suffix => "ObserverTest"
7 |
8 | source_root File.expand_path("../templates", __FILE__)
9 |
10 | def create_test_files
11 | template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_observer_test.rb")
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/generators/test_unit/observer/templates/unit_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | <% module_namespacing do -%>
4 | class <%= class_name %>ObserverTest < ActiveSupport::TestCase
5 | # test "the truth" do
6 | # assert true
7 | # end
8 | end
9 | <% end -%>
10 |
--------------------------------------------------------------------------------
/lib/rails-observers.rb:
--------------------------------------------------------------------------------
1 | require 'rails/observers/railtie' if defined? Rails
2 | require 'rails/observers/version'
3 |
--------------------------------------------------------------------------------
/lib/rails/observers/action_controller/caching.rb:
--------------------------------------------------------------------------------
1 | module ActionController #:nodoc:
2 | module Caching
3 | extend ActiveSupport::Autoload
4 |
5 | eager_autoload do
6 | autoload :Sweeper, 'rails/observers/action_controller/caching/sweeper'
7 | autoload :Sweeping, 'rails/observers/action_controller/caching/sweeping'
8 | end
9 |
10 | ActionController::Base.extend Sweeping::ClassMethods if defined?(ActiveRecord)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/rails/observers/action_controller/caching/sweeper.rb:
--------------------------------------------------------------------------------
1 | module ActionController #:nodoc:
2 | module Caching
3 | class Sweeper < ActiveRecord::Observer #:nodoc:
4 | def initialize(*args)
5 | super
6 | self.controller = nil
7 | end
8 |
9 | def controller
10 | Thread.current["observer:#{self.class.name}_controller"]
11 | end
12 |
13 | def controller=(controller)
14 | Thread.current["observer:#{self.class.name}_controller"] = controller
15 | end
16 |
17 | def before(controller)
18 | self.controller = controller
19 | callback(:before) if controller.perform_caching
20 | true # before method from sweeper should always return true
21 | end
22 |
23 | def after(controller)
24 | self.controller = controller
25 | callback(:after) if controller.perform_caching
26 | end
27 |
28 | def around(controller)
29 | before(controller)
30 | yield
31 | after(controller)
32 | ensure
33 | clean_up
34 | end
35 |
36 | protected
37 | # gets the action cache path for the given options.
38 | def action_path_for(options)
39 | Actions::ActionCachePath.new(controller, options).path
40 | end
41 |
42 | # Retrieve instance variables set in the controller.
43 | def assigns(key)
44 | controller.instance_variable_get("@#{key}")
45 | end
46 |
47 | private
48 | def clean_up
49 | # Clean up, so that the controller can be collected after this request
50 | self.controller = nil
51 | end
52 |
53 | def callback(timing)
54 | controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
55 | action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
56 |
57 | __send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
58 | __send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
59 | end
60 |
61 | def method_missing(method, *arguments, &block)
62 | return super if controller.nil?
63 | controller.__send__(method, *arguments, &block)
64 | end
65 |
66 | def respond_to_missing?(method, include_private = false)
67 | (controller.present? && controller.respond_to?(method)) || super
68 | end
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/lib/rails/observers/action_controller/caching/sweeping.rb:
--------------------------------------------------------------------------------
1 | module ActionController #:nodoc:
2 | module Caching
3 | # Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
4 | # They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
5 | #
6 | # class ListSweeper < ActionController::Caching::Sweeper
7 | # observe List, Item
8 | #
9 | # def after_save(record)
10 | # list = record.is_a?(List) ? record : record.list
11 | # expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
12 | # expire_action(:controller => "lists", :action => "all")
13 | # list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
14 | # end
15 | # end
16 | #
17 | # The sweeper is assigned in the controllers that wish to have its job performed using the cache_sweeper class method:
18 | #
19 | # class ListsController < ApplicationController
20 | # caches_action :index, :show, :public, :feed
21 | # cache_sweeper :list_sweeper, :only => [ :edit, :destroy, :share ]
22 | # end
23 | #
24 | # In the example above, four actions are cached and three actions are responsible for expiring those caches.
25 | #
26 | # You can also name an explicit class in the declaration of a sweeper, which is needed if the sweeper is in a module:
27 | #
28 | # class ListsController < ApplicationController
29 | # caches_action :index, :show, :public, :feed
30 | # cache_sweeper OpenBar::Sweeper, :only => [ :edit, :destroy, :share ]
31 | # end
32 | module Sweeping
33 | module ClassMethods #:nodoc:
34 | def cache_sweeper(*sweepers)
35 | configuration = sweepers.extract_options!
36 |
37 | sweepers.each do |sweeper|
38 | ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base)
39 | sweeper_instance = (sweeper.is_a?(Symbol) ? Object.const_get(sweeper.to_s.classify) : sweeper).instance
40 |
41 | if sweeper_instance.is_a?(Sweeper)
42 | around_action(sweeper_instance, :only => configuration[:only])
43 | else
44 | after_action(sweeper_instance, :only => configuration[:only])
45 | end
46 | end
47 | end
48 | end
49 | end
50 |
51 | if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
52 | require 'rails/observers/action_controller/caching/sweeper'
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/rails/observers/active_model.rb:
--------------------------------------------------------------------------------
1 | require 'rails/observers/active_model/active_model'
2 |
--------------------------------------------------------------------------------
/lib/rails/observers/active_model/active_model.rb:
--------------------------------------------------------------------------------
1 | module ActiveModel
2 | autoload :Observer, 'rails/observers/active_model/observing'
3 | autoload :Observing, 'rails/observers/active_model/observing'
4 | end
5 |
--------------------------------------------------------------------------------
/lib/rails/observers/active_model/observer_array.rb:
--------------------------------------------------------------------------------
1 | require 'set'
2 |
3 | module ActiveModel
4 | # Stores the enabled/disabled state of individual observers for
5 | # a particular model class.
6 | class ObserverArray < Array
7 | attr_reader :model_class
8 | def initialize(model_class, *args) #:nodoc:
9 | @model_class = model_class
10 | super(*args)
11 | end
12 |
13 | # Returns +true+ if the given observer is disabled for the model class,
14 | # +false+ otherwise.
15 | def disabled_for?(observer) #:nodoc:
16 | disabled_observers.include?(observer.class)
17 | end
18 |
19 | # Disables one or more observers. This supports multiple forms:
20 | #
21 | # ORM.observers.disable :all
22 | # # => disables all observers for all models subclassed from
23 | # # an ORM base class that includes ActiveModel::Observing
24 | # # e.g. ActiveRecord::Base
25 | #
26 | # ORM.observers.disable :user_observer
27 | # # => disables the UserObserver
28 | #
29 | # User.observers.disable AuditTrail
30 | # # => disables the AuditTrail observer for User notifications.
31 | # # Other models will still notify the AuditTrail observer.
32 | #
33 | # ORM.observers.disable :observer_1, :observer_2
34 | # # => disables Observer1 and Observer2 for all models.
35 | #
36 | # User.observers.disable :all do
37 | # # all user observers are disabled for
38 | # # just the duration of the block
39 | # end
40 | def disable(*observers, &block)
41 | set_enablement(false, observers, &block)
42 | end
43 |
44 | # Enables one or more observers. This supports multiple forms:
45 | #
46 | # ORM.observers.enable :all
47 | # # => enables all observers for all models subclassed from
48 | # # an ORM base class that includes ActiveModel::Observing
49 | # # e.g. ActiveRecord::Base
50 | #
51 | # ORM.observers.enable :user_observer
52 | # # => enables the UserObserver
53 | #
54 | # User.observers.enable AuditTrail
55 | # # => enables the AuditTrail observer for User notifications.
56 | # # Other models will not be affected (i.e. they will not
57 | # # trigger notifications to AuditTrail if previously disabled)
58 | #
59 | # ORM.observers.enable :observer_1, :observer_2
60 | # # => enables Observer1 and Observer2 for all models.
61 | #
62 | # User.observers.enable :all do
63 | # # all user observers are enabled for
64 | # # just the duration of the block
65 | # end
66 | #
67 | # Note: all observers are enabled by default. This method is only
68 | # useful when you have previously disabled one or more observers.
69 | def enable(*observers, &block)
70 | set_enablement(true, observers, &block)
71 | end
72 |
73 | protected
74 |
75 | def disabled_observers #:nodoc:
76 | @disabled_observers ||= Set.new
77 | end
78 |
79 | def observer_class_for(observer) #:nodoc:
80 | return observer if observer.is_a?(Class)
81 |
82 | if observer.respond_to?(:to_sym) # string/symbol
83 | observer.to_s.camelize.constantize
84 | else
85 | raise ArgumentError, "#{observer} was not a class or a " +
86 | "lowercase, underscored class name as expected."
87 | end
88 | end
89 |
90 | def start_transaction #:nodoc:
91 | disabled_observer_stack.push(disabled_observers.dup)
92 | each_subclass_array do |array|
93 | array.start_transaction
94 | end
95 | end
96 |
97 | def disabled_observer_stack #:nodoc:
98 | @disabled_observer_stack ||= []
99 | end
100 |
101 | def end_transaction #:nodoc:
102 | @disabled_observers = disabled_observer_stack.pop
103 | each_subclass_array do |array|
104 | array.end_transaction
105 | end
106 | end
107 |
108 | def transaction #:nodoc:
109 | start_transaction
110 |
111 | begin
112 | yield
113 | ensure
114 | end_transaction
115 | end
116 | end
117 |
118 | def each_subclass_array #:nodoc:
119 | model_class.descendants.each do |subclass|
120 | yield subclass.observers
121 | end
122 | end
123 |
124 | def set_enablement(enabled, observers) #:nodoc:
125 | if block_given?
126 | transaction do
127 | set_enablement(enabled, observers)
128 | yield
129 | end
130 | else
131 | observers = ActiveModel::Observer.descendants if observers == [:all]
132 | observers.each do |obs|
133 | klass = observer_class_for(obs)
134 |
135 | unless klass < ActiveModel::Observer
136 | raise ArgumentError.new("#{obs} does not refer to a valid observer")
137 | end
138 |
139 | if enabled
140 | disabled_observers.delete(klass)
141 | else
142 | disabled_observers << klass
143 | end
144 | end
145 |
146 | each_subclass_array do |array|
147 | array.set_enablement(enabled, observers)
148 | end
149 | end
150 | end
151 | end
152 | end
153 |
--------------------------------------------------------------------------------
/lib/rails/observers/active_model/observing.rb:
--------------------------------------------------------------------------------
1 | require 'singleton'
2 | require 'rails/observers/active_model/observer_array'
3 | require 'active_support/core_ext/module/aliasing'
4 | require 'active_support/core_ext/module/remove_method'
5 | require 'active_support/core_ext/string/inflections'
6 | require 'active_support/core_ext/enumerable'
7 | require 'active_support/core_ext/object/try'
8 | require 'active_support/descendants_tracker'
9 |
10 | module ActiveModel
11 | # == Active Model Observers Activation
12 | module Observing
13 | extend ActiveSupport::Concern
14 |
15 | included do
16 | extend ActiveSupport::DescendantsTracker
17 | end
18 |
19 | module ClassMethods
20 | # Activates the observers assigned.
21 | #
22 | # class ORM
23 | # include ActiveModel::Observing
24 | # end
25 | #
26 | # # Calls PersonObserver.instance
27 | # ORM.observers = :person_observer
28 | #
29 | # # Calls Cacher.instance and GarbageCollector.instance
30 | # ORM.observers = :cacher, :garbage_collector
31 | #
32 | # # Same as above, just using explicit class references
33 | # ORM.observers = Cacher, GarbageCollector
34 | #
35 | # Note: Setting this does not instantiate the observers yet.
36 | # instantiate_observers is called during startup, and before
37 | # each development request.
38 | def observers=(*values)
39 | observers.replace(values.flatten)
40 | end
41 |
42 | # Gets an array of observers observing this model. The array also provides
43 | # +enable+ and +disable+ methods that allow you to selectively enable and
44 | # disable observers (see ActiveModel::ObserverArray.enable and
45 | # ActiveModel::ObserverArray.disable for more on this).
46 | #
47 | # class ORM
48 | # include ActiveModel::Observing
49 | # end
50 | #
51 | # ORM.observers = :cacher, :garbage_collector
52 | # ORM.observers # => [:cacher, :garbage_collector]
53 | # ORM.observers.class # => ActiveModel::ObserverArray
54 | def observers
55 | @observers ||= ObserverArray.new(self)
56 | end
57 |
58 | # Returns the current observer instances.
59 | #
60 | # class Foo
61 | # include ActiveModel::Observing
62 | #
63 | # attr_accessor :status
64 | # end
65 | #
66 | # class FooObserver < ActiveModel::Observer
67 | # def on_spec(record, *args)
68 | # record.status = true
69 | # end
70 | # end
71 | #
72 | # Foo.observers = FooObserver
73 | # Foo.instantiate_observers
74 | #
75 | # Foo.observer_instances # => [#]
76 | def observer_instances
77 | @observer_instances ||= []
78 | end
79 |
80 | # Instantiate the global observers.
81 | #
82 | # class Foo
83 | # include ActiveModel::Observing
84 | #
85 | # attr_accessor :status
86 | # end
87 | #
88 | # class FooObserver < ActiveModel::Observer
89 | # def on_spec(record, *args)
90 | # record.status = true
91 | # end
92 | # end
93 | #
94 | # Foo.observers = FooObserver
95 | #
96 | # foo = Foo.new
97 | # foo.status = false
98 | # foo.notify_observers(:on_spec)
99 | # foo.status # => false
100 | #
101 | # Foo.instantiate_observers # => [FooObserver]
102 | #
103 | # foo = Foo.new
104 | # foo.status = false
105 | # foo.notify_observers(:on_spec)
106 | # foo.status # => true
107 | def instantiate_observers
108 | observers.each { |o| instantiate_observer(o) }
109 | end
110 |
111 | # Add a new observer to the pool. The new observer needs to respond to
112 | # update, otherwise it raises an +ArgumentError+ exception.
113 | #
114 | # class Foo
115 | # include ActiveModel::Observing
116 | # end
117 | #
118 | # class FooObserver < ActiveModel::Observer
119 | # end
120 | #
121 | # Foo.add_observer(FooObserver.instance)
122 | #
123 | # Foo.observers_instance
124 | # # => [#]
125 | def add_observer(observer)
126 | unless observer.respond_to? :update
127 | raise ArgumentError, "observer needs to respond to 'update'"
128 | end
129 | observer_instances << observer
130 | end
131 |
132 | # Fires notifications to model's observers.
133 | #
134 | # def save
135 | # notify_observers(:before_save)
136 | # ...
137 | # notify_observers(:after_save)
138 | # end
139 | #
140 | # Custom notifications can be sent in a similar fashion:
141 | #
142 | # notify_observers(:custom_notification, :foo)
143 | #
144 | # This will call custom_notification, passing as arguments
145 | # the current object and :foo.
146 | def notify_observers(*args)
147 | observer_instances.each { |observer| observer.update(*args) }
148 | end
149 |
150 | # Returns the total number of instantiated observers.
151 | #
152 | # class Foo
153 | # include ActiveModel::Observing
154 | #
155 | # attr_accessor :status
156 | # end
157 | #
158 | # class FooObserver < ActiveModel::Observer
159 | # def on_spec(record, *args)
160 | # record.status = true
161 | # end
162 | # end
163 | #
164 | # Foo.observers = FooObserver
165 | # Foo.observers_count # => 0
166 | # Foo.instantiate_observers
167 | # Foo.observers_count # => 1
168 | def observers_count
169 | observer_instances.size
170 | end
171 |
172 | # count_observers is deprecated. Use #observers_count.
173 | def count_observers
174 | msg = "count_observers is deprecated in favor of observers_count"
175 | ActiveSupport::Deprecation.warn(msg)
176 | observers_count
177 | end
178 |
179 | protected
180 | def instantiate_observer(observer) #:nodoc:
181 | # string/symbol
182 | if observer.respond_to?(:to_sym)
183 | observer = observer.to_s.camelize.constantize
184 | end
185 | if observer.respond_to?(:instance)
186 | observer.instance
187 | else
188 | raise ArgumentError,
189 | "#{observer} must be a lowercase, underscored class name (or " +
190 | "the class itself) responding to the method :instance. " +
191 | "Example: Person.observers = :big_brother # calls " +
192 | "BigBrother.instance"
193 | end
194 | end
195 |
196 | # Notify observers when the observed class is subclassed.
197 | def inherited(subclass) #:nodoc:
198 | super
199 | notify_observers :observed_class_inherited, subclass
200 | end
201 | end
202 |
203 | # Notify a change to the list of observers.
204 | #
205 | # class Foo
206 | # include ActiveModel::Observing
207 | #
208 | # attr_accessor :status
209 | # end
210 | #
211 | # class FooObserver < ActiveModel::Observer
212 | # def on_spec(record, *args)
213 | # record.status = true
214 | # end
215 | # end
216 | #
217 | # Foo.observers = FooObserver
218 | # Foo.instantiate_observers # => [FooObserver]
219 | #
220 | # foo = Foo.new
221 | # foo.status = false
222 | # foo.notify_observers(:on_spec)
223 | # foo.status # => true
224 | #
225 | # See ActiveModel::Observing::ClassMethods.notify_observers for more
226 | # information.
227 | def notify_observers(method, *extra_args)
228 | self.class.notify_observers(method, self, *extra_args)
229 | end
230 | end
231 |
232 | # == Active Model Observers
233 | #
234 | # Observer classes respond to life cycle callbacks to implement trigger-like
235 | # behavior outside the original class. This is a great way to reduce the
236 | # clutter that normally comes when the model class is burdened with
237 | # functionality that doesn't pertain to the core responsibility of the
238 | # class.
239 | #
240 | # class CommentObserver < ActiveModel::Observer
241 | # def after_save(comment)
242 | # Notifications.comment('admin@do.com', 'New comment was posted', comment).deliver
243 | # end
244 | # end
245 | #
246 | # This Observer sends an email when a Comment#save is finished.
247 | #
248 | # class ContactObserver < ActiveModel::Observer
249 | # def after_create(contact)
250 | # contact.logger.info('New contact added!')
251 | # end
252 | #
253 | # def after_destroy(contact)
254 | # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
255 | # end
256 | # end
257 | #
258 | # This Observer uses logger to log when specific callbacks are triggered.
259 | #
260 | # == Observing a class that can't be inferred
261 | #
262 | # Observers will by default be mapped to the class with which they share a
263 | # name. So CommentObserver will be tied to observing Comment,
264 | # ProductManagerObserver to ProductManager, and so on. If
265 | # you want to name your observer differently than the class you're interested
266 | # in observing, you can use the Observer.observe class method which
267 | # takes either the concrete class (Product) or a symbol for that
268 | # class (:product):
269 | #
270 | # class AuditObserver < ActiveModel::Observer
271 | # observe :account
272 | #
273 | # def after_update(account)
274 | # AuditTrail.new(account, 'UPDATED')
275 | # end
276 | # end
277 | #
278 | # If the audit observer needs to watch more than one kind of object, this can
279 | # be specified with multiple arguments:
280 | #
281 | # class AuditObserver < ActiveModel::Observer
282 | # observe :account, :balance
283 | #
284 | # def after_update(record)
285 | # AuditTrail.new(record, 'UPDATED')
286 | # end
287 | # end
288 | #
289 | # The AuditObserver will now act on both updates to Account
290 | # and Balance by treating them both as records.
291 | #
292 | # If you're using an Observer in a Rails application with Active Record, be
293 | # sure to read about the necessary configuration in the documentation for
294 | # ActiveRecord::Observer.
295 | class Observer
296 | include Singleton
297 | extend ActiveSupport::DescendantsTracker
298 |
299 | class << self
300 | # Attaches the observer to the supplied model classes.
301 | #
302 | # class AuditObserver < ActiveModel::Observer
303 | # observe :account, :balance
304 | # end
305 | #
306 | # AuditObserver.observed_classes # => [Account, Balance]
307 | def observe(*models)
308 | models.flatten!
309 | models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model }
310 | singleton_class.redefine_method(:observed_classes) { models }
311 | end
312 |
313 | # Returns an array of Classes to observe.
314 | #
315 | # AccountObserver.observed_classes # => [Account]
316 | #
317 | # You can override this instead of using the +observe+ helper.
318 | #
319 | # class AuditObserver < ActiveModel::Observer
320 | # def self.observed_classes
321 | # [Account, Balance]
322 | # end
323 | # end
324 | def observed_classes
325 | Array(observed_class)
326 | end
327 |
328 | # Returns the class observed by default. It's inferred from the observer's
329 | # class name.
330 | #
331 | # PersonObserver.observed_class # => Person
332 | # AccountObserver.observed_class # => Account
333 | def observed_class
334 | name[/(.*)Observer/, 1].try :constantize
335 | end
336 | end
337 |
338 | # Start observing the declared classes and their subclasses.
339 | # Called automatically by the instance method.
340 | def initialize #:nodoc:
341 | observed_classes.each { |klass| add_observer!(klass) }
342 | end
343 |
344 | def observed_classes #:nodoc:
345 | self.class.observed_classes
346 | end
347 |
348 | # Send observed_method(object) if the method exists and
349 | # the observer is enabled for the given object's class.
350 | def update(observed_method, object, *extra_args, &block) #:nodoc:
351 | return if !respond_to?(observed_method) || disabled_for?(object)
352 | send(observed_method, object, *extra_args, &block)
353 | end
354 |
355 | # Special method sent by the observed class when it is inherited.
356 | # Passes the new subclass.
357 | def observed_class_inherited(subclass) #:nodoc:
358 | self.class.observe(observed_classes + [subclass])
359 | add_observer!(subclass)
360 | end
361 |
362 | protected
363 | def add_observer!(klass) #:nodoc:
364 | klass.add_observer(self)
365 | end
366 |
367 | # Returns true if notifications are disabled for this object.
368 | def disabled_for?(object) #:nodoc:
369 | klass = object.class
370 | return false unless klass.respond_to?(:observers)
371 | klass.observers.disabled_for?(self)
372 | end
373 | end
374 | end
375 |
--------------------------------------------------------------------------------
/lib/rails/observers/active_resource/observing.rb:
--------------------------------------------------------------------------------
1 | require 'rails/observers/active_model/observing'
2 |
3 | module ActiveResource
4 | module Observing
5 | def self.prepended(context)
6 | context.include ActiveModel::Observing
7 | end
8 |
9 | def create(*)
10 | notify_observers(:before_create)
11 | if result = super
12 | notify_observers(:after_create)
13 | end
14 | result
15 | end
16 |
17 | def save(*)
18 | notify_observers(:before_save)
19 | if result = super
20 | notify_observers(:after_save)
21 | end
22 | result
23 | end
24 |
25 | def update(*)
26 | notify_observers(:before_update)
27 | if result = super
28 | notify_observers(:after_update)
29 | end
30 | result
31 | end
32 |
33 | def destroy(*)
34 | notify_observers(:before_destroy)
35 | if result = super
36 | notify_observers(:after_destroy)
37 | end
38 | result
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/rails/observers/activerecord/active_record.rb:
--------------------------------------------------------------------------------
1 | require 'rails/observers/activerecord/base'
2 |
3 | module ActiveRecord
4 | autoload :Observer, 'rails/observers/activerecord/observer'
5 | end
6 |
--------------------------------------------------------------------------------
/lib/rails/observers/activerecord/base.rb:
--------------------------------------------------------------------------------
1 | require 'rails/observers/active_model/active_model'
2 |
3 | module ActiveRecord
4 | class Base
5 | extend ActiveModel::Observing::ClassMethods
6 | include ActiveModel::Observing
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/rails/observers/activerecord/observer.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 | # = Active Record Observer
3 | #
4 | # Observer classes respond to life cycle callbacks to implement trigger-like
5 | # behavior outside the original class. This is a great way to reduce the
6 | # clutter that normally comes when the model class is burdened with
7 | # functionality that doesn't pertain to the core responsibility of the
8 | # class. Example:
9 | #
10 | # class CommentObserver < ActiveRecord::Observer
11 | # def after_save(comment)
12 | # Notifications.comment("admin@do.com", "New comment was posted", comment).deliver
13 | # end
14 | # end
15 | #
16 | # This Observer sends an email when a Comment#save is finished.
17 | #
18 | # class ContactObserver < ActiveRecord::Observer
19 | # def after_create(contact)
20 | # contact.logger.info('New contact added!')
21 | # end
22 | #
23 | # def after_destroy(contact)
24 | # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
25 | # end
26 | # end
27 | #
28 | # This Observer uses logger to log when specific callbacks are triggered.
29 | #
30 | # == Observing a class that can't be inferred
31 | #
32 | # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
33 | # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
34 | # differently than the class you're interested in observing, you can use the Observer.observe class method which takes
35 | # either the concrete class (Product) or a symbol for that class (:product):
36 | #
37 | # class AuditObserver < ActiveRecord::Observer
38 | # observe :account
39 | #
40 | # def after_update(account)
41 | # AuditTrail.new(account, "UPDATED")
42 | # end
43 | # end
44 | #
45 | # If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments:
46 | #
47 | # class AuditObserver < ActiveRecord::Observer
48 | # observe :account, :balance
49 | #
50 | # def after_update(record)
51 | # AuditTrail.new(record, "UPDATED")
52 | # end
53 | # end
54 | #
55 | # The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
56 | #
57 | # == Available callback methods
58 | #
59 | # The observer can implement callback methods for each of the methods described in the Callbacks module.
60 | #
61 | # == Storing Observers in Rails
62 | #
63 | # If you're using Active Record within Rails, observer classes are usually stored in app/models with the
64 | # naming convention of app/models/audit_observer.rb.
65 | #
66 | # == Configuration
67 | #
68 | # In order to activate an observer, list it in the config.active_record.observers configuration
69 | # setting in your config/application.rb file.
70 | #
71 | # config.active_record.observers = :comment_observer, :signup_observer
72 | #
73 | # Observers will not be invoked unless you define these in your application configuration.
74 | #
75 | # If you are using Active Record outside Rails, activate the observers explicitly in a configuration or
76 | # environment file:
77 | #
78 | # ActiveRecord::Base.add_observer CommentObserver.instance
79 | # ActiveRecord::Base.add_observer SignupObserver.instance
80 | #
81 | # == Loading
82 | #
83 | # Observers register themselves in the model class they observe, since it is the class that
84 | # notifies them of events when they occur. As a side-effect, when an observer is loaded its
85 | # corresponding model class is loaded.
86 | #
87 | # Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
88 | # application initializers. Now observers are loaded after application initializers,
89 | # so observed models can make use of extensions.
90 | #
91 | # If by any chance you are using observed models in the initialization you can still
92 | # load their observers by calling ModelObserver.instance before. Observers are
93 | # singletons and that call instantiates and registers them.
94 | #
95 | class Observer < ActiveModel::Observer
96 |
97 | protected
98 |
99 | def observed_classes
100 | klasses = super
101 | klasses + klasses.map { |klass| klass.descendants }.flatten
102 | end
103 |
104 | def add_observer!(klass)
105 | super
106 | define_callbacks klass
107 | end
108 |
109 | def define_callbacks(klass)
110 | observer = self
111 | observer_name = observer.class.name.underscore.gsub('/', '__')
112 |
113 | ActiveRecord::Callbacks::CALLBACKS.each do |callback|
114 | next unless respond_to?(callback)
115 | callback_meth = :"_notify_#{observer_name}_for_#{callback}"
116 | unless klass.respond_to?(callback_meth)
117 | klass.send(:define_method, callback_meth) do |&block|
118 | observer.update(callback, self, &block)
119 | end
120 | klass.send(callback, callback_meth)
121 | end
122 | end
123 | end
124 | end
125 | end
126 |
127 | if defined?(ActionController) and defined?(ActionController::Caching::Sweeping)
128 | require 'rails/observers/action_controller/caching/sweeper'
129 | end
130 |
--------------------------------------------------------------------------------
/lib/rails/observers/railtie.rb:
--------------------------------------------------------------------------------
1 | require 'rails/railtie'
2 |
3 | module Rails
4 | module Observers
5 | class Railtie < ::Rails::Railtie
6 | initializer "active_record.observer", :before => "active_record.set_configs" do |app|
7 | ActiveSupport.on_load(:active_record) do
8 | require "rails/observers/activerecord/active_record"
9 | observers = app.config.active_record.delete(:observers)
10 | self.observers = observers if observers
11 | end
12 | end
13 |
14 | initializer "action_controller.caching.sweepers" do
15 | ActiveSupport.on_load(:action_controller) do
16 | require "rails/observers/action_controller/caching"
17 | end
18 | end
19 |
20 | initializer "active_resource.observer" do |app|
21 | ActiveSupport.on_load(:active_resource) do
22 | require 'rails/observers/active_resource/observing'
23 |
24 | prepend ActiveResource::Observing
25 | end
26 | end
27 |
28 | config.after_initialize do |app|
29 | begin
30 | # Eager load `ActiveRecord::Base` to avoid circular references when
31 | # loading a constant for the first time.
32 | #
33 | # E.g. loading a `User` model that references `ActiveRecord::Base`
34 | # which calls `instantiate_observers` to instantiate a `UserObserver`
35 | # which eventually calls `observed_class` thus constantizing `"User"`,
36 | # the class we're loading. 💣💥
37 | require "active_record/base" if defined?(ActiveRecord)
38 | rescue LoadError
39 | end
40 |
41 | ActiveSupport.on_load(:active_record) do
42 | ActiveRecord::Base.instantiate_observers
43 |
44 | # Rails 5.1 forward-compat. AD::R is deprecated to AS::R in Rails 5.
45 | reloader = defined?(ActiveSupport::Reloader) ? ActiveSupport::Reloader : ActionDispatch::Reloader
46 | reloader.to_prepare do
47 | ActiveRecord::Base.instantiate_observers
48 | end
49 | end
50 |
51 | ActiveSupport.on_load(:active_resource) do
52 | self.instantiate_observers
53 |
54 | # Rails 5.1 forward-compat. AD::R is deprecated to AS::R in Rails 5.
55 | reloader = defined?(ActiveSupport::Reloader) ? ActiveSupport::Reloader : ActionDispatch::Reloader
56 | reloader.to_prepare do
57 | ActiveResource::Base.instantiate_observers
58 | end
59 | end
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/rails/observers/version.rb:
--------------------------------------------------------------------------------
1 | module Rails
2 | module Observers
3 | VERSION = "0.2.0"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/rails-observers.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | require File.expand_path('../lib/rails/observers/version', __FILE__)
3 |
4 | Gem::Specification.new do |s|
5 | s.name = "rails-observers"
6 | s.authors = ["Rafael Mendonça França", "Steve Klabnik"]
7 | s.email = ["rafaelmfranca@gmail.com", "steve@steveklabnik.com"]
8 | s.description = %q{Rails observer (removed from core in Rails 4.0)}
9 | s.summary = %q{ActiveModel::Observer, ActiveRecord::Observer and ActionController::Caching::Sweeper extracted from Rails.}
10 | s.homepage = "https://github.com/rails/rails-observers"
11 | s.version = Rails::Observers::VERSION
12 | s.license = 'MIT'
13 |
14 | s.files = Dir["LICENSE", "README.md", "lib/**/*"]
15 | s.require_paths = ["lib"]
16 |
17 | s.required_ruby_version = '>= 2.2.2'
18 |
19 | s.add_dependency 'activemodel', '>= 4.2'
20 |
21 | s.add_development_dependency 'minitest', '>= 5'
22 | s.add_development_dependency 'railties', '>= 4.2'
23 | s.add_development_dependency 'activerecord', '>= 4.2'
24 | s.add_development_dependency 'actionmailer', '>= 4.2'
25 | s.add_development_dependency 'actionpack', '>= 4.2'
26 | s.add_development_dependency 'activeresource', '>= 4.0'
27 | s.add_development_dependency 'sqlite3', '>= 1.3'
28 | end
29 |
--------------------------------------------------------------------------------
/test/active_resource_observer_test.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'active_resource'
3 | require 'rails/observers/active_resource/observing'
4 |
5 | ActiveResource::Base.prepend ActiveResource::Observing
6 |
7 | require 'active_support/core_ext/hash/conversions'
8 |
9 | class Person < ActiveResource::Base
10 | self.site = "http://37s.sunrise.i:3000"
11 | end
12 |
13 | class ActiveResourceObservingTest < ActiveSupport::TestCase
14 | cattr_accessor :history
15 |
16 | class PersonObserver < ActiveModel::Observer
17 | observe :person
18 |
19 | %w( after_create after_destroy after_save after_update
20 | before_create before_destroy before_save before_update).each do |method|
21 | define_method(method) { |*| log method }
22 | end
23 |
24 | private
25 | def log(method)
26 | (ActiveResourceObservingTest.history ||= []) << method.to_sym
27 | end
28 | end
29 |
30 | def setup
31 | @matz = { 'person' => { :id => 1, :name => 'Matz' } }.to_json
32 |
33 | ActiveResource::HttpMock.respond_to do |mock|
34 | mock.get "/people/1.json", {}, @matz
35 | mock.post "/people.json", {}, @matz, 201, 'Location' => '/people/1.json'
36 | mock.put "/people/1.json", {}, nil, 204
37 | mock.delete "/people/1.json", {}, nil, 200
38 | end
39 |
40 | PersonObserver.instance
41 | end
42 |
43 | def teardown
44 | self.history = nil
45 | end
46 |
47 | def test_create_fires_save_and_create_notifications
48 | Person.create(:name => 'Rick')
49 | assert_equal [:before_save, :before_create, :after_create, :after_save], self.history
50 | end
51 |
52 | def test_update_fires_save_and_update_notifications
53 | person = Person.find(1)
54 | person.save
55 | assert_equal [:before_save, :before_update, :after_update, :after_save], self.history
56 | end
57 |
58 | def test_destroy_fires_destroy_notifications
59 | person = Person.find(1)
60 | person.destroy
61 | assert_equal [:before_destroy, :after_destroy], self.history
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/test/configuration_test.rb:
--------------------------------------------------------------------------------
1 | require 'isolation/abstract_unit'
2 | require 'rails-observers'
3 |
4 | class ConfigurationTest < ActiveSupport::TestCase
5 | include ActiveSupport::Testing::Isolation
6 |
7 | def setup
8 | build_app
9 | boot_rails
10 | FileUtils.rm_rf("#{app_path}/config/environments")
11 | end
12 |
13 | def teardown
14 | teardown_app
15 | end
16 |
17 | test "config.active_record.observers" do
18 | add_to_config <<-RUBY
19 | config.active_record.observers = :foo_observer
20 | RUBY
21 |
22 | app_file 'app/models/foo.rb', <<-RUBY
23 | class Foo < ActiveRecord::Base
24 | end
25 | RUBY
26 |
27 | app_file 'app/models/foo_observer.rb', <<-RUBY
28 | class FooObserver < ActiveRecord::Observer
29 | end
30 | RUBY
31 |
32 | require "#{app_path}/config/environment"
33 |
34 | _ = ActiveRecord::Base
35 | assert defined?(FooObserver)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/console_test.rb:
--------------------------------------------------------------------------------
1 | require 'isolation/abstract_unit'
2 | require 'rails-observers'
3 |
4 | class ConsoleTest < ActiveSupport::TestCase
5 | include ActiveSupport::Testing::Isolation
6 |
7 | def setup
8 | build_app
9 | boot_rails
10 | end
11 |
12 | def teardown
13 | teardown_app
14 | end
15 |
16 | def load_environment
17 | require "#{rails_root}/config/environment"
18 | Rails.application.sandbox = false
19 | Rails.application.load_console
20 | end
21 |
22 | def test_active_record_does_not_panic_when_referencing_an_observed_constant
23 | add_to_config "config.active_record.observers = :user_observer"
24 |
25 | app_file "app/models/user.rb", <<-MODEL
26 | class User < ActiveRecord::Base
27 | end
28 | MODEL
29 |
30 | app_file "app/models/user_observer.rb", <<-MODEL
31 | class UserObserver < ActiveRecord::Observer
32 | end
33 | MODEL
34 |
35 | load_environment
36 | assert_nothing_raised { User }
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/fixtures/developers.yml:
--------------------------------------------------------------------------------
1 | david:
2 | id: 1
3 | name: David
4 | salary: 80000
5 |
--------------------------------------------------------------------------------
/test/fixtures/minimalistics.yml:
--------------------------------------------------------------------------------
1 | first:
2 | id: 1
3 |
--------------------------------------------------------------------------------
/test/fixtures/topics.yml:
--------------------------------------------------------------------------------
1 | first:
2 | id: 1
3 | title: The First Topic
4 | author_name: David
5 | author_email_address: david@loudthinking.com
6 | last_read: 2004-04-15
7 | content: Have a nice day
8 | approved: false
9 | replies_count: 1
10 |
11 | second:
12 | id: 2
13 | title: The Second Topic of the day
14 | author_name: Mary
15 | content: Have a nice day
16 | approved: true
17 | replies_count: 0
18 | parent_id: 1
19 | type: Reply
20 |
21 | third:
22 | id: 3
23 | title: The Third Topic of the day
24 | author_name: Carl
25 | content: I'm a troll
26 | approved: true
27 | replies_count: 1
28 |
29 | fourth:
30 | id: 4
31 | title: The Fourth Topic of the day
32 | author_name: Carl
33 | content: Why not?
34 | approved: true
35 | type: Reply
36 | parent_id: 3
37 |
--------------------------------------------------------------------------------
/test/generators/generators_test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 |
3 | require 'fileutils'
4 |
5 | require 'rails/all'
6 | require 'rails/generators'
7 | require 'rails/generators/test_case'
8 |
9 | ActiveSupport.test_order = :random if ActiveSupport.respond_to?(:test_order=)
10 |
11 | module TestApp
12 | class Application < Rails::Application
13 | end
14 | end
15 |
16 | # Call configure to load the settings from
17 | # Rails.application.config.generators to Rails::Generators
18 | Rails.application.load_generators
19 |
--------------------------------------------------------------------------------
/test/generators/namespaced_generators_test.rb:
--------------------------------------------------------------------------------
1 | require 'generators/generators_test_helper'
2 | require 'generators/rails/observer/observer_generator'
3 |
4 | class NamespacedObserverGeneratorTest < Rails::Generators::TestCase
5 | tests Rails::Generators::ObserverGenerator
6 | arguments %w(account)
7 | destination File.expand_path("../../tmp", __FILE__)
8 |
9 | def setup
10 | super
11 | prepare_destination
12 | Rails::Generators.namespace = TestApp
13 | end
14 |
15 | def teardown
16 | super
17 | Rails::Generators.namespace = nil
18 | end
19 |
20 | def test_invokes_default_orm
21 | run_generator
22 | assert_file "app/models/test_app/account_observer.rb", /module TestApp/, / class AccountObserver < ActiveRecord::Observer/
23 | end
24 |
25 | def test_invokes_default_orm_with_class_path
26 | run_generator ["admin/account"]
27 | assert_file "app/models/test_app/admin/account_observer.rb", /module TestApp/, / class Admin::AccountObserver < ActiveRecord::Observer/
28 | end
29 |
30 | def test_invokes_default_test_framework
31 | run_generator
32 | assert_file "test/unit/test_app/account_observer_test.rb", /module TestApp/, / class AccountObserverTest < ActiveSupport::TestCase/
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/test/generators/observer_generator_test.rb:
--------------------------------------------------------------------------------
1 | require 'generators/generators_test_helper'
2 | require 'generators/rails/observer/observer_generator'
3 |
4 | class ObserverGeneratorTest < Rails::Generators::TestCase
5 | tests Rails::Generators::ObserverGenerator
6 | destination File.expand_path("../../tmp", __FILE__)
7 | arguments %w(account)
8 |
9 | def setup
10 | super
11 | prepare_destination
12 | end
13 |
14 | def test_invokes_default_orm
15 | run_generator
16 | assert_file "app/models/account_observer.rb", /class AccountObserver < ActiveRecord::Observer/
17 | end
18 |
19 | def test_invokes_default_orm_with_class_path
20 | run_generator ["admin/account"]
21 | assert_file "app/models/admin/account_observer.rb", /class Admin::AccountObserver < ActiveRecord::Observer/
22 | end
23 |
24 | def test_invokes_default_test_framework
25 | run_generator
26 | assert_file "test/unit/account_observer_test.rb", /class AccountObserverTest < ActiveSupport::TestCase/
27 | end
28 |
29 | def test_logs_if_the_test_framework_cannot_be_found
30 | content = run_generator ["account", "--test-framework=rspec"]
31 | assert_match(/rspec \[not found\]/, content)
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'mocha/minitest'
3 | require 'active_record'
4 | require 'rails'
5 | require 'rails/observers/activerecord/active_record'
6 |
7 | FIXTURES_ROOT = File.expand_path(File.dirname(__FILE__)) + "/fixtures"
8 |
9 | class ActiveSupport::TestCase
10 | include ActiveRecord::TestFixtures
11 |
12 | self.test_order = :random if self.respond_to?(:test_order=)
13 | self.fixture_path = FIXTURES_ROOT
14 | self.use_instantiated_fixtures = false
15 |
16 | if respond_to?(:use_transactional_tests=)
17 | self.use_transactional_tests = true
18 | else
19 | self.use_transactional_fixtures = true
20 | end
21 | end
22 |
23 | if Rails.version.start_with?('4.2')
24 | ActiveRecord::Base.raise_in_transactional_callbacks = true
25 | end
26 |
27 | ActiveRecord::Base.configurations = { 'test' => { 'adapter' => 'sqlite3', 'database' => ':memory:' } }
28 | ActiveRecord::Base.establish_connection(:test)
29 |
30 | ActiveRecord::Schema.verbose = false
31 | ActiveRecord::Schema.define do
32 | create_table :topics do |t|
33 | t.string :title
34 | t.string :author_name
35 | t.string :author_email_address
36 | t.datetime :written_on
37 | t.time :bonus_time
38 | t.date :last_read
39 | t.text :content
40 | t.text :important
41 | t.boolean :approved, :default => true
42 | t.integer :replies_count, :default => 0
43 | t.integer :parent_id
44 | t.string :parent_title
45 | t.string :type
46 | t.string :group
47 | t.timestamps :null => false
48 | end
49 |
50 | create_table :comments do |t|
51 | t.string :title
52 | end
53 |
54 | create_table :minimalistics do |t|
55 | end
56 |
57 | create_table :developers do |t|
58 | t.string :name
59 | t.integer :salary
60 | end
61 | end
62 |
63 | class Topic < ActiveRecord::Base
64 | has_many :replies, dependent: :destroy, foreign_key: "parent_id"
65 | end
66 |
67 | class Reply < Topic
68 | belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true
69 | end
70 |
71 | class Comment < ActiveRecord::Base
72 | def self.lol
73 | "lol"
74 | end
75 | end
76 |
77 | class Developer < ActiveRecord::Base
78 | end
79 |
80 | class Minimalistic < ActiveRecord::Base
81 | end
82 |
--------------------------------------------------------------------------------
/test/isolation/abstract_unit.rb:
--------------------------------------------------------------------------------
1 | # Note:
2 | # It is important to keep this file as light as possible
3 | # the goal for tests that require this is to test booting up
4 | # rails from an empty state, so anything added here could
5 | # hide potential failures
6 | #
7 | # It is also good to know what is the bare minimum to get
8 | # Rails booted up.
9 | require 'fileutils'
10 |
11 | require 'bundler/setup'
12 | require 'minitest/autorun'
13 | require 'active_support/test_case'
14 |
15 | # These files do not require any others and are needed
16 | # to run the tests
17 | require "active_support/testing/isolation"
18 | require "active_support/core_ext/kernel/reporting"
19 | require 'tmpdir'
20 |
21 | module TestHelpers
22 | module Paths
23 | def app_template_path
24 | File.join Dir.tmpdir, 'app_template'
25 | end
26 |
27 | def tmp_path(*args)
28 | @tmp_path ||= File.realpath(Dir.mktmpdir)
29 | File.join(@tmp_path, *args)
30 | end
31 |
32 | def app_path(*args)
33 | tmp_path(*%w[app] + args)
34 | end
35 |
36 | def rails_root
37 | app_path
38 | end
39 | end
40 |
41 | module Generation
42 | # Build an application by invoking the generator and going through the whole stack.
43 | def build_app(options = {})
44 | @prev_rails_env = ENV['RAILS_ENV']
45 | ENV['RAILS_ENV'] = 'development'
46 |
47 | FileUtils.rm_rf(app_path)
48 | FileUtils.cp_r(app_template_path, app_path)
49 |
50 | # Delete the initializers unless requested
51 | unless options[:initializers]
52 | Dir["#{app_path}/config/initializers/*.rb"].each do |initializer|
53 | File.delete(initializer)
54 | end
55 | end
56 |
57 | gemfile_path = "#{app_path}/Gemfile"
58 | if options[:gemfile].blank? && File.exist?(gemfile_path)
59 | File.delete gemfile_path
60 | end
61 |
62 | routes = File.read("#{app_path}/config/routes.rb")
63 | if routes =~ /(\n\s*end\s*)\Z/
64 | File.open("#{app_path}/config/routes.rb", 'w') do |f|
65 | f.puts $` + "\nmatch ':controller(/:action(/:id))(.:format)', :via => :all\n" + $1
66 | end
67 | end
68 |
69 | add_to_config <<-RUBY
70 | config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
71 | config.session_store :cookie_store, :key => "_myapp_session"
72 | config.active_support.deprecation = :log
73 | config.action_controller.allow_forgery_protection = false
74 | config.eager_load = false
75 | RUBY
76 | end
77 |
78 | def teardown_app
79 | ENV['RAILS_ENV'] = @prev_rails_env if @prev_rails_env
80 | end
81 |
82 | def add_to_config(str)
83 | environment = File.read("#{app_path}/config/application.rb")
84 | if environment =~ /(\n\s*end\s*end\s*)\Z/
85 | File.open("#{app_path}/config/application.rb", 'w') do |f|
86 | f.puts $` + "\n#{str}\n" + $1
87 | end
88 | end
89 | end
90 |
91 | def app_file(path, contents)
92 | FileUtils.mkdir_p File.dirname("#{app_path}/#{path}")
93 | File.open("#{app_path}/#{path}", 'w') do |f|
94 | f.puts contents
95 | end
96 | end
97 |
98 | def boot_rails
99 | require 'rubygems' unless defined? Gem
100 | require 'bundler'
101 | Bundler.setup
102 | end
103 | end
104 | end
105 |
106 | class ActiveSupport::TestCase
107 | include TestHelpers::Paths
108 | include TestHelpers::Generation
109 | end
110 |
111 | Module.new do
112 | extend TestHelpers::Paths
113 |
114 | # Build a rails app
115 | FileUtils.rm_rf(app_template_path)
116 | FileUtils.mkdir(app_template_path)
117 |
118 | `rails new #{app_template_path} --skip-gemfile --skip-listen`
119 | end
120 |
--------------------------------------------------------------------------------
/test/lifecycle_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 |
3 | class SpecialDeveloper < Developer; end
4 |
5 | class DeveloperObserver < ActiveRecord::Observer
6 | def calls
7 | @calls ||= []
8 | end
9 |
10 | def before_save(developer)
11 | calls << developer
12 | end
13 | end
14 |
15 | class SalaryChecker < ActiveRecord::Observer
16 | observe :special_developer
17 | attr_accessor :last_saved
18 |
19 | def before_save(developer)
20 | should_abort = developer.salary <= 80000
21 | begin
22 | throw :abort if should_abort
23 | rescue
24 | return !should_abort
25 | end
26 | end
27 |
28 | module Implementation
29 | def after_save(developer)
30 | self.last_saved = developer
31 | end
32 | end
33 | include Implementation
34 | end
35 |
36 | class TopicaAuditor < ActiveRecord::Observer
37 | observe :topic
38 |
39 | attr_reader :topic
40 |
41 | def after_find(topic)
42 | @topic = topic
43 | end
44 | end
45 |
46 | class TopicObserver < ActiveRecord::Observer
47 | attr_reader :topic
48 |
49 | def after_find(topic)
50 | @topic = topic
51 | end
52 |
53 | # Create an after_save callback, so a notify_observer hook is created
54 | # on :topic.
55 | def after_save(nothing)
56 | end
57 | end
58 |
59 | class MinimalisticObserver < ActiveRecord::Observer
60 | attr_reader :minimalistic
61 |
62 | def after_find(minimalistic)
63 | @minimalistic = minimalistic
64 | end
65 | end
66 |
67 | class MultiObserver < ActiveRecord::Observer
68 | attr_reader :record
69 |
70 | def self.observed_class() [ Topic, Developer ] end
71 |
72 | cattr_reader :last_inherited
73 | @@last_inherited = nil
74 |
75 | def observed_class_inherited_with_testing(subclass)
76 | observed_class_inherited_without_testing(subclass)
77 | @@last_inherited = subclass
78 | end
79 |
80 | alias_method :observed_class_inherited_without_testing, :observed_class_inherited
81 | alias_method :observed_class_inherited, :observed_class_inherited_with_testing
82 |
83 | def after_find(record)
84 | @record = record
85 | end
86 | end
87 |
88 | class ValidatedComment < Comment
89 | attr_accessor :callers
90 |
91 | before_validation :record_callers
92 |
93 | after_validation do
94 | record_callers
95 | end
96 |
97 | def record_callers
98 | callers << self.class if callers
99 | end
100 | end
101 |
102 | class ValidatedCommentObserver < ActiveRecord::Observer
103 | attr_accessor :callers
104 |
105 | def after_validation(model)
106 | callers << self.class if callers
107 | end
108 | end
109 |
110 |
111 | class AroundTopic < Topic
112 | end
113 |
114 | class AroundTopicObserver < ActiveRecord::Observer
115 | observe :around_topic
116 | def topic_ids
117 | @topic_ids ||= []
118 | end
119 |
120 | def around_save(topic)
121 | topic_ids << topic.id
122 | yield(topic)
123 | topic_ids << topic.id
124 | end
125 | end
126 |
127 | class LifecycleTest < ActiveSupport::TestCase
128 | fixtures :topics, :developers, :minimalistics
129 |
130 | def test_before_destroy
131 | topic = Topic.find(1)
132 | assert_difference 'Topic.count', -(1 + topic.replies.size) do
133 | topic.destroy
134 | end
135 | end
136 |
137 | def test_auto_observer
138 | topic_observer = TopicaAuditor.instance
139 | assert_nil TopicaAuditor.observed_class
140 | assert_equal [Topic], TopicaAuditor.observed_classes.to_a
141 |
142 | topic = Topic.find(1)
143 | assert_equal topic.title, topic_observer.topic.title
144 | end
145 |
146 | def test_inferred_auto_observer
147 | topic_observer = TopicObserver.instance
148 | assert_equal Topic, TopicObserver.observed_class
149 |
150 | topic = Topic.find(1)
151 | assert_equal topic.title, topic_observer.topic.title
152 | end
153 |
154 | def test_observing_two_classes
155 | multi_observer = MultiObserver.instance
156 |
157 | topic = Topic.find(1)
158 | assert_equal topic.title, multi_observer.record.title
159 |
160 | developer = Developer.find(1)
161 | assert_equal developer.name, multi_observer.record.name
162 | end
163 |
164 | def test_observing_subclasses
165 | multi_observer = MultiObserver.instance
166 |
167 | developer = SpecialDeveloper.find(1)
168 | assert_equal developer.name, multi_observer.record.name
169 |
170 | klass = Class.new(Developer)
171 | assert_equal klass, multi_observer.last_inherited
172 |
173 | developer = klass.find(1)
174 | assert_equal developer.name, multi_observer.record.name
175 | end
176 |
177 | def test_after_find_can_be_observed_when_its_not_defined_on_the_model
178 | observer = MinimalisticObserver.instance
179 | assert_equal Minimalistic, MinimalisticObserver.observed_class
180 |
181 | minimalistic = Minimalistic.find(1)
182 | assert_equal minimalistic, observer.minimalistic
183 | end
184 |
185 | def test_after_find_can_be_observed_when_its_defined_on_the_model
186 | observer = TopicObserver.instance
187 | assert_equal Topic, TopicObserver.observed_class
188 |
189 | topic = Topic.find(1)
190 | assert_equal topic, observer.topic
191 | end
192 |
193 | def test_invalid_observer
194 | assert_raise(ArgumentError) { Topic.observers = Object.new; Topic.instantiate_observers }
195 | end
196 |
197 | test "model callbacks fire before observers are notified" do
198 | callers = []
199 |
200 | comment = ValidatedComment.new
201 | comment.callers = ValidatedCommentObserver.instance.callers = callers
202 |
203 | comment.valid?
204 | assert_equal [ValidatedComment, ValidatedComment, ValidatedCommentObserver], callers,
205 | "model callbacks did not fire before observers were notified"
206 | end
207 |
208 | test "able to save developer" do
209 | SalaryChecker.instance # activate
210 | developer = SpecialDeveloper.new :name => 'Roger', :salary => 100000
211 | assert developer.save, "developer with normal salary failed to save"
212 | end
213 |
214 | test "unable to save developer with low salary" do
215 | SalaryChecker.instance # activate
216 | developer = SpecialDeveloper.new :name => 'Rookie', :salary => 50000
217 | assert !developer.save, "allowed to save a developer with too low salary"
218 | end
219 |
220 | 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
221 | SalaryChecker.instance # activate
222 | developer = SpecialDeveloper.create! :name => 'Roger', :salary => 100000
223 | assert_equal developer, SalaryChecker.instance.last_saved
224 | end
225 |
226 | test "around filter from observer should accept block" do
227 | observer = AroundTopicObserver.instance
228 | topic = AroundTopic.new
229 | topic.save
230 | assert_nil observer.topic_ids.first
231 | assert_not_nil observer.topic_ids.last
232 | end
233 |
234 | test "able to disable observers" do
235 | observer = DeveloperObserver.instance # activate
236 | observer.calls.clear
237 |
238 | ActiveRecord::Base.observers.disable DeveloperObserver do
239 | Developer.create! :name => 'Ancestor', :salary => 100000
240 | SpecialDeveloper.create! :name => 'Descendent', :salary => 100000
241 | end
242 |
243 | assert_equal [], observer.calls
244 | end
245 |
246 | def test_observer_is_called_once
247 | observer = DeveloperObserver.instance # activate
248 | observer.calls.clear
249 |
250 | developer = Developer.create! :name => 'Ancestor', :salary => 100000
251 | special_developer = SpecialDeveloper.create! :name => 'Descendent', :salary => 100000
252 |
253 | assert_equal [developer, special_developer], observer.calls
254 | end
255 | end
256 |
--------------------------------------------------------------------------------
/test/models/observers.rb:
--------------------------------------------------------------------------------
1 | class ORM
2 | include ActiveModel::Observing
3 |
4 | def save
5 | notify_observers :before_save
6 | end
7 |
8 | class Observer < ActiveModel::Observer
9 | def before_save_invocations
10 | @before_save_invocations ||= []
11 | end
12 |
13 | def before_save(record)
14 | before_save_invocations << record
15 | end
16 | end
17 | end
18 |
19 | class Widget < ORM; end
20 | class Budget < ORM; end
21 | class WidgetObserver < ORM::Observer; end
22 | class BudgetObserver < ORM::Observer; end
23 | class AuditTrail < ORM::Observer
24 | observe :widget, :budget
25 | end
26 |
27 | ORM.instantiate_observers
28 |
--------------------------------------------------------------------------------
/test/observer_array_test.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'active_model'
3 | require 'rails/observers/active_model/active_model'
4 | require 'models/observers'
5 |
6 | class ObserverArrayTest < ActiveSupport::TestCase
7 | def teardown
8 | ORM.observers.enable :all
9 | Budget.observers.enable :all
10 | Widget.observers.enable :all
11 | end
12 |
13 | def assert_observer_notified(model_class, observer_class)
14 | observer_class.instance.before_save_invocations.clear
15 | model_instance = model_class.new
16 | model_instance.save
17 | assert_equal [model_instance], observer_class.instance.before_save_invocations
18 | end
19 |
20 | def assert_observer_not_notified(model_class, observer_class)
21 | observer_class.instance.before_save_invocations.clear
22 | model_instance = model_class.new
23 | model_instance.save
24 | assert_equal [], observer_class.instance.before_save_invocations
25 | end
26 |
27 | test "all observers are enabled by default" do
28 | assert_observer_notified Widget, WidgetObserver
29 | assert_observer_notified Budget, BudgetObserver
30 | assert_observer_notified Widget, AuditTrail
31 | assert_observer_notified Budget, AuditTrail
32 | end
33 |
34 | test "can disable individual observers using a class constant" do
35 | ORM.observers.disable WidgetObserver
36 |
37 | assert_observer_not_notified Widget, WidgetObserver
38 | assert_observer_notified Budget, BudgetObserver
39 | assert_observer_notified Widget, AuditTrail
40 | assert_observer_notified Budget, AuditTrail
41 | end
42 |
43 | test "can enable individual observers using a class constant" do
44 | ORM.observers.disable :all
45 | ORM.observers.enable AuditTrail
46 |
47 | assert_observer_not_notified Widget, WidgetObserver
48 | assert_observer_not_notified Budget, BudgetObserver
49 | assert_observer_notified Widget, AuditTrail
50 | assert_observer_notified Budget, AuditTrail
51 | end
52 |
53 | test "can disable individual observers using a symbol" do
54 | ORM.observers.disable :budget_observer
55 |
56 | assert_observer_notified Widget, WidgetObserver
57 | assert_observer_not_notified Budget, BudgetObserver
58 | assert_observer_notified Widget, AuditTrail
59 | assert_observer_notified Budget, AuditTrail
60 | end
61 |
62 | test "can enable individual observers using a symbol" do
63 | ORM.observers.disable :all
64 | ORM.observers.enable :audit_trail
65 |
66 | assert_observer_not_notified Widget, WidgetObserver
67 | assert_observer_not_notified Budget, BudgetObserver
68 | assert_observer_notified Widget, AuditTrail
69 | assert_observer_notified Budget, AuditTrail
70 | end
71 |
72 | test "can disable multiple observers at a time" do
73 | ORM.observers.disable :widget_observer, :budget_observer
74 |
75 | assert_observer_not_notified Widget, WidgetObserver
76 | assert_observer_not_notified Budget, BudgetObserver
77 | assert_observer_notified Widget, AuditTrail
78 | assert_observer_notified Budget, AuditTrail
79 | end
80 |
81 | test "can enable multiple observers at a time" do
82 | ORM.observers.disable :all
83 | ORM.observers.enable :widget_observer, :budget_observer
84 |
85 | assert_observer_notified Widget, WidgetObserver
86 | assert_observer_notified Budget, BudgetObserver
87 | assert_observer_not_notified Widget, AuditTrail
88 | assert_observer_not_notified Budget, AuditTrail
89 | end
90 |
91 | test "can disable all observers using :all" do
92 | ORM.observers.disable :all
93 |
94 | assert_observer_not_notified Widget, WidgetObserver
95 | assert_observer_not_notified Budget, BudgetObserver
96 | assert_observer_not_notified Widget, AuditTrail
97 | assert_observer_not_notified Budget, AuditTrail
98 | end
99 |
100 | test "can enable all observers using :all" do
101 | ORM.observers.disable :all
102 | ORM.observers.enable :all
103 |
104 | assert_observer_notified Widget, WidgetObserver
105 | assert_observer_notified Budget, BudgetObserver
106 | assert_observer_notified Widget, AuditTrail
107 | assert_observer_notified Budget, AuditTrail
108 | end
109 |
110 | test "can disable observers on individual models without affecting those observers on other models" do
111 | Widget.observers.disable :all
112 |
113 | assert_observer_not_notified Widget, WidgetObserver
114 | assert_observer_notified Budget, BudgetObserver
115 | assert_observer_not_notified Widget, AuditTrail
116 | assert_observer_notified Budget, AuditTrail
117 | end
118 |
119 | test "can enable observers on individual models without affecting those observers on other models" do
120 | ORM.observers.disable :all
121 | Budget.observers.enable AuditTrail
122 |
123 | assert_observer_not_notified Widget, WidgetObserver
124 | assert_observer_not_notified Budget, BudgetObserver
125 | assert_observer_not_notified Widget, AuditTrail
126 | assert_observer_notified Budget, AuditTrail
127 | end
128 |
129 | test "can disable observers for the duration of a block" do
130 | yielded = false
131 | ORM.observers.disable :budget_observer do
132 | yielded = true
133 | assert_observer_notified Widget, WidgetObserver
134 | assert_observer_not_notified Budget, BudgetObserver
135 | assert_observer_notified Widget, AuditTrail
136 | assert_observer_notified Budget, AuditTrail
137 | end
138 |
139 | assert yielded
140 | assert_observer_notified Widget, WidgetObserver
141 | assert_observer_notified Budget, BudgetObserver
142 | assert_observer_notified Widget, AuditTrail
143 | assert_observer_notified Budget, AuditTrail
144 | end
145 |
146 | test "can enable observers for the duration of a block" do
147 | yielded = false
148 | Widget.observers.disable :all
149 |
150 | Widget.observers.enable :all do
151 | yielded = true
152 | assert_observer_notified Widget, WidgetObserver
153 | assert_observer_notified Budget, BudgetObserver
154 | assert_observer_notified Widget, AuditTrail
155 | assert_observer_notified Budget, AuditTrail
156 | end
157 |
158 | assert yielded
159 | assert_observer_not_notified Widget, WidgetObserver
160 | assert_observer_notified Budget, BudgetObserver
161 | assert_observer_not_notified Widget, AuditTrail
162 | assert_observer_notified Budget, AuditTrail
163 | end
164 |
165 | test "raises an appropriate error when a developer accidentally enables or disables the wrong class (i.e. Widget instead of WidgetObserver)" do
166 | assert_raise ArgumentError do
167 | ORM.observers.enable :widget
168 | end
169 |
170 | assert_raise ArgumentError do
171 | ORM.observers.enable Widget
172 | end
173 |
174 | assert_raise ArgumentError do
175 | ORM.observers.disable :widget
176 | end
177 |
178 | assert_raise ArgumentError do
179 | ORM.observers.disable Widget
180 | end
181 | end
182 |
183 | test "allows #enable at the superclass level to override #disable at the subclass level when called last" do
184 | Widget.observers.disable :all
185 | ORM.observers.enable :all
186 |
187 | assert_observer_notified Widget, WidgetObserver
188 | assert_observer_notified Budget, BudgetObserver
189 | assert_observer_notified Widget, AuditTrail
190 | assert_observer_notified Budget, AuditTrail
191 | end
192 |
193 | test "allows #disable at the superclass level to override #enable at the subclass level when called last" do
194 | Budget.observers.enable :audit_trail
195 | ORM.observers.disable :audit_trail
196 |
197 | assert_observer_notified Widget, WidgetObserver
198 | assert_observer_notified Budget, BudgetObserver
199 | assert_observer_not_notified Widget, AuditTrail
200 | assert_observer_not_notified Budget, AuditTrail
201 | end
202 |
203 | test "can use the block form at different levels of the hierarchy" do
204 | yielded = false
205 | Widget.observers.disable :all
206 |
207 | ORM.observers.enable :all do
208 | yielded = true
209 | assert_observer_notified Widget, WidgetObserver
210 | assert_observer_notified Budget, BudgetObserver
211 | assert_observer_notified Widget, AuditTrail
212 | assert_observer_notified Budget, AuditTrail
213 | end
214 |
215 | assert yielded
216 | assert_observer_not_notified Widget, WidgetObserver
217 | assert_observer_notified Budget, BudgetObserver
218 | assert_observer_not_notified Widget, AuditTrail
219 | assert_observer_notified Budget, AuditTrail
220 | end
221 | end
222 |
223 |
--------------------------------------------------------------------------------
/test/observing_test.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'active_model'
3 | require 'rails/observers/active_model/active_model'
4 |
5 | class ObservedModel
6 | include ActiveModel::Observing
7 |
8 | class Observer
9 | end
10 | end
11 |
12 | class FooObserver < ActiveModel::Observer
13 | class << self
14 | public :new
15 | end
16 |
17 | attr_accessor :stub
18 |
19 | def on_spec(record, *args)
20 | stub.event_with(record, *args) if stub
21 | end
22 |
23 | def around_save(record)
24 | yield :in_around_save
25 | end
26 | end
27 |
28 | class Foo
29 | include ActiveModel::Observing
30 | end
31 |
32 | class ObservingTest < ActiveSupport::TestCase
33 | def setup
34 | ObservedModel.observers.clear
35 | FooObserver.singleton_class.instance_eval do
36 | alias_method :original_observed_classes, :observed_classes
37 | end
38 | end
39 |
40 | def teardown
41 | FooObserver.singleton_class.instance_eval do
42 | undef_method :observed_classes
43 | alias_method :observed_classes, :original_observed_classes
44 | end
45 | end
46 |
47 | test "initializes model with no cached observers" do
48 | assert ObservedModel.observers.empty?, "Not empty: #{ObservedModel.observers.inspect}"
49 | end
50 |
51 | test "stores cached observers in an array" do
52 | ObservedModel.observers << :foo
53 | assert ObservedModel.observers.include?(:foo), ":foo not in #{ObservedModel.observers.inspect}"
54 | end
55 |
56 | test "flattens array of assigned cached observers" do
57 | ObservedModel.observers = [[:foo], :bar]
58 | assert ObservedModel.observers.include?(:foo), ":foo not in #{ObservedModel.observers.inspect}"
59 | assert ObservedModel.observers.include?(:bar), ":bar not in #{ObservedModel.observers.inspect}"
60 | end
61 |
62 | test "uses an ObserverArray so observers can be disabled" do
63 | ObservedModel.observers = [:foo, :bar]
64 | assert ObservedModel.observers.is_a?(ActiveModel::ObserverArray)
65 | end
66 |
67 | test "instantiates observer names passed as strings" do
68 | ObservedModel.observers << 'foo_observer'
69 | FooObserver.expects(:instance)
70 | ObservedModel.instantiate_observers
71 | end
72 |
73 | test "instantiates observer names passed as symbols" do
74 | ObservedModel.observers << :foo_observer
75 | FooObserver.expects(:instance)
76 | ObservedModel.instantiate_observers
77 | end
78 |
79 | test "instantiates observer classes" do
80 | ObservedModel.observers << ObservedModel::Observer
81 | ObservedModel::Observer.expects(:instance)
82 | ObservedModel.instantiate_observers
83 | end
84 |
85 | test "raises an appropriate error when a developer accidentally adds the wrong class (i.e. Widget instead of WidgetObserver)" do
86 | assert_raise ArgumentError do
87 | ObservedModel.observers = ['string']
88 | ObservedModel.instantiate_observers
89 | end
90 | assert_raise ArgumentError do
91 | ObservedModel.observers = [:string]
92 | ObservedModel.instantiate_observers
93 | end
94 | assert_raise ArgumentError do
95 | ObservedModel.observers = [String]
96 | ObservedModel.instantiate_observers
97 | end
98 | end
99 |
100 | test "passes observers to subclasses" do
101 | FooObserver.instance
102 | bar = Class.new(Foo)
103 | assert_equal Foo.observers_count, bar.observers_count
104 | end
105 | end
106 |
107 | class ObserverTest < ActiveSupport::TestCase
108 | def setup
109 | ObservedModel.observers = :foo_observer
110 | FooObserver.singleton_class.instance_eval do
111 | alias_method :original_observed_classes, :observed_classes
112 | end
113 | end
114 |
115 | def teardown
116 | FooObserver.singleton_class.instance_eval do
117 | undef_method :observed_classes
118 | alias_method :observed_classes, :original_observed_classes
119 | end
120 | end
121 |
122 | test "guesses implicit observable model name" do
123 | assert_equal Foo, FooObserver.observed_class
124 | end
125 |
126 | test "tracks implicit observable models" do
127 | instance = FooObserver.new
128 | assert_equal [Foo], instance.observed_classes
129 | end
130 |
131 | test "tracks explicit observed model class" do
132 | FooObserver.observe ObservedModel
133 | instance = FooObserver.new
134 | assert_equal [ObservedModel], instance.observed_classes
135 | end
136 |
137 | test "tracks explicit observed model as string" do
138 | FooObserver.observe 'observed_model'
139 | instance = FooObserver.new
140 | assert_equal [ObservedModel], instance.observed_classes
141 | end
142 |
143 | test "tracks explicit observed model as symbol" do
144 | FooObserver.observe :observed_model
145 | instance = FooObserver.new
146 | assert_equal [ObservedModel], instance.observed_classes
147 | end
148 |
149 | test "calls existing observer event" do
150 | foo = Foo.new
151 | FooObserver.instance.stub = stub
152 | FooObserver.instance.stub.expects(:event_with).with(foo)
153 | Foo.notify_observers(:on_spec, foo)
154 | end
155 |
156 | test "calls existing observer event from the instance" do
157 | foo = Foo.new
158 | FooObserver.instance.stub = stub
159 | FooObserver.instance.stub.expects(:event_with).with(foo)
160 | foo.notify_observers(:on_spec)
161 | end
162 |
163 | test "passes extra arguments" do
164 | foo = Foo.new
165 | FooObserver.instance.stub = stub
166 | FooObserver.instance.stub.expects(:event_with).with(foo, :bar)
167 | Foo.send(:notify_observers, :on_spec, foo, :bar)
168 | end
169 |
170 | test "skips nonexistent observer event" do
171 | foo = Foo.new
172 | Foo.notify_observers(:whatever, foo)
173 | end
174 |
175 | test "update passes a block on to the observer" do
176 | yielded_value = nil
177 | FooObserver.instance.update(:around_save, Foo.new) do |val|
178 | yielded_value = val
179 | end
180 | assert_equal :in_around_save, yielded_value
181 | end
182 |
183 | test "observe redefines observed_classes class method" do
184 | class BarObserver < ActiveModel::Observer
185 | observe :foo
186 | end
187 |
188 | assert_equal [Foo], BarObserver.observed_classes
189 |
190 | BarObserver.observe(ObservedModel)
191 | assert_equal [ObservedModel], BarObserver.observed_classes
192 | end
193 | end
194 |
--------------------------------------------------------------------------------
/test/rake_test.rb:
--------------------------------------------------------------------------------
1 | require 'isolation/abstract_unit'
2 | require 'rails-observers'
3 |
4 | module ApplicationTests
5 | class RakeTest < ActiveSupport::TestCase
6 | include ActiveSupport::Testing::Isolation
7 |
8 | def setup
9 | build_app
10 | boot_rails
11 | FileUtils.rm_rf("#{app_path}/config/environments")
12 | end
13 |
14 | def teardown
15 | teardown_app
16 | end
17 |
18 | def test_load_activerecord_base_when_we_use_observers
19 | Dir.chdir(app_path) do
20 | `bundle exec rails g model user;
21 | bundle exec rake db:migrate;
22 | bundle exec rails g observer user;`
23 |
24 | add_to_config "config.active_record.observers = :user_observer"
25 |
26 | assert_equal "0", `bundle exec rails r "puts User.count"`.strip
27 |
28 | app_file "lib/tasks/count_user.rake", <<-RUBY
29 | namespace :user do
30 | task :count => :environment do
31 | puts User.count
32 | end
33 | end
34 | RUBY
35 |
36 | assert_equal "0", `bundle exec rake user:count`.strip
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/test/sweeper_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'action_controller'
3 | require 'rails/observers/activerecord/active_record'
4 | require 'rails/observers/action_controller/caching'
5 |
6 | SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
7 |
8 | class AppSweeper < ActionController::Caching::Sweeper; end
9 |
10 | class SweeperTestController < ActionController::Base
11 | include SharedTestRoutes.url_helpers
12 |
13 | cache_sweeper :app_sweeper
14 |
15 | def show
16 | render plain: 'hello world'
17 | end
18 |
19 | def error
20 | raise StandardError.new
21 | end
22 | end
23 |
24 | class SweeperTest < ActionController::TestCase
25 | def setup
26 | @routes = SharedTestRoutes
27 |
28 | @routes.draw do
29 | get 'sweeper_test/show'
30 | get 'sweeper_test/error'
31 | end
32 |
33 | super
34 | end
35 |
36 | def test_sweeper_should_not_ignore_no_method_error
37 | sweeper = ActionController::Caching::Sweeper.send(:new)
38 | assert_raise NoMethodError do
39 | sweeper.send_not_defined
40 | end
41 | end
42 |
43 | def test_sweeper_should_not_block_rendering
44 | response = test_process(SweeperTestController)
45 | assert_equal 'hello world', response.body
46 | end
47 |
48 | def test_sweeper_should_clean_up_if_exception_is_raised
49 | assert_raise StandardError do
50 | test_process(SweeperTestController, 'error')
51 | end
52 | assert_nil AppSweeper.instance.controller
53 | end
54 |
55 | def test_before_method_of_sweeper_should_always_return_true
56 | sweeper = ActionController::Caching::Sweeper.send(:new)
57 | assert sweeper.before(SweeperTestController.new)
58 | end
59 |
60 | def test_after_method_of_sweeper_should_always_return_nil
61 | sweeper = ActionController::Caching::Sweeper.send(:new)
62 | assert_nil sweeper.after(SweeperTestController.new)
63 | end
64 |
65 | def test_sweeper_should_not_ignore_unknown_method_calls
66 | sweeper = ActionController::Caching::Sweeper.send(:new)
67 | assert_raise NameError do
68 | sweeper.instance_eval do
69 | some_method_that_doesnt_exist
70 | end
71 | end
72 | end
73 |
74 | private
75 |
76 | def test_process(controller, action = "show")
77 | @controller = controller.is_a?(Class) ? controller.new : controller
78 | if ActionController::TestRequest.respond_to?(:create)
79 | if ActionController::TestRequest.method(:create).arity == 0
80 | @request = ActionController::TestRequest.create
81 | else
82 | @request = ActionController::TestRequest.create @controller.class
83 | end
84 | else
85 | @request = ActionController::TestRequest.new
86 | end
87 | if ActionController.constants.include?(:TestResponse)
88 | @response = ActionController::TestResponse.new
89 | else
90 | @response = ActionDispatch::TestResponse.new
91 | end
92 |
93 | process(action)
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/test/transaction_callbacks_test.rb:
--------------------------------------------------------------------------------
1 | require "helper"
2 |
3 | class TransactionCallbacksTest < ActiveSupport::TestCase
4 | if respond_to?(:use_transactional_tests=)
5 | self.use_transactional_tests = false
6 | else
7 | self.use_transactional_fixtures = false
8 | end
9 |
10 | fixtures :topics
11 |
12 | class TopicWithCallbacks < ActiveRecord::Base
13 | self.table_name = :topics
14 |
15 | after_commit{|record| record.send(:do_after_commit, nil)}
16 | after_commit(:on => :create){|record| record.send(:do_after_commit, :create)}
17 | after_commit(:on => :update){|record| record.send(:do_after_commit, :update)}
18 | after_commit(:on => :destroy){|record| record.send(:do_after_commit, :destroy)}
19 | after_rollback{|record| record.send(:do_after_rollback, nil)}
20 | after_rollback(:on => :create){|record| record.send(:do_after_rollback, :create)}
21 | after_rollback(:on => :update){|record| record.send(:do_after_rollback, :update)}
22 | after_rollback(:on => :destroy){|record| record.send(:do_after_rollback, :destroy)}
23 |
24 | def history
25 | @history ||= []
26 | end
27 |
28 | def after_commit_block(on = nil, &block)
29 | @after_commit ||= {}
30 | @after_commit[on] ||= []
31 | @after_commit[on] << block
32 | end
33 |
34 | def after_rollback_block(on = nil, &block)
35 | @after_rollback ||= {}
36 | @after_rollback[on] ||= []
37 | @after_rollback[on] << block
38 | end
39 |
40 | def do_after_commit(on)
41 | blocks = @after_commit[on] if defined?(@after_commit)
42 | blocks.each{|b| b.call(self)} if blocks
43 | end
44 |
45 | def do_after_rollback(on)
46 | blocks = @after_rollback[on] if defined?(@after_rollback)
47 | blocks.each{|b| b.call(self)} if blocks
48 | end
49 | end
50 |
51 | def setup
52 | @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id }
53 | end
54 |
55 | def test_call_after_commit_after_transaction_commits
56 | @first.after_commit_block{|r| r.history << :after_commit}
57 | @first.after_rollback_block{|r| r.history << :after_rollback}
58 |
59 | @first.save!
60 | assert_equal [:after_commit], @first.history
61 | end
62 |
63 | def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record
64 | @first.after_commit_block(:create){|r| r.history << :commit_on_create}
65 | @first.after_commit_block(:update){|r| r.history << :commit_on_update}
66 | @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
67 | @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
68 | @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
69 | @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
70 |
71 | @first.save!
72 | assert_equal [:commit_on_update], @first.history
73 | end
74 |
75 | def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record
76 | @first.after_commit_block(:create){|r| r.history << :commit_on_create}
77 | @first.after_commit_block(:update){|r| r.history << :commit_on_update}
78 | @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
79 | @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
80 | @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
81 | @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
82 |
83 | @first.destroy
84 | assert_equal [:commit_on_destroy], @first.history
85 | end
86 |
87 | def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record
88 | @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
89 | @new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
90 | @new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
91 | @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
92 | @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create}
93 | @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update}
94 | @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
95 |
96 | @new_record.save!
97 | assert_equal [:commit_on_create], @new_record.history
98 | end
99 |
100 | def test_call_after_rollback_after_transaction_rollsback
101 | @first.after_commit_block{|r| r.history << :after_commit}
102 | @first.after_rollback_block{|r| r.history << :after_rollback}
103 |
104 | Topic.transaction do
105 | @first.save!
106 | raise ActiveRecord::Rollback
107 | end
108 |
109 | assert_equal [:after_rollback], @first.history
110 | end
111 |
112 | def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record
113 | @first.after_commit_block(:create){|r| r.history << :commit_on_create}
114 | @first.after_commit_block(:update){|r| r.history << :commit_on_update}
115 | @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
116 | @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
117 | @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
118 | @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
119 |
120 | Topic.transaction do
121 | @first.save!
122 | raise ActiveRecord::Rollback
123 | end
124 |
125 | assert_equal [:rollback_on_update], @first.history
126 | end
127 |
128 | def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record
129 | @first.after_commit_block(:create){|r| r.history << :commit_on_create}
130 | @first.after_commit_block(:update){|r| r.history << :commit_on_update}
131 | @first.after_commit_block(:destroy){|r| r.history << :commit_on_update}
132 | @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
133 | @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
134 | @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
135 |
136 | Topic.transaction do
137 | @first.destroy
138 | raise ActiveRecord::Rollback
139 | end
140 |
141 | assert_equal [:rollback_on_destroy], @first.history
142 | end
143 |
144 | def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record
145 | @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
146 | @new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
147 | @new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
148 | @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
149 | @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create}
150 | @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update}
151 | @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
152 |
153 | Topic.transaction do
154 | @new_record.save!
155 | raise ActiveRecord::Rollback
156 | end
157 |
158 | assert_equal [:rollback_on_create], @new_record.history
159 | end
160 |
161 | def test_call_after_rollback_when_commit_fails
162 | TopicWithCallbacks.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
163 | begin
164 | TopicWithCallbacks.connection.class.class_eval do
165 | def commit_db_transaction; raise "boom!"; end
166 | end
167 |
168 | @first.after_commit_block{|r| r.history << :after_commit}
169 | @first.after_rollback_block{|r| r.history << :after_rollback}
170 |
171 | assert !@first.save rescue nil
172 | assert_equal [:after_rollback], @first.history
173 | ensure
174 | TopicWithCallbacks.connection.class.send(:remove_method, :commit_db_transaction)
175 | TopicWithCallbacks.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
176 | end
177 | end
178 |
179 | def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint
180 | def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
181 | def @first.commits(i=0); @commits ||= 0; @commits += i if i; end
182 | @first.after_rollback_block{|r| r.rollbacks(1)}
183 | @first.after_commit_block{|r| r.commits(1)}
184 |
185 | def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
186 | def @second.commits(i=0); @commits ||= 0; @commits += i if i; end
187 | @second.after_rollback_block{|r| r.rollbacks(1)}
188 | @second.after_commit_block{|r| r.commits(1)}
189 |
190 | Topic.transaction do
191 | @first.save!
192 | Topic.transaction(:requires_new => true) do
193 | @second.save!
194 | raise ActiveRecord::Rollback
195 | end
196 | end
197 |
198 | assert_equal 1, @first.commits
199 | assert_equal 0, @first.rollbacks
200 | assert_equal 0, @second.commits
201 | assert_equal 1, @second.rollbacks
202 | end
203 |
204 | def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails
205 | def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
206 | def @first.commits(i=0); @commits ||= 0; @commits += i if i; end
207 |
208 | @first.after_rollback_block{|r| r.rollbacks(1)}
209 | @first.after_commit_block{|r| r.commits(1)}
210 |
211 | Topic.transaction do
212 | @first.save
213 | Topic.transaction(:requires_new => true) do
214 | @first.save!
215 | raise ActiveRecord::Rollback
216 | end
217 | Topic.transaction(:requires_new => true) do
218 | @first.save!
219 | raise ActiveRecord::Rollback
220 | end
221 | end
222 |
223 | assert_equal 1, @first.commits
224 | assert_equal 2, @first.rollbacks
225 | end
226 |
227 | def test_after_transaction_callbacks_should_prevent_callbacks_from_being_called
228 | def @first.last_after_transaction_error=(e); @last_transaction_error = e; end
229 | def @first.last_after_transaction_error; @last_transaction_error; end
230 | @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";}
231 | @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";}
232 | @second.after_commit_block{|r| r.history << :after_commit}
233 | @second.after_rollback_block{|r| r.history << :after_rollback}
234 |
235 | assert_raises RuntimeError do
236 | Topic.transaction do
237 | @first.save!
238 | @second.save!
239 | end
240 | end
241 | assert_equal :commit, @first.last_after_transaction_error
242 | assert_equal [:after_commit], @second.history
243 |
244 | @second.history.clear
245 |
246 | assert_raises RuntimeError do
247 | Topic.transaction do
248 | @first.save!
249 | @second.save!
250 | raise ActiveRecord::Rollback
251 | end
252 | end
253 | assert_equal :rollback, @first.last_after_transaction_error
254 | assert_equal [], @second.history
255 | end
256 | end
257 |
--------------------------------------------------------------------------------