Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' of github.com:rails/rails
Browse files Browse the repository at this point in the history
wycats committed Jun 8, 2010
2 parents 6ebc7c8 + 5c9f27a commit df40dbe
Showing 24 changed files with 667 additions and 114 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ if mri || RUBY_ENGINE == "rbx"
gem "sqlite3-ruby", "~> 1.3.0", :require => 'sqlite3'

group :db do
# gem "pg", ">= 0.9.0"
gem "pg", ">= 0.9.0"
gem "mysql", ">= 2.8.1"
end
elsif RUBY_ENGINE == "jruby"
10 changes: 0 additions & 10 deletions actionpack/CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
*Rails 3.0.0 [beta 4] (June 8th, 2010)*

* Add shallow routes back to the new router [Diego Carrion]

resources :posts do
shallow do
resources :comments
end
end

You can now use comment_path for /comments/1 instead of post_comment_path for /posts/1/comments/1.

* Remove middleware laziness [José Valim]

* Make session stores rely on request.cookie_jar and change set_session semantics to return the cookie value instead of a boolean. [José Valim]
1 change: 1 addition & 0 deletions actionpack/lib/action_controller/caching/sweeping.rb
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@ class Sweeper < ActiveRecord::Observer #:nodoc:
def before(controller)
self.controller = controller
callback(:before) if controller.perform_caching
true # before method from sweeper should always return true
end

def after(controller)
37 changes: 11 additions & 26 deletions actionpack/lib/action_dispatch/routing/mapper.rb
Original file line number Diff line number Diff line change
@@ -350,10 +350,6 @@ def constraints(constraints = {})
scope(:constraints => constraints) { yield }
end

def shallow
scope(:shallow => true) { yield }
end

def defaults(defaults = {})
scope(:defaults => defaults) { yield }
end
@@ -378,21 +374,12 @@ def scope_options
@scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
end

def merge_shallow_scope(parent, child)
parent or child
end

def merge_path_scope(parent, child)
parent_path = (@scope[:shallow] and child.eql?(':id')) ? parent.split('/').last : parent
Mapper.normalize_path "#{parent_path}/#{child}"
Mapper.normalize_path("#{parent}/#{child}")
end

def merge_name_prefix_scope(parent, child)
if @scope[:shallow]
child
else
parent ? "#{parent}_#{child}" : child
end
parent ? "#{parent}_#{child}" : child
end

def merge_module_scope(parent, child)
@@ -535,10 +522,6 @@ def nested_options
options["#{singular}_id".to_sym] = id_constraint if id_constraint?
options
end

def shallow?
options[:shallow]
end
end

class SingletonResource < Resource #:nodoc:
@@ -620,12 +603,8 @@ def resources(*resources, &block)

resource = Resource.new(resources.pop, options)

scope(:path => resource.path, :controller => resource.controller, :shallow => resource.shallow?) do
scope(:path => resource.path, :controller => resource.controller) do
with_scope_level(:resources, resource) do
if @scope[:shallow] && @scope[:name_prefix]
@scope[:path] = "/#{@scope[:name_prefix].pluralize}/:#{@scope[:name_prefix]}_id/#{resource.path}"
end

yield if block_given?

with_scope_level(:collection) do
@@ -639,8 +618,6 @@ def resources(*resources, &block)
with_scope_level(:member) do
scope(':id') do
scope(resource.options) do
@scope[:name_prefix] = nil if @scope[:shallow]

get :show if resource.actions.include?(:show)
put :update if resource.actions.include?(:update)
delete :destroy if resource.actions.include?(:destroy)
@@ -702,6 +679,14 @@ def nested
end
end

def namespace(path)
if resource_scope?
nested { super }
else
super
end
end

def match(*args)
options = args.extract_options!

14 changes: 8 additions & 6 deletions actionpack/lib/action_view/test_case.rb
Original file line number Diff line number Diff line change
@@ -131,12 +131,14 @@ def make_test_case_available_to_view!
end

