├── .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 | [![Build Status](https://secure.travis-ci.org/rails/rails-observers.png)](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 | --------------------------------------------------------------------------------