def _view
view = ActionView::Base.new(ActionController::Base.view_paths, _assigns, @controller)
view.singleton_class.send :include, _helpers
view.singleton_class.send :include, @controller._router.url_helpers
view.singleton_class.send :delegate, :alert, :notice, :to => "request.flash"
view.output_buffer = self.output_buffer
view
@_view ||= begin
view = ActionView::Base.new(ActionController::Base.view_paths, _assigns, @controller)
view.singleton_class.send :include, _helpers
view.singleton_class.send :include, @controller._router.url_helpers
view.singleton_class.send :delegate, :alert, :notice, :to => "request.flash"
view.output_buffer = self.output_buffer
view
end
end

EXCLUDE_IVARS = %w{
3 changes: 3 additions & 0 deletions actionpack/test/abstract_unit.rb
Original file line number Diff line number Diff line change
@@ -24,6 +24,9 @@
require 'action_dispatch'
require 'active_support/dependencies'
require 'active_model'
require 'active_record'
require 'action_controller/caching'
require 'action_controller/caching/sweeping'

begin
require 'ruby-debug'
6 changes: 6 additions & 0 deletions actionpack/test/controller/filters_test.rb
Original file line number Diff line number Diff line change
@@ -445,6 +445,12 @@ def filter_three

end


def test_before_method_of_sweeper_should_always_return_true
sweeper = ActionController::Caching::Sweeper.send(:new)
assert sweeper.before(TestController.new)
end

def test_non_yielding_around_filters_not_returning_false_do_not_raise
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", true
61 changes: 22 additions & 39 deletions actionpack/test/dispatch/routing_test.rb
Original file line number Diff line number Diff line change
@@ -34,33 +34,6 @@ def self.matches?(request)
end
end

resources :users do
shallow do
resources :photos do
resources :types do
member do
post :preview
end
collection do
delete :erase
end
end
end
end
end

shallow do
resources :teams do
resources :players
end

resources :countries do
resources :cities do
resources :places
end
end
end

match 'account/logout' => redirect("/logout"), :as => :logout_redirect
match 'account/login', :to => redirect("/login")

@@ -171,6 +144,16 @@ def self.matches?(request)

resources :sheep

resources :clients do
namespace :google do
resource :account do
namespace :secret do
resource :info
end
end
end
end

match 'sprockets.js' => ::TestRoutingMapper::SprocketsApp

match 'people/:id/update', :to => 'people#update', :as => :update_person
@@ -779,18 +762,6 @@ def test_update_person_route
end
end

def test_shallow_routes
with_test_routes do
assert_equal '/photos/4', photo_path(4)
assert_equal '/types/10/edit', edit_type_path(10)
assert_equal '/types/5/preview', preview_type_path(5)
assert_equal '/photos/2/types', photo_types_path(2)
assert_equal '/cities/1/places', url_for(:controller => :places, :action => :index, :city_id => 1, :only_path => true)
assert_equal '/teams/new', url_for(:controller => :teams, :action => :new, :only_path => true)
assert_equal '/photos/11/types/erase', url_for(:controller => :types, :action => :erase, :photo_id => 11, :only_path => true)
end
end

def test_update_project_person
with_test_routes do
get '/projects/1/people/2/update'
@@ -852,6 +823,18 @@ def test_nested_namespace
assert_equal '/account/admin/subscription', account_admin_subscription_path
end
end

def test_namespace_nested_in_resources
with_test_routes do
get '/clients/1/google/account'
assert_equal '/clients/1/google/account', client_google_account_path(1)
assert_equal 'google/accounts#show', @response.body

get '/clients/1/google/account/secret/info'
assert_equal '/clients/1/google/account/secret/info', client_google_account_secret_info_path(1)
assert_equal 'google/secret/infos#show', @response.body
end
end

def test_articles_with_id
with_test_routes do
4 changes: 4 additions & 0 deletions actionpack/test/template/test_case_test.rb
Original file line number Diff line number Diff line change
@@ -37,6 +37,10 @@ class GeneralViewTest < ActionView::TestCase
include SharedTests
test_case = self

test "memoizes the _view" do
assert_same _view, _view
end

test "works without testing a helper module" do
assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy'))
end
4 changes: 4 additions & 0 deletions activerecord/CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
*Rails 3.0.0 [beta 4] (June 8th, 2010)*

* Fixed that ActiveRecord::Base.compute_type would swallow NoMethodError #4751 [Andrew Bloomgarden, Andrew White]

* Add index length support for MySQL. #1852 [Emili Parreno, Pratik Naik]

Example:
@@ -12,6 +14,8 @@

* find_or_create_by_attr(value, ...) works when attr is protected. #4457 [Santiago Pastorino, Marc-André Lafortune]

* New callbacks: after_commit and after_rollback. Do expensive operations like image thumbnailing after_commit instead of after_save. #2991 [Brian Durand]

* Serialized attributes are not converted to YAML if they are any of the formats that can be serialized to XML (like Hash, Array and Strings). [José Valim]

* Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 [Curtis Hawthorne]
4 changes: 3 additions & 1 deletion activerecord/lib/active_record/base.rb
Original file line number Diff line number Diff line change
@@ -1219,7 +1219,9 @@ def compute_type(type_name)
begin
constant = candidate.constantize
return constant if candidate == constant.to_s
rescue NameError
rescue NameError => e
# We don't want to swallow NoMethodError < NameError errors
raise e unless e.instance_of?(NameError)
rescue ArgumentError
end
end
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/callbacks.rb
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ module ActiveRecord
# class CreditCard < ActiveRecord::Base
# # Strip everything but digits, so the user can specify "555 234 34" or
# # "5552-3434" or both will mean "55523434"
# def before_validation_on_create
# before_validation(:on => :create) do
# self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
# end
# end
Original file line number Diff line number Diff line change
@@ -122,6 +122,8 @@ def transaction(options = {})
requires_new = options[:requires_new] || !last_transaction_joinable

transaction_open = false
@_current_transaction_records ||= []

begin
if block_given?
if requires_new || open_transactions == 0
@@ -132,6 +134,7 @@ def transaction(options = {})
end
increment_open_transactions
transaction_open = true
@_current_transaction_records.push([])
end
yield
end
@@ -141,8 +144,10 @@ def transaction(options = {})
decrement_open_transactions
if open_transactions == 0
rollback_db_transaction
rollback_transaction_records(true)
else
rollback_to_savepoint
rollback_transaction_records(false)
end
end
raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
@@ -157,20 +162,35 @@ def transaction(options = {})
begin
if open_transactions == 0
commit_db_transaction
commit_transaction_records
else
release_savepoint
save_point_records = @_current_transaction_records.pop
unless save_point_records.blank?
@_current_transaction_records.push([]) if @_current_transaction_records.empty?
@_current_transaction_records.last.concat(save_point_records)
end
end
rescue Exception => database_transaction_rollback
if open_transactions == 0
rollback_db_transaction
rollback_transaction_records(true)
else
rollback_to_savepoint
rollback_transaction_records(false)
end
raise
end
end
end

# Register a record with the current transaction so that its after_commit and after_rollback callbacks
# can be called.
def add_transaction_record(record)
last_batch = @_current_transaction_records.last
last_batch << record if last_batch
end

# Begins the transaction (and turns off auto-committing).
def begin_db_transaction() end

@@ -268,6 +288,42 @@ def sanitize_limit(limit)
limit.to_i
end
end

# Send a rollback message to all records after they have been rolled back. If rollback
# is false, only rollback records since the last save point.
def rollback_transaction_records(rollback) #:nodoc
if rollback
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
else
records = @_current_transaction_records.pop
end

unless records.blank?
records.uniq.each do |record|
begin
record.rolledback!(rollback)
rescue Exception => e
record.logger.error(e) if record.respond_to?(:logger)
end
end
end
end

# Send a commit message to all records after they have been committed.
def commit_transaction_records #:nodoc
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
unless records.blank?
records.uniq.each do |record|
begin
record.committed!
rescue Exception => e
record.logger.error(e) if record.respond_to?(:logger)
end
end
end
end
end
end
end
12 changes: 5 additions & 7 deletions activerecord/lib/active_record/fixtures.rb
Original file line number Diff line number Diff line change
@@ -787,16 +787,14 @@ def to_hash
end

def key_list
columns = @fixture.keys.collect{ |column_name| @connection.quote_column_name(column_name) }
columns.join(", ")
@fixture.keys.map { |column_name| @connection.quote_column_name(column_name) }.join(', ')
end

def value_list
list = @fixture.inject([]) do |fixtures, (key, value)|
col = model_class.columns_hash[key] if model_class.respond_to?(:ancestors) && model_class.ancestors.include?(ActiveRecord::Base)
fixtures << @connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
end
list * ', '
cols = (model_class && model_class < ActiveRecord::Base) ? model_class.columns_hash : {}
@fixture.map do |key, value|
@connection.quote(value, cols[key]).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
end.join(', ')
end

def find
137 changes: 126 additions & 11 deletions activerecord/lib/active_record/transactions.rb
Original file line number Diff line number Diff line change
@@ -8,6 +8,10 @@ module Transactions
class TransactionError < ActiveRecordError # :nodoc:
end

included do
define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
end

# Transactions are protective blocks where SQL statements are only permanent
# if they can all succeed as one atomic action. The classic example is a
# transfer between two accounts where you can only have a deposit if the
@@ -72,7 +76,7 @@ class TransactionError < ActiveRecordError # :nodoc:
#
# Both +save+ and +destroy+ come wrapped in a transaction that ensures
# that whatever you do in validations or callbacks will happen under its
# protected cover. So you can use validations to check for values that
# protected cover. So you can use validations to check for values that
# the transaction depends on or you can raise exceptions in the callbacks
# to rollback, including <tt>after_*</tt> callbacks.
#
@@ -158,6 +162,21 @@ class TransactionError < ActiveRecordError # :nodoc:
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
# for more information about savepoints.
#
# === Callbacks
#
# There are two types of callbacks associated with committing and rolling back transactions:
# +after_commit+ and +after_rollback+.
#
# +after_commit+ callbacks are called on every record saved or destroyed within a
# transaction immediately after the transaction is committed. +after_rollback+ callbacks
# are called on every record saved or destroyed within a transaction immediately after the
# transaction or savepoint is rolled back.
#
# These callbacks are useful for interacting with other systems since you will be guaranteed
# that the callback is only executed when the database is in a permanent state. For example,
# +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from
# within a transaction could trigger the cache to be regenerated before the database is updated.
#
# === Caveats
#
# If you're on MySQL, then do not use DDL operations in nested transactions
@@ -182,6 +201,24 @@ def transaction(options = {}, &block)
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
connection.transaction(options, &block)
end

def after_commit(*args, &block)
options = args.last
if options.is_a?(Hash) && options[:on]
options[:if] = Array.wrap(options[:if])
options[:if] << "transaction_include_action?(:#{options[:on]})"
end
set_callback(:commit, :after, *args, &block)
end

def after_rollback(*args, &block)
options = args.last
if options.is_a?(Hash) && options[:on]
options[:if] = Array.wrap(options[:if])
options[:if] << "transaction_include_action?(:#{options[:on]})"
end
set_callback(:rollback, :after, *args, &block)
end
end

# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
@@ -205,19 +242,36 @@ def save!(*) #:nodoc:

# Reset id and @new_record if the transaction rolls back.
def rollback_active_record_state!
id_present = has_attribute?(self.class.primary_key)
previous_id = id
previous_new_record = new_record?
remember_transaction_record_state
yield
rescue Exception
@new_record = previous_new_record
if id_present
self.id = previous_id
else
@attributes.delete(self.class.primary_key)
@attributes_cache.delete(self.class.primary_key)
end
restore_transaction_record_state
raise
ensure
clear_transaction_record_state
end

# Call the after_commit callbacks
def committed! #:nodoc:
_run_commit_callbacks
ensure
clear_transaction_record_state
end

# Call the after rollback callbacks. The restore_state argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
def rolledback!(force_restore_state = false) #:nodoc:
_run_rollback_callbacks
ensure
restore_transaction_record_state(force_restore_state)
end

# Add the record to the current transaction so that the :after_rollback and :after_commit callbacks
# can be called.
def add_to_transaction
if self.class.connection.add_transaction_record(self)
remember_transaction_record_state
end
end

# Executes +method+ within a transaction and captures its return value as a
@@ -229,10 +283,71 @@ def rollback_active_record_state!
def with_transaction_returning_status
status = nil
self.class.transaction do
add_to_transaction
status = yield
raise ActiveRecord::Rollback unless status
end
status
end

protected

# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state #:nodoc
@_start_transaction_state ||= {}
unless @_start_transaction_state.include?(:new_record)
@_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
@_start_transaction_state[:new_record] = @new_record
end
unless @_start_transaction_state.include?(:destroyed)
@_start_transaction_state[:destroyed] = @destroyed
end
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
end

# Clear the new record state and id of a record.
def clear_transaction_record_state #:nodoc
if defined?(@_start_transaction_state)
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1
end
end

# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
def restore_transaction_record_state(force = false) #:nodoc
if defined?(@_start_transaction_state)
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
if @_start_transaction_state[:level] < 1
restore_state = remove_instance_variable(:@_start_transaction_state)
if restore_state
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
if restore_state[:id]
self.id = restore_state[:id]
else
@attributes.delete(self.class.primary_key)
@attributes_cache.delete(self.class.primary_key)
end
end
end
end
end

# Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
def transaction_record_state(state) #:nodoc
@_start_transaction_state[state] if defined?(@_start_transaction_state)
end

# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
def transaction_include_action?(action) #:nodoc
case action
when :create
transaction_record_state(:new_record)
when :destroy
destroyed?
when :update
!(transaction_record_state(:new_record) || destroyed?)
end
end
end
end
8 changes: 5 additions & 3 deletions activerecord/test/cases/active_schema_test_mysql.rb
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
def setup
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
alias_method :execute_without_stub, :execute
remove_method :execute
def execute(sql, name = nil) return sql end
end
end
@@ -66,7 +67,7 @@ def test_drop_table_with_specific_database
assert_equal "DROP TABLE `otherdb`.`people`", drop_table('otherdb.people')
end

def test_add_timestamps
def test_add_timestamps
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me do |t|
@@ -79,8 +80,8 @@ def test_add_timestamps
end
end
end
def test_remove_timestamps

def test_remove_timestamps
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me do |t|
@@ -106,6 +107,7 @@ def with_real_execute
ensure
#before finishing, we restore the alias to the mock-up method
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
remove_method :execute
alias_method :execute, :execute_with_stub
end
end
1 change: 1 addition & 0 deletions activerecord/test/cases/active_schema_test_postgresql.rb
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ class PostgresqlActiveSchemaTest < Test::Unit::TestCase
def setup
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
alias_method :real_execute, :execute
remove_method :execute
def execute(sql, name = nil) sql end
end
end
8 changes: 4 additions & 4 deletions activerecord/test/cases/adapter_test.rb
Original file line number Diff line number Diff line change
@@ -145,13 +145,13 @@ def test_foreign_key_violations_are_translated_to_specific_exception

def test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas
sql_inject = "1 select * from schema"
assert_no_match /schema/, @connection.add_limit_offset!("", :limit=>sql_inject)
assert_no_match /schema/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)
assert_no_match(/schema/, @connection.add_limit_offset!("", :limit=>sql_inject))
assert_no_match(/schema/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7))
end

def test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas
sql_inject = "1, 7 procedure help()"
assert_no_match /procedure/, @connection.add_limit_offset!("", :limit=>sql_inject)
assert_no_match /procedure/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)
assert_no_match(/procedure/, @connection.add_limit_offset!("", :limit=>sql_inject))
assert_no_match(/procedure/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7))
end
end
17 changes: 17 additions & 0 deletions activerecord/test/cases/base_test.rb
Original file line number Diff line number Diff line change
@@ -2334,6 +2334,23 @@ def test_dup
assert !Minimalistic.new.freeze.dup.frozen?
end

def test_compute_type_success
assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author')
end

def test_compute_type_nonexistent_constant
assert_raises NameError do
ActiveRecord::Base.send :compute_type, 'NonexistentModel'
end
end

def test_compute_type_no_method_error
String.any_instance.stubs(:constantize).raises(NoMethodError)
assert_raises NoMethodError do
ActiveRecord::Base.send :compute_type, 'InvalidModel'
end
end

protected
def with_env_tz(new_tz = 'US/Eastern')
old_tz, ENV['TZ'] = ENV['TZ'], new_tz
240 changes: 240 additions & 0 deletions activerecord/test/cases/transaction_callbacks_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
require "cases/helper"
require 'models/topic'
require 'models/reply'

class TransactionCallbacksTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
fixtures :topics

class TopicWithCallbacks < ActiveRecord::Base
set_table_name :topics

after_commit{|record| record.send(:do_after_commit, nil)}
after_commit(:on => :create){|record| record.send(:do_after_commit, :create)}
after_commit(:on => :update){|record| record.send(:do_after_commit, :update)}
after_commit(:on => :destroy){|record| record.send(:do_after_commit, :destroy)}
after_rollback{|record| record.send(:do_after_rollback, nil)}
after_rollback(:on => :create){|record| record.send(:do_after_rollback, :create)}
after_rollback(:on => :update){|record| record.send(:do_after_rollback, :update)}
after_rollback(:on => :destroy){|record| record.send(:do_after_rollback, :destroy)}

def history
@history ||= []
end

def after_commit_block(on = nil, &block)
@after_commit ||= {}
@after_commit[on] ||= []
@after_commit[on] << block
end

def after_rollback_block(on = nil, &block)
@after_rollback ||= {}
@after_rollback[on] ||= []
@after_rollback[on] << block
end

def do_after_commit(on)
blocks = @after_commit[on] if defined?(@after_commit)
blocks.each{|b| b.call(self)} if blocks
end

def do_after_rollback(on)
blocks = @after_rollback[on] if defined?(@after_rollback)
blocks.each{|b| b.call(self)} if blocks
end
end

def setup
@first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id }
end

def test_call_after_commit_after_transaction_commits
@first.after_commit_block{|r| r.history << :after_commit}
@first.after_rollback_block{|r| r.history << :after_rollback}

@first.save!
assert_equal [:after_commit], @first.history
end

def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record
@first.after_commit_block(:create){|r| r.history << :commit_on_create}
@first.after_commit_block(:update){|r| r.history << :commit_on_update}
@first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
@first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
@first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
@first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}

@first.save!
assert_equal [:commit_on_update], @first.history
end

def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record
@first.after_commit_block(:create){|r| r.history << :commit_on_create}
@first.after_commit_block(:update){|r| r.history << :commit_on_update}
@first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
@first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
@first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
@first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}

@first.destroy
assert_equal [:commit_on_destroy], @first.history
end

def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record
@new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
@new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
@new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
@new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
@new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create}
@new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update}
@new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}

@new_record.save!
assert_equal [:commit_on_create], @new_record.history
end

def test_call_after_rollback_after_transaction_rollsback
@first.after_commit_block{|r| r.history << :after_commit}
@first.after_rollback_block{|r| r.history << :after_rollback}

Topic.transaction do
@first.save!
raise ActiveRecord::Rollback
end

assert_equal [:after_rollback], @first.history
end

def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record
@first.after_commit_block(:create){|r| r.history << :commit_on_create}
@first.after_commit_block(:update){|r| r.history << :commit_on_update}
@first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
@first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
@first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
@first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}

Topic.transaction do
@first.save!
raise ActiveRecord::Rollback
end

assert_equal [:rollback_on_update], @first.history
end

def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record
@first.after_commit_block(:create){|r| r.history << :commit_on_create}
@first.after_commit_block(:update){|r| r.history << :commit_on_update}
@first.after_commit_block(:destroy){|r| r.history << :commit_on_update}
@first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
@first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
@first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}

Topic.transaction do
@first.destroy
raise ActiveRecord::Rollback
end

assert_equal [:rollback_on_destroy], @first.history
end

def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record
@new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
@new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
@new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
@new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
@new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create}
@new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update}
@new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}

Topic.transaction do
@new_record.save!
raise ActiveRecord::Rollback
end

assert_equal [:rollback_on_create], @new_record.history
end

def test_call_after_rollback_when_commit_fails
@first.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
begin
@first.connection.class.class_eval do
def commit_db_transaction; raise "boom!"; end
end

@first.after_commit_block{|r| r.history << :after_commit}
@first.after_rollback_block{|r| r.history << :after_rollback}

assert !@first.save rescue nil
assert_equal [:after_rollback], @first.history
ensure
@first.connection.class.send(:remove_method, :commit_db_transaction)
@first.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
end
end

def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint
def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
def @first.commits(i=0); @commits ||= 0; @commits += i if i; end
@first.after_rollback_block{|r| r.rollbacks(1)}
@first.after_commit_block{|r| r.commits(1)}

def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
def @second.commits(i=0); @commits ||= 0; @commits += i if i; end
@second.after_rollback_block{|r| r.rollbacks(1)}
@second.after_commit_block{|r| r.commits(1)}

Topic.transaction do
@first.save!
Topic.transaction(:requires_new => true) do
@second.save!
raise ActiveRecord::Rollback
end
end

assert_equal 1, @first.commits
assert_equal 0, @first.rollbacks
assert_equal 0, @second.commits
assert_equal 1, @second.rollbacks
end

def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails
def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
def @first.commits(i=0); @commits ||= 0; @commits += i if i; end

@first.after_rollback_block{|r| r.rollbacks(1)}
@first.after_commit_block{|r| r.commits(1)}

Topic.transaction do
@first.save
Topic.transaction(:requires_new => true) do
@first.save!
raise ActiveRecord::Rollback
end
Topic.transaction(:requires_new => true) do
@first.save!
raise ActiveRecord::Rollback
end
end

assert_equal 1, @first.commits
assert_equal 2, @first.rollbacks
end

def test_after_transaction_callbacks_should_not_raise_errors
def @first.last_after_transaction_error=(e); @last_transaction_error = e; end
def @first.last_after_transaction_error; @last_transaction_error; end
@first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";}
@first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";}

@first.save!
assert_equal :commit, @first.last_after_transaction_error

Topic.transaction do
@first.save!
raise ActiveRecord::Rollback
end

assert_equal :rollback, @first.last_after_transaction_error
end
end
33 changes: 33 additions & 0 deletions activerecord/test/cases/transactions_test.rb
Original file line number Diff line number Diff line change
@@ -320,6 +320,33 @@ def test_rollback_when_commit_raises
end
end

def test_restore_active_record_state_for_all_records_in_a_transaction
topic_1 = Topic.new(:title => 'test_1')
topic_2 = Topic.new(:title => 'test_2')
Topic.transaction do
assert topic_1.save
assert topic_2.save
@first.save
@second.destroy
assert_equal false, topic_1.new_record?
assert_not_nil topic_1.id
assert_equal false, topic_2.new_record?
assert_not_nil topic_2.id
assert_equal false, @first.new_record?
assert_not_nil @first.id
assert_equal true, @second.destroyed?
raise ActiveRecord::Rollback
end

assert_equal true, topic_1.new_record?
assert_nil topic_1.id
assert_equal true, topic_2.new_record?
assert_nil topic_2.id
assert_equal false, @first.new_record?
assert_not_nil @first.id
assert_equal false, @second.destroyed?
end

if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
def test_outside_transaction_works
assert Topic.connection.outside_transaction?
@@ -382,6 +409,12 @@ def test_sqlite_add_column_in_transaction
end

private
def define_callback_method(callback_method)
define_method(callback_method) do
self.history << [callback_method, :method]
end
end

def add_exception_raising_after_save_callback_to_topic
Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1
remove_method(:after_save_for_transaction)
2 changes: 1 addition & 1 deletion railties/guides/source/3_0_release_notes.textile
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ h4. Rails 3 requires Ruby 1.8.7+

Rails 3.0 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.0 is also compatible with Ruby 1.9.2.

TIP: Note that Ruby 1.8.7 p248 and p249 has marshaling bugs that crash Rails 3.0.0. Ruby 1.9.1 outright segfaults on Rails 3.0.0, so if you want to use Rails 3 with 1.9.x, jump on 1.9.2 trunk for smooth sailing.
TIP: Note that Ruby 1.8.7 p248 and p249 has marshaling bugs that crash Rails 3.0.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing.

h4. Rails Application object

117 changes: 114 additions & 3 deletions railties/guides/source/active_support_core_extensions.textile
Original file line number Diff line number Diff line change
@@ -2869,7 +2869,7 @@ Date.new(2010, 2, 28).advance(:days => 1).advance(:months => 1)
# => Thu, 01 Apr 2010
</ruby>

h5. Changing Date Components
h5. Changing Components

The method +change+ allows you to get a new date which is the same as the receiver except for the given year, month, or day:

@@ -2909,11 +2909,122 @@ date.end_of_day # => Sun Jun 06 23:59:59 +0200 2010

+beginning_of_day+ is aliased to +at_beginning_of_day+, +midnight+, +at_midnight+

h4. Conversions
h4(#date-conversions). Conversions

h3. Extensions to +DateTime+

NOTE TO SELF: Since +DateTime+ is a subclass of +Date+, you get inherited methods that return +DateTime+ objects.
NOTE: All the following methods are defined in +active_support/core_ext/date_time/calculations.rb+.

WARNING: +DateTime+ is not aware of DST rules and so some of these methods have edge cases when a DST change is going on. For example +seconds_since_midnight+ might not return the real amount in such a day.

h4(#calculations-datetime). Calculations

The class +DateTime+ is a subclass of +Date+ so by loading +active_support/core_ext/date/calculations.rb+ you inherit these methods and their aliases, except that they will always return datetimes:

<ruby>
yesterday
tomorrow
beginning_of_week
end_on_week
next_week
months_ago
months_since
beginning_of_month
end_of_month
prev_month
next_month
beginning_of_quarter
end_of_quarter
beginning_of_year
end_of_year
years_ago
years_since
prev_year
next_year
</ruby>

The following methods are reimplemented so you do *not* need to load +active_support/core_ext/date/calculations.rb+ for these ones:

<ruby>
beginning_of_day
end_of_day
ago
since
</ruby>

On the other hand, +advance+ and +change+ are also defined and support more options, they are documented below.

h5. Named Datetimes

h6. +DateTime.current+

Active Support defines +DateTime.current+ to be like +Time.now.to_datetime+, except that it honors the user time zone, if defined. It also defines instance predicates +past?+, and +future?+ relative to +DateTime.current+.

h5. Other Extensions

h6. +seconds_since_midnight+

The method +seconds_since_midnight+ returns the number of seconds since midnight:

<ruby>
now = DateTime.current # => Mon, 07 Jun 2010 20:26:36 +0000
now.seconds_since_midnight # => 73596
</ruby>

h6(#utc-datetime). +utc+

The method +utc+ gives you the same datetime in the receiver expressed in UTC.

<ruby>
now = DateTime.current # => Mon, 07 Jun 2010 19:27:52 -0400
now.utc # => Mon, 07 Jun 2010 23:27:52 +0000
</ruby>

This method is also aliased as +getutc+.

h6. +utc?+

The predicate +utc?+ says whether the receiver has UTC as its time zone:

<ruby>
now = DateTime.now # => Mon, 07 Jun 2010 19:30:47 -0400
now.utc? # => false
now.utc.utc? # => true
</ruby>

h5(#datetime-changing-components). Changing Components

The method +change+ allows you to get a new datetime which is the same as the receiver except for the given options, which may include +:year+, +:month+, +:day+, +:hour+, +:min+, +:sec+, +:offset+, +:start+:

<ruby>
now = DateTime.current
# => Tue, 08 Jun 2010 01:56:22 +0000
now.change(:year => 2011, :offset => Rational(-6, 24))
# => Wed, 08 Jun 2011 01:56:22 -0600
</ruby>

If hours are zeroed, then minutes and seconds are too (unless they have given values):

<ruby>
now.change(:hour => 0)
# => Tue, 08 Jun 2010 00:00:00 +0000
</ruby>

Similarly, if minutes are zeroed, then seconds are too (unless it has given a value):

<ruby>
now.change(:min => 0)
# => Tue, 08 Jun 2010 01:00:00 +0000
</ruby>

This method is not tolerant to non-existing dates, if the change is invalid +ArgumentError+ is raised:

<ruby>
DateTime.current.change(:month => 2, :day => 30)
# => ArgumentError: invalid date
</ruby>

h4(#datetime-conversions). Conversions

h3. Extensions to +Time+

2 changes: 1 addition & 1 deletion railties/guides/source/getting_started.textile
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ This guide is designed for beginners who want to get started with a Rails applic

* The "Ruby":http://www.ruby-lang.org/en/downloads language version 1.8.7 or higher

TIP: Note that Ruby 1.8.7 p248 and p249 has marshaling bugs that crash Rails 3.0.0. Ruby 1.9.1 outright segfaults on Rails 3.0.0, so if you want to use Rails 3 with 1.9.x, jump on 1.9.2 trunk for smooth sailing.
TIP: Note that Ruby 1.8.7 p248 and p249 has marshaling bugs that crash Rails 3.0.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing.

* The "RubyGems":http://rubyforge.org/frs/?group_id=126 packaging system
* A working installation of the "SQLite3 Database":http://www.sqlite.org

0 comments on commit df40dbe

Please sign in to comment.