aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gemfile3
-rw-r--r--actionmailer/lib/action_mailer/base.rb3
-rw-r--r--actionmailer/lib/rails/generators/mailer/USAGE (renamed from railties/lib/rails/generators/rails/mailer/USAGE)0
-rw-r--r--actionmailer/lib/rails/generators/mailer/mailer_generator.rb (renamed from railties/lib/rails/generators/rails/mailer/mailer_generator.rb)2
-rw-r--r--actionmailer/lib/rails/generators/mailer/templates/mailer.rb (renamed from railties/lib/rails/generators/rails/mailer/templates/mailer.rb)0
-rw-r--r--actionpack/CHANGELOG4
-rw-r--r--actionpack/lib/action_controller/caching/actions.rb2
-rw-r--r--actionpack/lib/action_view/base.rb4
-rw-r--r--actionpack/lib/action_view/helpers/date_helper.rb77
-rw-r--r--actionpack/lib/action_view/helpers/prototype_helper.rb2
-rw-r--r--actionpack/test/controller/caching_test.rb31
-rw-r--r--actionpack/test/template/date_helper_test.rb101
-rw-r--r--activemodel/lib/active_model/serializers/xml.rb162
-rw-r--r--activemodel/lib/active_model/validations.rb3
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb6
-rw-r--r--activemodel/test/cases/serializeration/xml_serialization_test.rb15
-rw-r--r--activemodel/test/models/contact.rb8
-rw-r--r--activerecord/CHANGELOG6
-rwxr-xr-xactiverecord/lib/active_record/associations.rb88
-rwxr-xr-xactiverecord/lib/active_record/base.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb56
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb35
-rw-r--r--activerecord/lib/active_record/migration.rb10
-rw-r--r--activerecord/lib/active_record/railtie.rb9
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb1
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb73
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb85
-rw-r--r--activerecord/lib/active_record/transactions.rb126
-rw-r--r--activerecord/lib/rails/generators/active_record.rb10
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb9
-rwxr-xr-xactiverecord/test/cases/base_test.rb5
-rw-r--r--activerecord/test/cases/locking_test.rb51
-rw-r--r--activerecord/test/cases/migration_test.rb19
-rw-r--r--activerecord/test/cases/serialization_test.rb7
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb244
-rw-r--r--activerecord/test/cases/transactions_test.rb61
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb12
-rw-r--r--activerecord/test/models/author.rb4
-rw-r--r--activerecord/test/models/contact.rb4
-rw-r--r--activesupport/CHANGELOG34
-rw-r--r--activesupport/activesupport.gemspec2
-rw-r--r--activesupport/lib/active_support/cache.rb496
-rw-r--r--activesupport/lib/active_support/cache/compressed_mem_cache_store.rb21
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb180
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb184
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb156
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb166
-rw-r--r--activesupport/lib/active_support/cache/synchronized_memory_store.rb40
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb42
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb149
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb94
-rw-r--r--activesupport/lib/active_support/json/encoding.rb2
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb45
-rw-r--r--activesupport/lib/active_support/xml_mini.rb127
-rw-r--r--activesupport/test/caching_test.rb649
-rw-r--r--activesupport/test/core_ext/array_ext_test.rb10
-rw-r--r--activesupport/test/json/encoding_test.rb6
-rw-r--r--activesupport/test/transliterate_test.rb51
-rw-r--r--railties/guides/source/3_0_release_notes.textile4
-rw-r--r--railties/guides/source/action_mailer_basics.textile10
-rw-r--r--railties/guides/source/command_line.textile42
-rw-r--r--railties/guides/source/credits.html.erb8
-rw-r--r--railties/guides/source/generators.textile34
-rw-r--r--railties/guides/source/getting_started.textile114
-rw-r--r--railties/guides/source/initialization.textile154
-rw-r--r--railties/guides/source/performance_testing.textile20
-rw-r--r--railties/guides/source/testing.textile38
-rw-r--r--railties/lib/rails/application.rb4
-rw-r--r--railties/lib/rails/application/bootstrap.rb3
-rw-r--r--railties/lib/rails/application/configuration.rb20
-rw-r--r--railties/lib/rails/commands.rb102
-rw-r--r--railties/lib/rails/commands/application.rb2
-rw-r--r--railties/lib/rails/commands/benchmarker.rb (renamed from railties/lib/rails/commands/performance/benchmarker.rb)0
-rw-r--r--railties/lib/rails/commands/profiler.rb (renamed from railties/lib/rails/commands/performance/profiler.rb)0
-rw-r--r--railties/lib/rails/commands/runner.rb3
-rw-r--r--railties/lib/rails/configuration.rb1
-rw-r--r--railties/lib/rails/engine.rb2
-rw-r--r--railties/lib/rails/generators/base.rb38
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/script/rails7
-rw-r--r--railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt4
-rw-r--r--railties/lib/rails/railtie.rb1
-rw-r--r--railties/railties.gemspec2
-rw-r--r--railties/test/application/middleware_test.rb7
-rw-r--r--railties/test/application/model_initialization_test.rb33
-rw-r--r--railties/test/application/paths_test.rb5
-rw-r--r--railties/test/application/rake_test.rb14
-rw-r--r--railties/test/generators/actions_test.rb2
-rw-r--r--railties/test/generators/app_generator_test.rb7
-rw-r--r--railties/test/generators/mailer_generator_test.rb3
91 files changed, 3072 insertions, 1419 deletions
diff --git a/Gemfile b/Gemfile
index dd26a9d1c2..f5968827a1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,6 +7,9 @@ gem "rake", ">= 0.8.7"
gem "mocha", ">= 0.9.8"
group :mri do
+ gem 'json'
+ gem 'yajl-ruby'
+
if RUBY_VERSION < '1.9'
gem "system_timer"
gem "ruby-debug", ">= 0.10.3"
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index d827ccdf2b..e566132f4e 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -291,8 +291,6 @@ module ActionMailer #:nodoc:
:parts_order => [ "text/plain", "text/enriched", "text/html" ]
}.freeze
- ActiveSupport.run_load_hooks(:action_mailer, self)
-
class << self
def mailer_name
@@ -643,5 +641,6 @@ module ActionMailer #:nodoc:
container.add_part(part)
end
+ ActiveSupport.run_load_hooks(:action_mailer, self)
end
end
diff --git a/railties/lib/rails/generators/rails/mailer/USAGE b/actionmailer/lib/rails/generators/mailer/USAGE
index a08d459739..a08d459739 100644
--- a/railties/lib/rails/generators/rails/mailer/USAGE
+++ b/actionmailer/lib/rails/generators/mailer/USAGE
diff --git a/railties/lib/rails/generators/rails/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
index 8993181d79..dd7fa640c9 100644
--- a/railties/lib/rails/generators/rails/mailer/mailer_generator.rb
+++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
@@ -1,6 +1,8 @@
module Rails
module Generators
class MailerGenerator < NamedBase
+ source_root File.expand_path("../templates", __FILE__)
+
argument :actions, :type => :array, :default => [], :banner => "method method"
check_class_collision
diff --git a/railties/lib/rails/generators/rails/mailer/templates/mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb
index 7343eb28b3..7343eb28b3 100644
--- a/railties/lib/rails/generators/rails/mailer/templates/mailer.rb
+++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index 04e44be291..edd7d113a6 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -1,5 +1,9 @@
*Rails 3.0.0 [beta 4/release candidate] (unreleased)*
+* Fixed inconsistencies in form builder and view helpers #4432 [Neeraj Singh]
+
+* Both :xml and :json renderers now forwards the given options to the model, allowing you to invoke them as render :xml => @projects, :include => :tasks [José Valim, Yehuda Katz]
+
* Renamed the field error CSS class from fieldWithErrors to field_with_errors for consistency. [Jeremy Kemper]
* Add support for shorthand routes like /projects/status(.:format) #4423 [Diego Carrion]
diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb
index 43ddf6435a..546f043c58 100644
--- a/actionpack/lib/action_controller/caching/actions.rb
+++ b/actionpack/lib/action_controller/caching/actions.rb
@@ -133,7 +133,7 @@ module ActionController #:nodoc:
body = controller._save_fragment(cache_path.path, @store_options)
end
- body = controller.render_to_string(:text => cache, :layout => true) unless @cache_layout
+ body = controller.render_to_string(:text => body, :layout => true) unless @cache_layout
controller.response_body = body
controller.content_type = Mime[cache_path.extension || :html]
diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb
index be9791505e..4ac2ee52d6 100644
--- a/actionpack/lib/action_view/base.rb
+++ b/actionpack/lib/action_view/base.rb
@@ -176,8 +176,6 @@ module ActionView #:nodoc:
delegate :logger, :to => 'ActionController::Base', :allow_nil => true
end
- ActiveSupport.run_load_hooks(:action_view, self)
-
attr_accessor :base_path, :assigns, :template_extension, :lookup_context
attr_internal :captures, :request, :controller, :template, :config
@@ -229,5 +227,7 @@ module ActionView #:nodoc:
response.body_parts << part
nil
end
+
+ ActiveSupport.run_load_hooks(:action_view, self)
end
end
diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb
index 42018ee261..7d846a01dd 100644
--- a/actionpack/lib/action_view/helpers/date_helper.rb
+++ b/actionpack/lib/action_view/helpers/date_helper.rb
@@ -589,56 +589,50 @@ module ActionView
@options = options.dup
@html_options = html_options.dup
@datetime = datetime
+ @options[:datetime_separator] ||= ' &mdash; '
+ @options[:time_separator] ||= ' : '
end
def select_datetime
- # TODO: Remove tag conditional
- # Ideally we could just join select_date and select_date for the tag case
+ order = date_order.dup
+ order -= [:hour, :minute, :second]
+ @options[:discard_year] ||= true unless order.include?(:year)
+ @options[:discard_month] ||= true unless order.include?(:month)
+ @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
+ @options[:discard_minute] ||= true if @options[:discard_hour]
+ @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute]
+
+ # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
+ # valid (otherwise it could be 31 and february wouldn't be a valid date)
+ if @datetime && @options[:discard_day] && !@options[:discard_month]
+ @datetime = @datetime.change(:day => 1)
+ end
+
if @options[:tag] && @options[:ignore_date]
select_time
- elsif @options[:tag]
- order = date_order.dup
- order -= [:hour, :minute, :second]
-
- @options[:discard_year] ||= true unless order.include?(:year)
- @options[:discard_month] ||= true unless order.include?(:month)
- @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
- @options[:discard_minute] ||= true if @options[:discard_hour]
- @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute]
-
- # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
- # valid (otherwise it could be 31 and february wouldn't be a valid date)
- if @datetime && @options[:discard_day] && !@options[:discard_month]
- @datetime = @datetime.change(:day => 1)
- end
-
+ else
[:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
order += [:hour, :minute, :second] unless @options[:discard_hour]
build_selects_from_types(order)
- else
- "#{select_date}#{@options[:datetime_separator]}#{select_time}".html_safe
end
end
def select_date
order = date_order.dup
- # TODO: Remove tag conditional
- if @options[:tag]
- @options[:discard_hour] = true
- @options[:discard_minute] = true
- @options[:discard_second] = true
+ @options[:discard_hour] = true
+ @options[:discard_minute] = true
+ @options[:discard_second] = true
- @options[:discard_year] ||= true unless order.include?(:year)
- @options[:discard_month] ||= true unless order.include?(:month)
- @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
+ @options[:discard_year] ||= true unless order.include?(:year)
+ @options[:discard_month] ||= true unless order.include?(:month)
+ @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
- # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
- # valid (otherwise it could be 31 and february wouldn't be a valid date)
- if @datetime && @options[:discard_day] && !@options[:discard_month]
- @datetime = @datetime.change(:day => 1)
- end
+ # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
+ # valid (otherwise it could be 31 and february wouldn't be a valid date)
+ if @datetime && @options[:discard_day] && !@options[:discard_month]
+ @datetime = @datetime.change(:day => 1)
end
[:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
@@ -649,15 +643,12 @@ module ActionView
def select_time
order = []
- # TODO: Remove tag conditional
- if @options[:tag]
- @options[:discard_month] = true
- @options[:discard_year] = true
- @options[:discard_day] = true
- @options[:discard_second] ||= true unless @options[:include_seconds]
+ @options[:discard_month] = true
+ @options[:discard_year] = true
+ @options[:discard_day] = true
+ @options[:discard_second] ||= true unless @options[:include_seconds]
- order += [:year, :month, :day] unless @options[:ignore_date]
- end
+ order += [:year, :month, :day] unless @options[:ignore_date]
order += [:hour, :minute]
order << :second if @options[:include_seconds]
@@ -937,10 +928,8 @@ module ActionView
options[:include_position] = true
options[:prefix] ||= @object_name
options[:index] = @auto_index if @auto_index && !options.has_key?(:index)
- options[:datetime_separator] ||= ' &mdash; '
- options[:time_separator] ||= ' : '
- DateTimeSelector.new(datetime, options.merge(:tag => true), html_options)
+ DateTimeSelector.new(datetime, options, html_options)
end
def default_datetime(options)
diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb
index ebe0b4e876..a798c3eaef 100644
--- a/actionpack/lib/action_view/helpers/prototype_helper.rb
+++ b/actionpack/lib/action_view/helpers/prototype_helper.rb
@@ -767,7 +767,7 @@ module ActionView
end
def grep(variable, pattern, &block)
- enumerate :grep, :variable => variable, :return => true, :method_args => [pattern], :yield_args => %w(value index), &block
+ enumerate :grep, :variable => variable, :return => true, :method_args => [::ActiveSupport::JSON::Variable.new(pattern.inspect)], :yield_args => %w(value index), &block
end
def in_groups_of(variable, number, fill_with = nil)
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index 115cc91467..4431eb2e2a 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -265,23 +265,27 @@ class ActionCacheTest < ActionController::TestCase
def test_simple_action_cache
get :index
+ assert_response :success
cached_time = content_to_cache
assert_equal cached_time, @response.body
assert fragment_exist?('hostname.com/action_caching_test')
reset!
get :index
+ assert_response :success
assert_equal cached_time, @response.body
end
def test_simple_action_not_cached
get :destroy
+ assert_response :success
cached_time = content_to_cache
assert_equal cached_time, @response.body
assert !fragment_exist?('hostname.com/action_caching_test/destroy')
reset!
get :destroy
+ assert_response :success
assert_not_equal cached_time, @response.body
end
@@ -289,12 +293,14 @@ class ActionCacheTest < ActionController::TestCase
def test_action_cache_with_layout
get :with_layout
+ assert_response :success
cached_time = content_to_cache
assert_not_equal cached_time, @response.body
assert fragment_exist?('hostname.com/action_caching_test/with_layout')
reset!
get :with_layout
+ assert_response :success
assert_not_equal cached_time, @response.body
body = body_to_string(read_fragment('hostname.com/action_caching_test/with_layout'))
assert_equal @response.body, body
@@ -302,12 +308,14 @@ class ActionCacheTest < ActionController::TestCase
def test_action_cache_with_layout_and_layout_cache_false
get :layout_false
+ assert_response :success
cached_time = content_to_cache
assert_not_equal cached_time, @response.body
assert fragment_exist?('hostname.com/action_caching_test/layout_false')
reset!
get :layout_false
+ assert_response :success
assert_not_equal cached_time, @response.body
body = body_to_string(read_fragment('hostname.com/action_caching_test/layout_false'))
@@ -317,6 +325,7 @@ class ActionCacheTest < ActionController::TestCase
def test_action_cache_conditional_options
@request.env['HTTP_ACCEPT'] = 'application/json'
get :index
+ assert_response :success
assert !fragment_exist?('hostname.com/action_caching_test')
end
@@ -325,41 +334,50 @@ class ActionCacheTest < ActionController::TestCase
@controller.expects(:read_fragment).with('hostname.com/action_caching_test', :expires_in => 1.hour).once
@controller.expects(:write_fragment).with('hostname.com/action_caching_test', '12345.0', :expires_in => 1.hour).once
get :index
+ assert_response :success
end
def test_action_cache_with_custom_cache_path
get :show
+ assert_response :success
cached_time = content_to_cache
assert_equal cached_time, @response.body
assert fragment_exist?('test.host/custom/show')
reset!
get :show
+ assert_response :success
assert_equal cached_time, @response.body
end
def test_action_cache_with_custom_cache_path_in_block
get :edit
+ assert_response :success
assert fragment_exist?('test.host/edit')
reset!
get :edit, :id => 1
+ assert_response :success
assert fragment_exist?('test.host/1;edit')
end
def test_cache_expiration
get :index
+ assert_response :success
cached_time = content_to_cache
reset!
get :index
+ assert_response :success
assert_equal cached_time, @response.body
reset!
get :expire
+ assert_response :success
reset!
get :index
+ assert_response :success
new_cached_time = content_to_cache
assert_not_equal cached_time, @response.body
reset!
@@ -376,9 +394,11 @@ class ActionCacheTest < ActionController::TestCase
@request.request_uri = "/action_caching_test/expire.xml"
get :expire, :format => :xml
+ assert_response :success
reset!
get :index
+ assert_response :success
new_cached_time = content_to_cache
assert_not_equal cached_time, @response.body
end
@@ -386,12 +406,14 @@ class ActionCacheTest < ActionController::TestCase
def test_cache_is_scoped_by_subdomain
@request.host = 'jamis.hostname.com'
get :index
+ assert_response :success
jamis_cache = content_to_cache
reset!
@request.host = 'david.hostname.com'
get :index
+ assert_response :success
david_cache = content_to_cache
assert_not_equal jamis_cache, @response.body
@@ -399,12 +421,14 @@ class ActionCacheTest < ActionController::TestCase
@request.host = 'jamis.hostname.com'
get :index
+ assert_response :success
assert_equal jamis_cache, @response.body
reset!
@request.host = 'david.hostname.com'
get :index
+ assert_response :success
assert_equal david_cache, @response.body
end
@@ -433,20 +457,24 @@ class ActionCacheTest < ActionController::TestCase
end
get :index, :format => 'xml'
+ assert_response :success
cached_time = content_to_cache
assert_equal cached_time, @response.body
assert fragment_exist?('hostname.com/action_caching_test/index.xml')
reset!
get :index, :format => 'xml'
+ assert_response :success
assert_equal cached_time, @response.body
assert_equal 'application/xml', @response.content_type
reset!
get :expire_xml
+ assert_response :success
reset!
get :index, :format => 'xml'
+ assert_response :success
assert_not_equal cached_time, @response.body
end
end
@@ -455,6 +483,7 @@ class ActionCacheTest < ActionController::TestCase
# run it twice to cache it the first time
get :index, :id => 'content-type', :format => 'xml'
get :index, :id => 'content-type', :format => 'xml'
+ assert_response :success
assert_equal 'application/xml', @response.content_type
end
@@ -462,6 +491,7 @@ class ActionCacheTest < ActionController::TestCase
# run it twice to cache it the first time
get :show, :format => 'xml'
get :show, :format => 'xml'
+ assert_response :success
assert_equal 'application/xml', @response.content_type
end
@@ -469,6 +499,7 @@ class ActionCacheTest < ActionController::TestCase
# run it twice to cache it the first time
get :edit, :id => 1, :format => 'xml'
get :edit, :id => 1, :format => 'xml'
+ assert_response :success
assert_equal 'application/xml', @response.content_type
end
diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb
index da2477b6f8..053fcc4d24 100644
--- a/actionpack/test/template/date_helper_test.rb
+++ b/actionpack/test/template/date_helper_test.rb
@@ -669,19 +669,11 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_date_with_incomplete_order
- # NOTE: modified this test because of minimal API change
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
+ # Since the order is incomplete nothing will be shown
+ expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
+ expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
+ expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n)
+
assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day])
end
@@ -903,10 +895,14 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
+ expected << " &mdash; "
+
expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -955,10 +951,14 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
+ expected << " &mdash; "
+
expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -971,6 +971,7 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
+
expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n)
expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
@@ -979,10 +980,14 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
+ expected << " &mdash; "
+
expected << %(<select id="date_first_hour" name="date[first][hour]" class="selector">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_first_minute" name="date[first][minute]" class="selector">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -1039,10 +1044,14 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="">Day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
+ expected << " &mdash; "
+
expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -1065,10 +1074,14 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
+ expected << " &mdash; "
+
expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -1078,10 +1091,16 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_time
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -1091,7 +1110,10 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_time_with_separator
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+ expected << %(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
@@ -1106,14 +1128,22 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_time_with_seconds
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+ expected << ' : '
+
expected << %(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
+ expected << ' : '
+
expected << %(<select id="date_second" name="date[second]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -1122,7 +1152,11 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_time_with_seconds_and_separator
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
@@ -1142,10 +1176,16 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_time_with_html_options
- expected = %(<select id="date_hour" name="date[hour]" class="selector">\n)
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]" class="selector">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_minute" name="date[minute]" class="selector">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -1159,14 +1199,22 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_time_with_default_prompt
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+
+ expected << " : "
expected << %(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_second" name="date[second]">\n)
expected << %(<option value="">Seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -1175,15 +1223,22 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_time_with_custom_prompt
-
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_second" name="date[second]">\n)
expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -2006,10 +2061,14 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
+ expected << " &mdash; "
+
expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
+ expected << " : "
+
expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb
index ee3e0eab06..df7026b3ec 100644
--- a/activemodel/lib/active_model/serializers/xml.rb
+++ b/activemodel/lib/active_model/serializers/xml.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/slice'
@@ -15,65 +16,29 @@ module ActiveModel
def initialize(name, serializable, raw_value=nil)
@name, @serializable = name, serializable
- @raw_value = raw_value || @serializable.send(name)
-
+ @value = value || @serializable.send(name)
@type = compute_type
- @value = compute_value
- end
-
- # There is a significant speed improvement if the value
- # does not need to be escaped, as <tt>tag!</tt> escapes all values
- # to ensure that valid XML is generated. For known binary
- # values, it is at least an order of magnitude faster to
- # Base64 encode binary values and directly put them in the
- # output XML than to pass the original value or the Base64
- # encoded value to the <tt>tag!</tt> method. It definitely makes
- # no sense to Base64 encode the value and then give it to
- # <tt>tag!</tt>, since that just adds additional overhead.
- def needs_encoding?
- ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
end
- def decorations(include_types = true)
+ def decorations
decorations = {}
-
- if type == :binary
- decorations[:encoding] = 'base64'
- end
-
- if include_types && type != :string
- decorations[:type] = type
- end
-
- if value.nil?
- decorations[:nil] = true
- end
-
+ decorations[:encoding] = 'base64' if type == :binary
+ decorations[:type] = type unless type == :string
+ decorations[:nil] = true if value.nil?
decorations
end
- protected
- def compute_type
- type = Hash::XML_TYPE_NAMES[@raw_value.class.name]
- type ||= :string if @raw_value.respond_to?(:to_str)
- type ||= :yaml
- type
- end
+ protected
- def compute_value
- if formatter = Hash::XML_FORMATTING[type.to_s]
- @raw_value ? formatter.call(@raw_value) : nil
- else
- @raw_value
- end
- end
+ def compute_type
+ type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
+ type ||= :string if value.respond_to?(:to_str)
+ type ||= :yaml
+ type
+ end
end
class MethodAttribute < Attribute #:nodoc:
- protected
- def compute_type
- Hash::XML_TYPE_NAMES[@raw_value.class.name] || :string
- end
end
attr_reader :options
@@ -92,7 +57,7 @@ module ActiveModel
# then because <tt>:except</tt> is set to a default value, the second
# level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if
# <tt>:only</tt> is set, always delete <tt>:except</tt>.
- def serializable_attributes_hash
+ def attributes_hash
attributes = @serializable.attributes
if options[:only].any?
attributes.slice(*options[:only])
@@ -104,10 +69,12 @@ module ActiveModel
end
def serializable_attributes
- serializable_attributes_hash.map { |name, value| self.class::Attribute.new(name, @serializable, value) }
+ attributes_hash.map do |name, value|
+ self.class::Attribute.new(name, @serializable, value)
+ end
end
- def serializable_method_attributes
+ def serializable_methods
Array.wrap(options[:methods]).inject([]) do |methods, name|
methods << self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
methods
@@ -115,80 +82,53 @@ module ActiveModel
end
def serialize
- args = [root]
-
- if options[:namespace]
- args << {:xmlns => options[:namespace]}
- end
+ require 'builder' unless defined? ::Builder
- if options[:type]
- args << {:type => options[:type]}
- end
-
- builder.tag!(*args) do
- add_attributes
- procs = options.delete(:procs)
- options[:procs] = procs
- add_procs
- yield builder if block_given?
- end
- end
+ options[:indent] ||= 2
+ options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
- private
- def builder
- @builder ||= begin
- require 'builder' unless defined? ::Builder
- options[:indent] ||= 2
- builder = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
+ @builder = options[:builder]
+ @builder.instruct! unless options[:skip_instruct]
- unless options[:skip_instruct]
- builder.instruct!
- options[:skip_instruct] = true
- end
+ root = (options[:root] || @serializable.class.model_name.singular).to_s
+ root = ActiveSupport::XmlMini.rename_key(root, options)
- builder
- end
- end
-
- def root
- root = (options[:root] || @serializable.class.model_name.singular).to_s
- reformat_name(root)
- end
+ args = [root]
+ args << {:xmlns => options[:namespace]} if options[:namespace]
+ args << {:type => options[:type]} if options[:type] && !options[:skip_types]
- def dasherize?
- !options.has_key?(:dasherize) || options[:dasherize]
+ @builder.tag!(*args) do
+ add_attributes_and_methods
+ add_extra_behavior
+ add_procs
+ yield @builder if block_given?
end
+ end
- def camelize?
- options.has_key?(:camelize) && options[:camelize]
- end
+ private
- def reformat_name(name)
- name = name.camelize if camelize?
- dasherize? ? name.dasherize : name
- end
+ def add_extra_behavior
+ end
- def add_attributes
- (serializable_attributes + serializable_method_attributes).each do |attribute|
- builder.tag!(
- reformat_name(attribute.name),
- attribute.value.to_s,
- attribute.decorations(!options[:skip_types])
- )
- end
+ def add_attributes_and_methods
+ (serializable_attributes + serializable_methods).each do |attribute|
+ key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
+ ActiveSupport::XmlMini.to_tag(key, attribute.value,
+ options.merge(attribute.decorations))
end
+ end
- def add_procs
- if procs = options.delete(:procs)
- [ *procs ].each do |proc|
- if proc.arity > 1
- proc.call(options, @serializable)
- else
- proc.call(options)
- end
+ def add_procs
+ if procs = options.delete(:procs)
+ Array.wrap(procs).each do |proc|
+ if proc.arity == 1
+ proc.call(options)
+ else
+ proc.call(options, @serializable)
end
end
end
+ end
end
def to_xml(options = {}, &block)
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index 708557f4ae..c69cabc888 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -133,6 +133,9 @@ module ActiveModel
_validators[attribute.to_sym]
end
+ def attribute_method?(attribute)
+ method_defined?(attribute)
+ end
private
def _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index 0423fcd17f..fbd622eb6d 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -14,8 +14,10 @@ module ActiveModel
def setup(klass)
# Note: instance_methods.map(&:to_s) is important for 1.9 compatibility
# as instance_methods returns symbols unlike 1.8 which returns strings.
- new_attributes = attributes.reject { |name| klass.instance_methods.map(&:to_s).include?("#{name}=") }
- klass.send(:attr_accessor, *new_attributes)
+ attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
+ attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
+ klass.send(:attr_reader, *attr_readers)
+ klass.send(:attr_writer, *attr_writers)
end
end
diff --git a/activemodel/test/cases/serializeration/xml_serialization_test.rb b/activemodel/test/cases/serializeration/xml_serialization_test.rb
index 6340aad531..3ba826a8d0 100644
--- a/activemodel/test/cases/serializeration/xml_serialization_test.rb
+++ b/activemodel/test/cases/serializeration/xml_serialization_test.rb
@@ -1,6 +1,7 @@
require 'cases/helper'
require 'models/contact'
require 'active_support/core_ext/object/instance_variables'
+require 'ostruct'
class Contact
extend ActiveModel::Naming
@@ -23,7 +24,9 @@ class XmlSerializationTest < ActiveModel::TestCase
@contact.age = 25
@contact.created_at = Time.utc(2006, 8, 1)
@contact.awesome = false
- @contact.preferences = { :gem => 'ruby' }
+ customer = OpenStruct.new
+ customer.name = "John"
+ @contact.preferences = customer
end
test "should serialize default root" do
@@ -92,8 +95,16 @@ class XmlSerializationTest < ActiveModel::TestCase
assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml
end
+ test "should serialize array" do
+ assert_match %r{<social type=\"array\">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(:methods => :social)
+ end
+
+ test "should serialize hash" do
+ assert_match %r{<network>\s*<git type=\"symbol\">github</git>\s*</network>}, @contact.to_xml(:methods => :network)
+ end
+
test "should serialize yaml" do
- assert_match %r{<preferences type=\"yaml\">--- \n:gem: ruby\n</preferences>}, @contact.to_xml
+ assert_match %r{<preferences type=\"yaml\">--- !ruby/object:OpenStruct \ntable:\s*:name: John\n</preferences>}, @contact.to_xml
end
test "should call proc on object" do
diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb
index a9009fbdef..605e435f39 100644
--- a/activemodel/test/models/contact.rb
+++ b/activemodel/test/models/contact.rb
@@ -3,6 +3,14 @@ class Contact
attr_accessor :id, :name, :age, :created_at, :awesome, :preferences
+ def social
+ %w(twitter github)
+ end
+
+ def network
+ {:git => :github}
+ end
+
def initialize(options = {})
options.each { |name, value| send("#{name}=", value) }
end
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index f7c6d1dc77..ac5bd8e635 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,11 @@
*Rails 3.0.0 [beta 4/release candidate] (unreleased)*
+* 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]
+
* PostgreSQL: drop support for old postgres driver. Use pg 0.9.0 or later. [Jeremy Kemper]
* Observers can prevent records from saving by returning false, just like before_save and friends. #4087 [Mislav Marohnić]
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index d94cc03938..6c64210c92 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1500,7 +1500,16 @@ module ActiveRecord
when :destroy
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
- send(reflection.name).each { |o| o.destroy }
+ send(reflection.name).each do |o|
+ # No point in executing the counter update since we're going to destroy the parent anyway
+ counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
+ if(o.respond_to? counter_method) then
+ class << o
+ self
+ end.send(:define_method, counter_method, Proc.new {})
+ end
+ o.destroy
+ end
end
before_destroy method_name
when :delete_all
@@ -1728,6 +1737,14 @@ module ActiveRecord
build(associations)
end
+ def graft(*associations)
+ associations.each do |association|
+ join_associations.detect {|a| association == a} ||
+ build(association.reflection.name, association.find_parent_in(self), association.join_class)
+ end
+ self
+ end
+
def join_associations
@joins[1..-1].to_a
end
@@ -1736,6 +1753,16 @@ module ActiveRecord
@joins[0]
end
+ def count_aliases_from_table_joins(name)
+ quoted_name = join_base.active_record.connection.quote_table_name(name.downcase)
+ join_sql = join_base.table_joins.to_s.downcase
+ join_sql.blank? ? 0 :
+ # Table names
+ join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size +
+ # Table aliases
+ join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size
+ end
+
def instantiate(rows)
rows.each_with_index do |row, i|
primary_id = join_base.record_id(row)
@@ -1780,22 +1807,22 @@ module ActiveRecord
end
protected
- def build(associations, parent = nil)
+ def build(associations, parent = nil, join_class = Arel::InnerJoin)
parent ||= @joins.last
case associations
when Symbol, String
reflection = parent.reflections[associations.to_s.intern] or
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
@reflections << reflection
- @joins << build_join_association(reflection, parent)
+ @joins << build_join_association(reflection, parent).with_join_class(join_class)
when Array
associations.each do |association|
- build(association, parent)
+ build(association, parent, join_class)
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
- build(name, parent)
- build(associations[name])
+ build(name, parent, join_class)
+ build(associations[name], nil, join_class)
end
else
raise ConfigurationError, associations.inspect
@@ -1872,6 +1899,12 @@ module ActiveRecord
@table_joins = joins
end
+ def ==(other)
+ other.is_a?(JoinBase) &&
+ other.active_record == active_record &&
+ other.table_joins == table_joins
+ end
+
def aliased_prefix
"t0"
end
@@ -1937,6 +1970,27 @@ module ActiveRecord
end
end
+ def ==(other)
+ other.is_a?(JoinAssociation) &&
+ other.reflection == reflection &&
+ other.parent == parent
+ end
+
+ def find_parent_in(other_join_dependency)
+ other_join_dependency.joins.detect do |join|
+ self.parent == join
+ end
+ end
+
+ def join_class
+ @join_class ||= Arel::InnerJoin
+ end
+
+ def with_join_class(join_class)
+ @join_class = join_class
+ self
+ end
+
def association_join
return @join if @join
@@ -2036,27 +2090,25 @@ module ActiveRecord
end
def join_relation(joining_relation, join = nil)
- if (relations = relation).is_a?(Array)
- joining_relation.joins(Relation::JoinOperation.new(relations.first, Arel::OuterJoin, association_join.first)).
- joins(Relation::JoinOperation.new(relations.last, Arel::OuterJoin, association_join.last))
- else
- joining_relation.joins(Relation::JoinOperation.new(relations, Arel::OuterJoin, association_join))
- end
+ joining_relation.joins(self.with_join_class(Arel::OuterJoin))
end
protected
def aliased_table_name_for(name, suffix = nil)
- if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{active_record.connection.quote_table_name name.downcase}\son}
- @join_dependency.table_aliases[name] += 1
+ if @join_dependency.table_aliases[name].zero?
+ @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name)
end
- unless @join_dependency.table_aliases[name].zero?
- # if the table name has been used, then use an alias
+ if !@join_dependency.table_aliases[name].zero? # We need an alias
name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
- table_index = @join_dependency.table_aliases[name]
@join_dependency.table_aliases[name] += 1
- name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
+ if @join_dependency.table_aliases[name] == 1 # First time we've seen this name
+ # Also need to count the aliases from the table_aliases to avoid incorrect count
+ @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name)
+ end
+ table_index = @join_dependency.table_aliases[name]
+ name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1
else
@join_dependency.table_aliases[name] += 1
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 2d7cfad80d..9ed53cc4af 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -931,6 +931,10 @@ module ActiveRecord #:nodoc:
subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
end
+ def attribute_method?(attribute)
+ super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
+ end
+
# Set the lookup ancestors for ActiveModel.
def lookup_ancestors #:nodoc:
klass = self
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 0c87e052c4..b9fb452eee 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -122,6 +122,8 @@ module ActiveRecord
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 @@ module ActiveRecord
end
increment_open_transactions
transaction_open = true
+ @_current_transaction_records.push([])
end
yield
end
@@ -141,8 +144,10 @@ module ActiveRecord
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 @@ module ActiveRecord
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 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 9044ca418b..60ad23f38c 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -23,6 +23,16 @@ module ActiveRecord
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
+ # Optimistic locking will also check for stale data when objects are destroyed. Example:
+ #
+ # p1 = Person.find(1)
+ # p2 = Person.find(1)
+ #
+ # p1.first_name = "Michael"
+ # p1.save
+ #
+ # p2.destroy # Raises a ActiveRecord::StaleObjectError
+ #
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
@@ -39,6 +49,7 @@ module ActiveRecord
self.lock_optimistically = true
alias_method_chain :update, :lock
+ alias_method_chain :destroy, :lock
alias_method_chain :attributes_from_column_definition, :lock
class << self
@@ -88,7 +99,7 @@ module ActiveRecord
unless affected_rows == 1
- raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
+ raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
end
affected_rows
@@ -100,6 +111,28 @@ module ActiveRecord
end
end
+ def destroy_with_lock #:nodoc:
+ return destroy_without_lock unless locking_enabled?
+
+ unless new_record?
+ lock_col = self.class.locking_column
+ previous_value = send(lock_col).to_i
+
+ affected_rows = connection.delete(
+ "DELETE FROM #{self.class.quoted_table_name} " +
+ "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
+ "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
+ "#{self.class.name} Destroy"
+ )
+
+ unless affected_rows == 1
+ raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}"
+ end
+ end
+
+ freeze
+ end
+
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index c163fb982a..940f825038 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -384,9 +384,13 @@ module ActiveRecord
class << self
def migrate(migrations_path, target_version = nil)
case
- when target_version.nil? then up(migrations_path, target_version)
- when current_version > target_version then down(migrations_path, target_version)
- else up(migrations_path, target_version)
+ when target_version.nil?
+ up(migrations_path, target_version)
+ when current_version == 0 && target_version == 0
+ when current_version > target_version
+ down(migrations_path, target_version)
+ else
+ up(migrations_path, target_version)
end
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index f3d21d4969..898df0a67a 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -61,13 +61,8 @@ module ActiveRecord
# Setup database middleware after initializers have run
initializer "active_record.initialize_database_middleware", :after => "action_controller.set_configs" do |app|
middleware = app.config.middleware
- if middleware.include?("ActiveRecord::SessionStore")
- middleware.insert_before "ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement
- middleware.insert_before "ActiveRecord::SessionStore", ActiveRecord::QueryCache
- else
- middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement
- middleware.use ActiveRecord::QueryCache
- end
+ middleware.insert_after "::ActionDispatch::Callbacks", ActiveRecord::QueryCache
+ middleware.insert_after "::ActionDispatch::Callbacks", ActiveRecord::ConnectionAdapters::ConnectionManagement
end
initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app|
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 3514d0a259..d6144dc206 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -188,7 +188,6 @@ module ActiveRecord
def construct_relation_for_association_calculations
including = (@eager_load_values + @includes_values).uniq
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel))
-
relation = except(:includes, :eager_load, :preload)
apply_join_dependency(relation, join_dependency)
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 58af930446..7bca12d85e 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -80,6 +80,26 @@ module ActiveRecord
@arel ||= build_arel
end
+ def custom_join_sql(*joins)
+ arel = table
+ joins.each do |join|
+ next if join.blank?
+
+ @implicit_readonly = true
+
+ case join
+ when Hash, Array, Symbol
+ if array_of_strings?(join)
+ join_string = join.join(' ')
+ arel = arel.join(join_string)
+ end
+ else
+ arel = arel.join(join)
+ end
+ end
+ arel.joins(arel)
+ end
+
def build_arel
arel = table
@@ -88,50 +108,41 @@ module ActiveRecord
joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
- # Build association joins first
joins.each do |join|
association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join)
end
- if association_joins.any?
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins.uniq, nil)
- to_join = []
+ stashed_association_joins = joins.select {|j| j.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)}
- join_dependency.join_associations.each do |association|
- if (association_relation = association.relation).is_a?(Array)
- to_join << [association_relation.first, association.association_join.first]
- to_join << [association_relation.last, association.association_join.last]
- else
- to_join << [association_relation, association.association_join]
- end
- end
+ non_association_joins = (joins - association_joins - stashed_association_joins).reject {|j| j.blank?}
+ custom_joins = custom_join_sql(*non_association_joins)
- to_join.each do |tj|
- unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] }
- joined_associations << tj
- arel = arel.join(tj[0]).on(*tj[1])
- end
- end
- end
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins)
- joins.each do |join|
- next if join.blank?
+ join_dependency.graft(*stashed_association_joins)
- @implicit_readonly = true
+ @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
- case join
- when Relation::JoinOperation
- arel = arel.join(join.relation, join.join_class).on(*join.on)
- when Hash, Array, Symbol
- if array_of_strings?(join)
- join_string = join.join(' ')
- arel = arel.join(join_string)
- end
+ to_join = []
+
+ join_dependency.join_associations.each do |association|
+ if (association_relation = association.relation).is_a?(Array)
+ to_join << [association_relation.first, association.join_class, association.association_join.first]
+ to_join << [association_relation.last, association.join_class, association.association_join.last]
else
- arel = arel.join(join)
+ to_join << [association_relation, association.join_class, association.association_join]
end
end
+ to_join.each do |tj|
+ unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] }
+ joined_associations << tj
+ arel = arel.join(tj[0], tj[1]).on(*tj[2])
+ end
+ end
+
+ arel = arel.join(custom_joins)
+
@where_values.uniq.each do |where|
next if where.blank?
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 255b03433d..b2d4a48945 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -182,16 +182,31 @@ module ActiveRecord #:nodoc:
options[:except] |= Array.wrap(@serializable.class.inheritance_column)
end
+ def add_extra_behavior
+ add_includes
+ end
+
+ def add_includes
+ procs = options.delete(:procs)
+ @serializable.send(:serializable_add_includes, options) do |association, records, opts|
+ add_associations(association, records, opts)
+ end
+ options[:procs] = procs
+ end
+
+ # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
def add_associations(association, records, opts)
+ association_name = association.to_s.singularize
+ merged_options = options.merge(opts).merge!(:root => association_name, :skip_instruct => true)
+
if records.is_a?(Enumerable)
- tag = reformat_name(association.to_s)
- type = options[:skip_types] ? {} : {:type => "array"}
+ tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
+ type = options[:skip_types] ? { } : {:type => "array"}
if records.empty?
- builder.tag!(tag, type)
+ @builder.tag!(tag, type)
else
- builder.tag!(tag, type) do
- association_name = association.to_s.singularize
+ @builder.tag!(tag, type) do
records.each do |record|
if options[:skip_types]
record_type = {}
@@ -200,60 +215,30 @@ module ActiveRecord #:nodoc:
record_type = {:type => record_class}
end
- record.to_xml opts.merge(:root => association_name).merge(record_type)
+ record.to_xml merged_options.merge(record_type)
end
end
end
- else
- if record = @serializable.send(association)
- record.to_xml(opts.merge(:root => association))
- end
- end
- end
-
- def serialize
- args = [root]
- if options[:namespace]
- args << {:xmlns=>options[:namespace]}
- end
-
- if options[:type]
- args << {:type=>options[:type]}
- end
-
- builder.tag!(*args) do
- add_attributes
- procs = options.delete(:procs)
- @serializable.send(:serializable_add_includes, options) { |association, records, opts|
- add_associations(association, records, opts)
- }
- options[:procs] = procs
- add_procs
- yield builder if block_given?
+ elsif record = @serializable.send(association)
+ record.to_xml(merged_options)
end
end
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
- protected
- def compute_type
- type = @serializable.class.serialized_attributes.has_key?(name) ? :yaml : @serializable.class.columns_hash[name].type
-
- case type
- when :text
- :string
- when :time
- :datetime
- else
- type
- end
- end
- end
+ def compute_type
+ type = @serializable.class.serialized_attributes.has_key?(name) ?
+ super : @serializable.class.columns_hash[name].type
- class MethodAttribute < Attribute #:nodoc:
- protected
- def compute_type
- Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string
+ case type
+ when :text
+ :string
+ when :time
+ :datetime
+ else
+ type
end
+ end
+ protected :compute_type
end
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index cf0fe8934d..0a55ef2b53 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -12,6 +12,9 @@ module ActiveRecord
[:destroy, :save, :save!].each do |method|
alias_method_chain method, :transactions
end
+
+ define_model_callbacks :commit, :commit_on_update, :commit_on_create, :commit_on_destroy, :only => :after
+ define_model_callbacks :rollback, :rollback_on_update, :rollback_on_create, :rollback_on_destroy
end
# Transactions are protective blocks where SQL statements are only permanent
@@ -108,7 +111,7 @@ module ActiveRecord
# rescue ActiveRecord::StatementInvalid
# # ...which we ignore.
# end
- #
+ #
# # On PostgreSQL, the transaction is now unusable. The following
# # statement will cause a PostgreSQL error, even though the unique
# # constraint is no longer violated:
@@ -132,7 +135,7 @@ module ActiveRecord
# raise ActiveRecord::Rollback
# end
# end
- #
+ #
# User.find(:all) # => empty
#
# It is also possible to requires a sub-transaction by passing
@@ -147,7 +150,7 @@ module ActiveRecord
# raise ActiveRecord::Rollback
# end
# end
- #
+ #
# User.find(:all) # => Returns only Kotori
#
# Most databases don't support true nested transactions. At the time of
@@ -157,6 +160,21 @@ module ActiveRecord
# 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.
+ #
+ # The after_commit callbacks are called on every record saved or destroyed within a
+ # transaction immediately after the transaction is committed. The 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
@@ -166,7 +184,7 @@ module ActiveRecord
# is finished and tries to release the savepoint it created earlier, a
# database error will occur because the savepoint has already been
# automatically released. The following example demonstrates the problem:
- #
+ #
# Model.connection.transaction do # BEGIN
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...) # active_record_1 now automatically released
@@ -197,24 +215,55 @@ module ActiveRecord
end
def save_with_transactions! #:nodoc:
- rollback_active_record_state! { self.class.transaction { save_without_transactions! } }
+ with_transaction_returning_status(:save_without_transactions!)
end
# 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
+ restore_transaction_record_state
+ raise
+ ensure
+ clear_transaction_record_state
+ end
+
+ # Call the after_commit callbacks
+ def committed! #:nodoc:
+ if transaction_record_state(:new_record)
+ _run_commit_on_create_callbacks
+ elsif transaction_record_state(:destroyed)
+ _run_commit_on_destroy_callbacks
else
- @attributes.delete(self.class.primary_key)
- @attributes_cache.delete(self.class.primary_key)
+ _run_commit_on_update_callbacks
+ end
+ _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:
+ if transaction_record_state(:new_record)
+ _run_rollback_on_create_callbacks
+ elsif transaction_record_state(:destroyed)
+ _run_rollback_on_destroy_callbacks
+ else
+ _run_rollback_on_update_callbacks
+ end
+ _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
- raise
end
# Executes +method+ within a transaction and captures its return value as a
@@ -226,10 +275,59 @@ module ActiveRecord
def with_transaction_returning_status(method, *args)
status = nil
self.class.transaction do
+ add_to_transaction
status = send(method, *args)
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] = @new_record
+ 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
end
end
diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb
index d2b1e86857..5d8a8e81bc 100644
--- a/activerecord/lib/rails/generators/active_record.rb
+++ b/activerecord/lib/rails/generators/active_record.rb
@@ -8,16 +8,12 @@ module ActiveRecord
class Base < Rails::Generators::NamedBase #:nodoc:
include Rails::Generators::Migration
- def self.source_root
- @_ar_source_root ||= begin
- if base_name && generator_name
- File.expand_path(File.join(base_name, generator_name, 'templates'), File.dirname(__FILE__))
- end
- end
+ # Set the current directory as base for the inherited generators.
+ def self.base_root
+ File.dirname(__FILE__)
end
# Implement the required interface for Rails::Generators::Migration.
- #
def self.next_migration_number(dirname) #:nodoc:
next_migration_number = current_migration_number(dirname) + 1
if ActiveRecord::Base.timestamped_migrations
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index ed2e2e9f8f..fe558f9d3b 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -29,6 +29,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
assert_equal 2, authors[1].categorizations.size
end
+ def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
+ assert_nothing_raised do
+ Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all
+ end
+ authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all
+ assert_equal 1, assert_no_queries { authors.size }
+ assert_equal 9, assert_no_queries { authors[0].comments.size }
+ end
+
def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
assert_equal 2, authors.size
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 2f4243a6aa..3623680de9 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -2085,6 +2085,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal "topic", xml.root.name
assert_equal "The First Topic" , xml.elements["//title"].text
assert_equal "David" , xml.elements["//author-name"].text
+ assert_match "Have a nice day", xml.elements["//content"].text
assert_equal "1", xml.elements["//id"].text
assert_equal "integer" , xml.elements["//id"].attributes['type']
@@ -2095,10 +2096,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text
assert_equal "datetime" , xml.elements["//written-on"].attributes['type']
- assert_match(/^--- Have a nice day\n/ , xml.elements["//content"].text)
- assert_equal 'Have a nice day' , YAML.load(xml.elements["//content"].text)
- assert_equal "yaml" , xml.elements["//content"].attributes['type']
-
assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text
assert_equal nil, xml.elements["//parent-id"].text
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index dfaecf35cf..aa2d9527f9 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -38,6 +38,25 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
+ # See Lighthouse ticket #1966
+ def test_lock_destroy
+ p1 = Person.find(1)
+ p2 = Person.find(1)
+ assert_equal 0, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ p1.first_name = 'stu'
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
+
+ assert p1.destroy
+ assert_equal true, p1.frozen?
+ assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
+ end
+
def test_lock_repeating
p1 = Person.find(1)
p2 = Person.find(1)
@@ -150,6 +169,32 @@ class OptimisticLockingTest < ActiveRecord::TestCase
end
end
end
+
+ # See Lighthouse ticket #1966
+ def test_destroy_dependents
+ # Establish dependent relationship between People and LegacyThing
+ add_counter_column_to(Person, 'legacy_things_count')
+ LegacyThing.connection.add_column LegacyThing.table_name, 'person_id', :integer
+ LegacyThing.reset_column_information
+ LegacyThing.class_eval do
+ belongs_to :person, :counter_cache => true
+ end
+ Person.class_eval do
+ has_many :legacy_things, :dependent => :destroy
+ end
+
+ # Make sure that counter incrementing doesn't cause problems
+ p1 = Person.new(:first_name => 'fjord')
+ p1.save!
+ t = LegacyThing.new(:person => p1)
+ t.save!
+ p1.reload
+ assert_equal 1, p1.legacy_things_count
+ assert p1.destroy
+ assert_equal true, p1.frozen?
+ assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) }
+ assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) }
+ end
def test_quote_table_name
ref = references(:michael_magician)
@@ -168,11 +213,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase
private
- def add_counter_column_to(model)
- model.connection.add_column model.table_name, :test_count, :integer, :null => false, :default => 0
+ def add_counter_column_to(model, col='test_count')
+ model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0
model.reset_column_information
# OpenBase does not set a value to existing rows when adding a not null default column
- model.update_all(:test_count => 0) if current_adapter?(:OpenBaseAdapter)
+ model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter)
end
def remove_counter_column_from(model)
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 7a26ee072d..a3d1ceaa1f 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -1136,6 +1136,25 @@ if ActiveRecord::Base.connection.supports_migrations?
load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb")
end
+ def test_target_version_zero_should_run_only_once
+ # migrate up to 1
+ ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1)
+
+ # migrate down to 0
+ ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0)
+
+ # now unload the migrations that have been defined
+ PeopleHaveLastNames.unloadable
+ ActiveSupport::Dependencies.remove_unloadable_constants!
+
+ # migrate down to 0 again
+ ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0)
+
+ assert !defined? PeopleHaveLastNames
+ ensure
+ load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb")
+ end
+
def test_migrator_db_has_no_schema_migrations_table
# Oracle adapter raises error if semicolon is present as last character
if current_adapter?(:OracleAdapter)
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 8841694271..8c385af97c 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -44,4 +44,11 @@ class SerializationTest < ActiveRecord::TestCase
assert_equal @contact_attributes[:awesome], contact.awesome, "For #{format}"
end
end
+
+ def test_serialize_should_xml_skip_instruct_for_included_records
+ @contact.alternative = Contact.new(:name => 'Copa Cabana')
+ @serialized = @contact.to_xml(:include => [ :alternative ])
+ assert_equal @serialized.index('<?xml '), 0
+ assert_nil @serialized.index('<?xml ', 1)
+ end
end
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
new file mode 100644
index 0000000000..a07da093f1
--- /dev/null
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -0,0 +1,244 @@
+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 @first.history, [:after_commit]
+ end
+
+ def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record
+ commit_callback = []
+ @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_commit_block(:create){|r| r.history << :rollback_on_create}
+ @first.after_commit_block(:update){|r| r.history << :rollback_on_update}
+ @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
+
+ @first.save!
+ assert @first.history, [:commit_on_update]
+ end
+
+ def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record
+ commit_callback = []
+ @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_commit_block(:create){|r| r.history << :rollback_on_create}
+ @first.after_commit_block(:update){|r| r.history << :rollback_on_update}
+ @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
+
+ @first.destroy
+ assert @first.history, [:commit_on_destroy]
+ 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_commit_block(:create){|r| r.history << :rollback_on_create}
+ @new_record.after_commit_block(:update){|r| r.history << :rollback_on_update}
+ @new_record.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
+
+ @new_record.save!
+ assert @new_record.history, [:commit_on_create]
+ 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 @first.history, [:after_rollback]
+ end
+
+ def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record
+ commit_callback = []
+ @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_commit_block(:create){|r| r.history << :rollback_on_create}
+ @first.after_commit_block(:update){|r| r.history << :rollback_on_update}
+ @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
+
+ Topic.transaction do
+ @first.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert @first.history, [:rollback_on_update]
+ end
+
+ def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record
+ commit_callback = []
+ @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_commit_block(:create){|r| r.history << :rollback_on_create}
+ @first.after_commit_block(:update){|r| r.history << :rollback_on_update}
+ @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
+
+ Topic.transaction do
+ @first.destroy
+ raise ActiveRecord::Rollback
+ end
+
+ assert @first.history, [:rollback_on_destroy]
+ 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_commit_block(:create){|r| r.history << :rollback_on_create}
+ @new_record.after_commit_block(:update){|r| r.history << :rollback_on_update}
+ @new_record.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
+
+ Topic.transaction do
+ @new_record.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert @new_record.history, [:rollback_on_create]
+ 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 @first.history == [:after_rollback]
+ 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 1, @first.commits
+ assert 0, @first.rollbacks
+ assert 1, @second.commits
+ assert 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
+
+ @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
+ @first.save!
+ raise ActiveRecord::Rollback
+ end
+ Topic.transaction(:requires_new => true) do
+ @first.save!
+ raise ActiveRecord::Rollback
+ end
+ end
+
+ assert 1, @first.commits
+ assert 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 @first.last_after_transaction_error, :commit
+
+ Topic.transaction do
+ @first.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert_equal @first.last_after_transaction_error, :rollback
+ end
+end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index c550030329..958a4e4f94 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -262,22 +262,22 @@ class TransactionTest < ActiveRecord::TestCase
assert !@first.reload.approved?
assert !@second.reload.approved?
end if Topic.connection.supports_savepoints?
-
+
def test_many_savepoints
Topic.transaction do
@first.content = "One"
@first.save!
-
+
begin
Topic.transaction :requires_new => true do
@first.content = "Two"
@first.save!
-
+
begin
Topic.transaction :requires_new => true do
@first.content = "Three"
@first.save!
-
+
begin
Topic.transaction :requires_new => true do
@first.content = "Four"
@@ -286,22 +286,22 @@ class TransactionTest < ActiveRecord::TestCase
end
rescue
end
-
+
@three = @first.reload.content
raise
end
rescue
end
-
+
@two = @first.reload.content
raise
end
rescue
end
-
+
@one = @first.reload.content
end
-
+
assert_equal "One", @one
assert_equal "Two", @two
assert_equal "Three", @three
@@ -319,7 +319,34 @@ class TransactionTest < ActiveRecord::TestCase
end
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?
@@ -328,7 +355,7 @@ class TransactionTest < ActiveRecord::TestCase
Topic.connection.rollback_db_transaction
assert Topic.connection.outside_transaction?
end
-
+
def test_rollback_wont_be_executed_if_no_transaction_active
assert_raise RuntimeError do
Topic.transaction do
@@ -338,7 +365,7 @@ class TransactionTest < ActiveRecord::TestCase
end
end
end
-
+
def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active
Topic.transaction do
Topic.transaction do
@@ -358,12 +385,12 @@ class TransactionTest < ActiveRecord::TestCase
#
# We go back to the connection for the column queries because
# Topic.columns is cached and won't report changes to the DB
-
+
assert_nothing_raised do
Topic.reset_column_information
Topic.connection.add_column('topics', 'stuff', :string)
assert Topic.column_names.include?('stuff')
-
+
Topic.reset_column_information
Topic.connection.remove_column('topics', 'stuff')
assert !Topic.column_names.include?('stuff')
@@ -382,6 +409,12 @@ class TransactionTest < ActiveRecord::TestCase
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)
@@ -440,7 +473,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
def test_automatic_savepoint_in_outer_transaction
@first = Topic.find(1)
-
+
begin
Topic.transaction do
@first.approved = true
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index b1c75ec8cd..751946ffc5 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -79,8 +79,8 @@ class DefaultXmlSerializationTest < ActiveRecord::TestCase
assert_match %r{<awesome type=\"boolean\">false</awesome>}, @xml
end
- def test_should_serialize_yaml
- assert_match %r{<preferences type=\"yaml\">---\s?\n:gem: ruby\n</preferences>}, @xml
+ def test_should_serialize_hash
+ assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @xml
end
end
@@ -234,4 +234,12 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
assert types.include?('StiPost')
end
+ def test_should_produce_xml_for_methods_returning_array
+ xml = authors(:david).to_xml(:methods => :social)
+ array = Hash.from_xml(xml)['author']['social']
+ assert_equal 2, array.size
+ assert array.include? 'twitter'
+ assert array.include? 'github'
+ end
+
end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 025f6207f8..655b45bf57 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -104,6 +104,10 @@ class Author < ActiveRecord::Base
"#{id}-#{name}"
end
+ def social
+ %w(twitter github)
+ end
+
private
def log_before_adding(object)
@post_log << "before_adding#{object.id || '<new>'}"
diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb
index dbfa57bf49..975a885331 100644
--- a/activerecord/test/models/contact.rb
+++ b/activerecord/test/models/contact.rb
@@ -13,4 +13,6 @@ class Contact < ActiveRecord::Base
column :preferences, :string
serialize :preferences
-end \ No newline at end of file
+
+ belongs_to :alternative, :class_name => 'Contact'
+end
diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG
index c47839b001..f24a1b1c6c 100644
--- a/activesupport/CHANGELOG
+++ b/activesupport/CHANGELOG
@@ -1,5 +1,39 @@
*Rails 3.0.0 [beta 4/release candidate] (unreleased)*
+* Array#to_xml is more powerful and able to handle the same types as Hash#to_xml #4490 [Neeraj Singh]
+
+* Harmonize the caching API and refactor the backends. #4452 [Brian Durand]
+ All caches:
+ * Add default options to initializer that will be sent to all read, write, fetch, exist?, increment, and decrement
+ * Add support for the :expires_in option to fetch and write for all caches. Cache entries are stored with the create timestamp and a ttl so that expiration can be handled independently of the implementation.
+ * Add support for a :namespace option. This can be used to set a global prefix for cache entries.
+ * Deprecate expand_cache_key on ActiveSupport::Cache and move it to ActionController::Caching and ActionDispatch::Http::Cache since the logic in the method used some Rails specific environment variables and was only used by ActionPack classes. Not very DRY but there didn't seem to be a good shared spot and ActiveSupport really shouldn't be Rails specific.
+ * Add support for :race_condition_ttl to fetch. This setting can prevent race conditions on fetch calls where several processes try to regenerate a recently expired entry at once.
+ * Add support for :compress option to fetch and write which will compress any data over a configurable threshold.
+ * Nil values can now be stored in the cache and are distinct from cache misses for fetch.
+ * Easier API to create new implementations. Just need to implement the methods read_entry, write_entry, and delete_entry instead of overwriting existing methods.
+ * Since all cache implementations support storing objects, update the docs to state that ActiveCache::Cache::Store implementations should store objects. Keys, however, must be strings since some implementations require that.
+ * Increase test coverage.
+ * Document methods which are provided as convenience but which may not be universally available.
+
+ MemoryStore:
+ * MemoryStore can now safely be used as the cache for single server sites.
+ * Make thread safe so that the default cache implementation used by Rails is thread safe. The overhead is minimal and it is still the fastest store available.
+ * Provide :size initialization option indicating the maximum size of the cache in memory (defaults to 32Mb).
+ * Add prune logic that removes the least recently used cache entries to keep the cache size from exceeding the max.
+ * Deprecated SynchronizedMemoryStore since it isn't needed anymore.
+
+ FileStore:
+ * Escape key values so they will work as file names on all file systems, be consistent, and case sensitive
+ * Use a hash algorithm to segment the cache into sub directories so that a large cache doesn't exceed file system limits.
+ * FileStore can be slow so implement the LocalCache strategy to cache reads for the duration of a request.
+ * Add cleanup method to keep the disk from filling up with expired entries.
+ * Fix increment and decrement to use file system locks so they are consistent between processes.
+
+ MemCacheStore:
+ * Support all keys. Previously keys with spaces in them would fail
+ * Deprecate CompressedMemCacheStore since it isn't needed anymore (use :compress => true)
+
* JSON: encode objects that don't have a native JSON representation using to_hash, if available, instead of instance_values (the old fallback) or to_s (other encoders' default). Encode BigDecimal and Regexp encode as strings to conform with other encoders. Try to transcode non-UTF-8 strings. [Jeremy Kemper]
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index ad1401bfa9..0fea84a6ef 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
s.has_rdoc = true
- s.add_dependency('i18n', '~> 0.3.6')
+ s.add_dependency('i18n', '~> 0.4.0.beta')
s.add_dependency('tzinfo', '~> 0.3.16')
s.add_dependency('builder', '~> 2.1.2')
s.add_dependency('memcache-client', '>= 1.7.5')
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 7213b24f2d..ec5007c284 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -1,8 +1,12 @@
require 'benchmark'
+require 'zlib'
+require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/benchmark'
require 'active_support/core_ext/exception'
require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/numeric/bytes'
+require 'active_support/core_ext/numeric/time'
require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/string/inflections'
@@ -11,10 +15,16 @@ module ActiveSupport
module Cache
autoload :FileStore, 'active_support/cache/file_store'
autoload :MemoryStore, 'active_support/cache/memory_store'
- autoload :SynchronizedMemoryStore, 'active_support/cache/synchronized_memory_store'
autoload :MemCacheStore, 'active_support/cache/mem_cache_store'
+ autoload :SynchronizedMemoryStore, 'active_support/cache/synchronized_memory_store'
autoload :CompressedMemCacheStore, 'active_support/cache/compressed_mem_cache_store'
+ EMPTY_OPTIONS = {}.freeze
+
+ # These options mean something to all cache implementations. Individual cache
+ # implementations may support additional optons.
+ UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl]
+
module Strategy
autoload :LocalCache, 'active_support/cache/strategy/local_cache'
end
@@ -59,15 +69,12 @@ module ActiveSupport
end
end
- RAILS_CACHE_ID = ENV["RAILS_CACHE_ID"]
- RAILS_APP_VERION = ENV["RAILS_APP_VERION"]
- EXPANDED_CACHE = RAILS_CACHE_ID || RAILS_APP_VERION
-
def self.expand_cache_key(key, namespace = nil)
expanded_cache_key = namespace ? "#{namespace}/" : ""
- if EXPANDED_CACHE
- expanded_cache_key << "#{RAILS_CACHE_ID || RAILS_APP_VERION}/"
+ prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
+ if prefix
+ expanded_cache_key << "#{prefix}/"
end
expanded_cache_key <<
@@ -92,26 +99,75 @@ module ActiveSupport
# ActiveSupport::Cache::MemCacheStore. MemCacheStore is currently the most
# popular cache store for large production websites.
#
- # ActiveSupport::Cache::Store is meant for caching strings. Some cache
- # store implementations, like MemoryStore, are able to cache arbitrary
- # Ruby objects, but don't count on every cache store to be able to do that.
+ # Some implementations may not support all methods beyond the basic cache
+ # methods of +fetch+, +write+, +read+, +exist?+, and +delete+.
+ #
+ # ActiveSupport::Cache::Store can store any serializable Ruby object.
#
# cache = ActiveSupport::Cache::MemoryStore.new
#
# cache.read("city") # => nil
# cache.write("city", "Duckburgh")
# cache.read("city") # => "Duckburgh"
+ #
+ # Keys are always translated into Strings and are case sensitive. When an
+ # object is specified as a key, its +cache_key+ method will be called if it
+ # is defined. Otherwise, the +to_param+ method will be called. Hashes and
+ # Arrays can be used as keys. The elements will be delimited by slashes
+ # and Hashes elements will be sorted by key so they are consistent.
+ #
+ # cache.read("city") == cache.read(:city) # => true
+ #
+ # Nil values can be cached.
+ #
+ # If your cache is on a shared infrastructure, you can define a namespace for
+ # your cache entries. If a namespace is defined, it will be prefixed on to every
+ # key. The namespace can be either a static value or a Proc. If it is a Proc, it
+ # will be invoked when each key is evaluated so that you can use application logic
+ # to invalidate keys.
+ #
+ # cache.namespace = lambda { @last_mod_time } # Set the namespace to a variable
+ # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
+ #
+ # All caches support auto expiring content after a specified number of seconds.
+ # To set the cache entry time to live, you can either specify +:expires_in+ as
+ # an option to the constructor to have it affect all entries or to the +fetch+
+ # or +write+ methods for just one entry.
+ #
+ # cache = ActiveSupport::Cache::MemoryStore.new(:expire_in => 5.minutes)
+ # cache.write(key, value, :expire_in => 1.minute) # Set a lower value for one entry
+ #
+ # Caches can also store values in a compressed format to save space and reduce
+ # time spent sending data. Since there is some overhead, values must be large
+ # enough to warrant compression. To turn on compression either pass
+ # <tt>:compress => true</tt> in the initializer or to +fetch+ or +write+.
+ # To specify the threshold at which to compress values, set
+ # <tt>:compress_threshold</tt>. The default threshold is 32K.
class Store
- cattr_accessor :logger, :instance_writter => false
+
+ cattr_accessor :logger, :instance_writer => true
attr_reader :silence
alias :silence? :silence
+ # Create a new cache. The options will be passed to any write method calls except
+ # for :namespace which can be used to set the global namespace for the cache.
+ def initialize (options = nil)
+ @options = options ? options.dup : {}
+ end
+
+ # Get the default options set when the cache was created.
+ def options
+ @options ||= {}
+ end
+
+ # Silence the logger.
def silence!
@silence = true
self
end
+ # Silence the logger within a block.
def mute
previous_silence, @silence = defined?(@silence) && @silence, true
yield
@@ -152,28 +208,85 @@ module ActiveSupport
# cache.write("today", "Monday")
# cache.fetch("today", :force => true) # => nil
#
+ # Setting <tt>:compress</tt> will store a large cache entry set by the call
+ # in a compressed format.
+ #
+ # Setting <tt>:expires_in</tt> will set an expiration time on the cache
+ # entry if it is set by call.
+ #
+ # Setting <tt>:race_condition_ttl</tt> will invoke logic on entries set with
+ # an <tt>:expires_in</tt> option. If an entry is found in the cache that is
+ # expired and it has been expired for less than the number of seconds specified
+ # by this option and a block was passed to the method call, then the expiration
+ # future time of the entry in the cache will be updated to that many seconds
+ # in the and the block will be evaluated and written to the cache.
+ #
+ # This is very useful in situations where a cache entry is used very frequently
+ # under heavy load. The first process to find an expired cache entry will then
+ # become responsible for regenerating that entry while other processes continue
+ # to use the slightly out of date entry. This can prevent race conditions where
+ # too many processes are trying to regenerate the entry all at once. If the
+ # process regenerating the entry errors out, the entry will be regenerated
+ # after the specified number of seconds.
+ #
+ # # Set all values to expire after one minute.
+ # cache = ActiveSupport::Cache::MemoryCache.new(:expires_in => 1.minute)
+ #
+ # cache.write("foo", "original value")
+ # val_1 = nil
+ # val_2 = nil
+ # sleep 60
+ #
+ # Thread.new do
+ # val_1 = cache.fetch("foo", :race_condition_ttl => 10) do
+ # sleep 1
+ # "new value 1"
+ # end
+ # end
+ #
+ # Thread.new do
+ # val_2 = cache.fetch("foo", :race_condition_ttl => 10) do
+ # "new value 2"
+ # end
+ # end
+ #
+ # # val_1 => "new value 1"
+ # # val_2 => "original value"
+ # # cache.fetch("foo") => "new value 1"
+ #
# Other options will be handled by the specific cache store implementation.
- # Internally, #fetch calls #read, and calls #write on a cache miss.
+ # Internally, #fetch calls #read_entry, and calls #write_entry on a cache miss.
# +options+ will be passed to the #read and #write calls.
#
- # For example, MemCacheStore's #write method supports the +:expires_in+
- # option, which tells the memcached server to automatically expire the
- # cache item after a certain period. This options is also supported by
- # FileStore's #read method. We can use this option with #fetch too:
+ # For example, MemCacheStore's #write method supports the +:raw+
+ # option, which tells the memcached server to store all values as strings.
+ # We can use this option with #fetch too:
#
# cache = ActiveSupport::Cache::MemCacheStore.new
- # cache.fetch("foo", :force => true, :expires_in => 5.seconds) do
- # "bar"
+ # cache.fetch("foo", :force => true, :raw => true) do
+ # :bar
# end
# cache.fetch("foo") # => "bar"
- # sleep(6)
- # cache.fetch("foo") # => nil
- def fetch(key, options = {}, &block)
- if !options[:force] && value = read(key, options)
- value
+ def fetch(name, options = nil, &block)
+ options = merged_options(options)
+ key = namespaced_key(name, options)
+ entry = instrument(:read, name, options) { read_entry(key, options) } unless options[:force]
+ if entry && entry.expired?
+ race_ttl = options[:race_condition_ttl].to_f
+ if race_ttl and Time.now.to_f - entry.expires_at <= race_ttl
+ entry.expires_at = Time.now + race_ttl
+ write_entry(key, entry, :expires_in => race_ttl * 2)
+ else
+ delete_entry(key, options)
+ end
+ entry = nil
+ end
+
+ if entry
+ entry.value
elsif block_given?
- result = instrument(:generate, key, options, &block)
- write(key, result, options)
+ result = instrument(:generate, name, options, &block)
+ write(name, result, options)
result
end
end
@@ -182,15 +295,47 @@ module ActiveSupport
# the cache with the given key, then that data is returned. Otherwise,
# nil is returned.
#
- # You may also specify additional options via the +options+ argument.
- # The specific cache store implementation will decide what to do with
- # +options+.
+ # Options are passed to the underlying cache implementation.
+ def read(name, options = nil)
+ options = merged_options(options)
+ key = namespaced_key(name, options)
+ instrument(:read, name, options) do
+ entry = read_entry(key, options)
+ if entry
+ if entry.expired?
+ delete_entry(key, options)
+ nil
+ else
+ entry.value
+ end
+ else
+ nil
+ end
+ end
+ end
+
+ # Read multiple values at once from the cache. Options can be passed
+ # in the last argument.
#
- # For example, FileStore supports the +:expires_in+ option, which
- # makes the method return nil for cache items older than the specified
- # period.
- def read(key, options = nil, &block)
- instrument(:read, key, options, &block)
+ # Some cache implementation may optimize this method.
+ #
+ # Returns a hash mapping the names provided to the values found.
+ def read_multi(*names)
+ options = names.extract_options!
+ options = merged_options(options)
+ results = {}
+ names.each do |name|
+ key = namespaced_key(name, options)
+ entry = read_entry(key, options)
+ if entry
+ if entry.expired?
+ delete_entry(key)
+ else
+ results[name] = entry.value
+ end
+ end
+ end
+ results
end
# Writes the given value to the cache, with the given key.
@@ -198,56 +343,160 @@ module ActiveSupport
# You may also specify additional options via the +options+ argument.
# The specific cache store implementation will decide what to do with
# +options+.
+ def write(name, value, options = nil)
+ options = merged_options(options)
+ instrument(:write, name, options) do
+ entry = Entry.new(value, options)
+ write_entry(namespaced_key(name, options), entry, options)
+ end
+ end
+
+ # Delete an entry in the cache. Returns +true+ if there was an entry to delete.
#
- # For example, MemCacheStore supports the +:expires_in+ option, which
- # tells the memcached server to automatically expire the cache item after
- # a certain period:
+ # Options are passed to the underlying cache implementation.
+ def delete(name, options = nil)
+ options = merged_options(options)
+ instrument(:delete, name) do
+ delete_entry(namespaced_key(name, options), options)
+ end
+ end
+
+ # Return true if the cache contains an entry with this name.
#
- # cache = ActiveSupport::Cache::MemCacheStore.new
- # cache.write("foo", "bar", :expires_in => 5.seconds)
- # cache.read("foo") # => "bar"
- # sleep(6)
- # cache.read("foo") # => nil
- def write(key, value, options = nil, &block)
- instrument(:write, key, options, &block)
+ # Options are passed to the underlying cache implementation.
+ def exist?(name, options = nil)
+ options = merged_options(options)
+ instrument(:exist?, name) do
+ entry = read_entry(namespaced_key(name, options), options)
+ if entry && !entry.expired?
+ true
+ else
+ false
+ end
+ end
end
- def delete(key, options = nil, &block)
- instrument(:delete, key, options, &block)
+ # Delete all entries whose keys match a pattern.
+ #
+ # Options are passed to the underlying cache implementation.
+ #
+ # Not all implementations may support +delete_matched+.
+ def delete_matched(matcher, options = nil)
+ raise NotImplementedError.new("#{self.class.name} does not support delete_matched")
end
- def delete_matched(matcher, options = nil, &block)
- instrument(:delete_matched, matcher.inspect, options, &block)
+ # Increment an integer value in the cache.
+ #
+ # Options are passed to the underlying cache implementation.
+ #
+ # Not all implementations may support +delete_matched+.
+ def increment(name, amount = 1, options = nil)
+ raise NotImplementedError.new("#{self.class.name} does not support increment")
end
- def exist?(key, options = nil, &block)
- instrument(:exist?, key, options, &block)
+ # Increment an integer value in the cache.
+ #
+ # Options are passed to the underlying cache implementation.
+ #
+ # Not all implementations may support +delete_matched+.
+ def decrement(name, amount = 1, options = nil)
+ raise NotImplementedError.new("#{self.class.name} does not support decrement")
end
- def increment(key, amount = 1)
- if num = read(key)
- write(key, num + amount)
- else
- nil
- end
+ # Cleanup the cache by removing expired entries. Not all cache implementations may
+ # support this method.
+ #
+ # Options are passed to the underlying cache implementation.
+ #
+ # Not all implementations may support +delete_matched+.
+ def cleanup(options = nil)
+ raise NotImplementedError.new("#{self.class.name} does not support cleanup")
end
- def decrement(key, amount = 1)
- if num = read(key)
- write(key, num - amount)
- else
- nil
- end
+ # Clear the entire cache. Not all cache implementations may support this method.
+ # You should be careful with this method since it could affect other processes
+ # if you are using a shared cache.
+ #
+ # Options are passed to the underlying cache implementation.
+ #
+ # Not all implementations may support +delete_matched+.
+ def clear(options = nil)
+ raise NotImplementedError.new("#{self.class.name} does not support clear")
end
+ protected
+ # Add the namespace defined in the options to a pattern designed to match keys.
+ # Implementations that support delete_matched should call this method to translate
+ # a pattern that matches names into one that matches namespaced keys.
+ def key_matcher(pattern, options)
+ prefix = options[:namespace].is_a?(Proc) ? options[:namespace].call : options[:namespace]
+ if prefix
+ source = pattern.source
+ if source.start_with?('^')
+ source = source[1, source.length]
+ else
+ source = ".*#{source[0, source.length]}"
+ end
+ Regexp.new("^#{Regexp.escape(prefix)}:#{source}", pattern.options)
+ else
+ pattern
+ end
+ end
+
+ # Read an entry from the cache implementation. Subclasses must implement this method.
+ def read_entry(key, options) # :nodoc:
+ raise NotImplementedError.new
+ end
+
+ # Write an entry to the cache implementation. Subclasses must implement this method.
+ def write_entry(key, entry, options) # :nodoc:
+ raise NotImplementedError.new
+ end
+
+ # Delete an entry from the cache implementation. Subclasses must implement this method.
+ def delete_entry(key, options) # :nodoc:
+ raise NotImplementedError.new
+ end
+
private
- def expires_in(options)
- expires_in = options && options[:expires_in]
- raise ":expires_in must be a number" if expires_in && !expires_in.is_a?(Numeric)
- expires_in || 0
+ # Merge the default options with ones specific to a method call.
+ def merged_options(call_options) # :nodoc:
+ if call_options
+ options.merge(call_options)
+ else
+ options.dup
+ end
+ end
+
+ # Expand a key to be a consistent string value. If the object responds to +cache_key+,
+ # it will be called. Otherwise, the to_param method will be called. If the key is a
+ # Hash, the keys will be sorted alphabetically.
+ def expanded_key(key) # :nodoc:
+ if key.respond_to?(:cache_key)
+ key = key.cache_key.to_s
+ elsif key.is_a?(Array)
+ if key.size > 1
+ key.collect{|element| expanded_key(element)}.to_param
+ else
+ key.first.to_param
+ end
+ elsif key.is_a?(Hash)
+ key = key.to_a.sort{|a,b| a.first.to_s <=> b.first.to_s}.collect{|k,v| "#{k}=#{v}"}.to_param
+ else
+ key = key.to_param
+ end
+ end
+
+ # Prefix a key with the namespace. The two values will be delimited with a colon.
+ def namespaced_key(key, options)
+ key = expanded_key(key)
+ namespace = options[:namespace] if options
+ prefix = namespace.is_a?(Proc) ? namespace.call : namespace
+ key = "#{prefix}:#{key}" if prefix
+ key
end
- def instrument(operation, key, options)
+ def instrument(operation, key, options = nil)
log(operation, key, options)
if self.class.instrument
@@ -259,9 +508,118 @@ module ActiveSupport
end
end
- def log(operation, key, options)
- return unless logger && !silence?
- logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}")
+ def log(operation, key, options = nil)
+ return unless logger && logger.debug? && !silence?
+ logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}")
+ end
+ end
+
+ # Entry that is put into caches. It supports expiration time on entries and can compress values
+ # to save space in the cache.
+ class Entry
+ attr_reader :created_at, :expires_in
+
+ DEFAULT_COMPRESS_LIMIT = 16.kilobytes
+
+ class << self
+ # Create an entry with internal attributes set. This method is intended to be
+ # used by implementations that store cache entries in a native format instead
+ # of as serialized Ruby objects.
+ def create (raw_value, created_at, options = {})
+ entry = new(nil)
+ entry.instance_variable_set(:@value, raw_value)
+ entry.instance_variable_set(:@created_at, created_at.to_f)
+ entry.instance_variable_set(:@compressed, !!options[:compressed])
+ entry.instance_variable_set(:@expires_in, options[:expires_in])
+ entry
+ end
+ end
+
+ # Create a new cache entry for the specified value. Options supported are
+ # +:compress+, +:compress_threshold+, and +:expires_in+.
+ def initialize(value, options = {})
+ @compressed = false
+ @expires_in = options[:expires_in]
+ @expires_in = @expires_in.to_f if @expires_in
+ @created_at = Time.now.to_f
+ if value
+ if should_compress?(value, options)
+ @value = Zlib::Deflate.deflate(Marshal.dump(value))
+ @compressed = true
+ else
+ @value = value
+ end
+ else
+ @value = nil
+ end
+ end
+
+ # Get the raw value. This value may be serialized and compressed.
+ def raw_value
+ @value
+ end
+
+ # Get the value stored in the cache.
+ def value
+ if @value
+ val = compressed? ? Marshal.load(Zlib::Inflate.inflate(@value)) : @value
+ unless val.frozen?
+ val.freeze rescue nil
+ end
+ val
+ end
+ end
+
+ def compressed?
+ @compressed
+ end
+
+ # Check if the entry is expired. The +expires_in+ parameter can override the
+ # value set when the entry was created.
+ def expired?
+ if @expires_in && @created_at + @expires_in <= Time.now.to_f
+ true
+ else
+ false
+ end
+ end
+
+ # Set a new time to live on the entry so it expires at the given time.
+ def expires_at=(time)
+ if time
+ @expires_in = time.to_f - @created_at
+ else
+ @expires_in = nil
+ end
+ end
+
+ # Seconds since the epoch when the cache entry will expire.
+ def expires_at
+ @expires_in ? @created_at + @expires_in : nil
+ end
+
+ # Get the size of the cached value. This could be less than value.size
+ # if the data is compressed.
+ def size
+ if @value.nil?
+ 0
+ elsif @value.respond_to?(:bytesize)
+ @value.bytesize
+ else
+ Marshal.dump(@value).bytesize
+ end
+ end
+
+ private
+ def should_compress?(value, options)
+ if options[:compress] && value
+ unless value.is_a?(Numeric)
+ compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT
+ serialized_value = value.is_a?(String) ? value : Marshal.dump(value)
+ return true if serialized_value.size >= compress_threshold
+ end
+ end
+ false
end
end
end
diff --git a/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb b/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb
index d2370d78c5..7c7d1c4b00 100644
--- a/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb
@@ -1,21 +1,12 @@
-require 'active_support/gzip'
-
module ActiveSupport
module Cache
class CompressedMemCacheStore < MemCacheStore
- def read(name, options = nil)
- if value = super(name, (options || {}).merge(:raw => true))
- if raw?(options)
- value
- else
- Marshal.load(ActiveSupport::Gzip.decompress(value))
- end
- end
- end
-
- def write(name, value, options = nil)
- value = ActiveSupport::Gzip.compress(Marshal.dump(value)) unless raw?(options)
- super(name, value, (options || {}).merge(:raw => true))
+ def initialize(*args)
+ ActiveSupport::Deprecation.warn('ActiveSupport::Cache::CompressedMemCacheStore has been deprecated in favor of ActiveSupport::Cache::MemCacheStore(:compress => true).', caller)
+ addresses = args.dup
+ options = addresses.extract_options!
+ args = addresses + [options.merge(:compress => true)]
+ super(*args)
end
end
end
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index 7521efe7c5..fc225e77a2 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -3,73 +3,171 @@ require 'active_support/core_ext/file/atomic'
module ActiveSupport
module Cache
# A cache store implementation which stores everything on the filesystem.
+ #
+ # FileStore implements the Strategy::LocalCache strategy which implements
+ # an in memory cache inside of a block.
class FileStore < Store
attr_reader :cache_path
- def initialize(cache_path)
+ DIR_FORMATTER = "%03X"
+ ESCAPE_FILENAME_CHARS = /[^a-z0-9_.-]/i
+ UNESCAPE_FILENAME_CHARS = /%[0-9A-F]{2}/
+
+ def initialize(cache_path, options = nil)
+ super(options)
@cache_path = cache_path
+ extend Strategy::LocalCache
end
- # Reads a value from the cache.
- #
- # Possible options:
- # - +:expires_in+ - the number of seconds that this value may stay in
- # the cache.
- def read(name, options = nil)
- super do
- file_name = real_file_path(name)
- expires = expires_in(options)
-
- if File.exist?(file_name) && (expires <= 0 || Time.now - File.mtime(file_name) < expires)
- File.open(file_name, 'rb') { |f| Marshal.load(f) }
- end
+ def clear(options = nil)
+ root_dirs = Dir.entries(cache_path).reject{|f| ['.', '..'].include?(f)}
+ FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
+ end
+
+ def cleanup(options = nil)
+ options = merged_options(options)
+ each_key(options) do |key|
+ entry = read_entry(key, options)
+ delete_entry(key, options) if entry && entry.expired?
end
end
- # Writes a value to the cache.
- def write(name, value, options = nil)
- super do
- ensure_cache_path(File.dirname(real_file_path(name)))
- File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) }
- value
+ def increment(name, amount = 1, options = nil)
+ file_name = key_file_path(namespaced_key(name, options))
+ lock_file(file_name) do
+ options = merged_options(options)
+ if num = read(name, options)
+ num = num.to_i + amount
+ write(name, num, options)
+ num
+ else
+ nil
+ end
end
- rescue => e
- logger.error "Couldn't create cache directory: #{name} (#{e.message})" if logger
end
- def delete(name, options = nil)
- super do
- File.delete(real_file_path(name))
+ def decrement(name, amount = 1, options = nil)
+ file_name = key_file_path(namespaced_key(name, options))
+ lock_file(file_name) do
+ options = merged_options(options)
+ if num = read(name, options)
+ num = num.to_i - amount
+ write(name, num, options)
+ num
+ else
+ nil
+ end
end
- rescue SystemCallError => e
- # If there's no cache, then there's nothing to complain about
end
def delete_matched(matcher, options = nil)
- super do
- search_dir(@cache_path) do |f|
- if f =~ matcher
- begin
- File.delete(f)
- rescue SystemCallError => e
- # If there's no cache, then there's nothing to complain about
+ options = merged_options(options)
+ instrument(:delete_matched, matcher.inspect) do
+ matcher = key_matcher(matcher, options)
+ search_dir(cache_path) do |path|
+ key = file_path_key(path)
+ delete_entry(key, options) if key.match(matcher)
+ end
+ end
+ end
+
+ protected
+
+ def read_entry(key, options)
+ file_name = key_file_path(key)
+ if File.exist?(file_name)
+ entry = File.open(file_name) { |f| Marshal.load(f) }
+ if entry && !entry.expired? && !entry.expires_in && !self.options[:expires_in]
+ # Check for deprecated use of +:expires_in+ option from versions < 3.0
+ deprecated_expires_in = options[:expires_in]
+ if deprecated_expires_in
+ ActiveSupport::Deprecation.warn('Setting :expires_in on read has been deprecated in favor of setting it on write.', caller)
+ if entry.created_at + deprecated_expires_in.to_f <= Time.now.to_f
+ delete_entry(key, options)
+ entry = nil
+ end
end
end
+ entry
end
+ rescue
+ nil
end
- end
- def exist?(name, options = nil)
- super do
- File.exist?(real_file_path(name))
+ def write_entry(key, entry, options)
+ file_name = key_file_path(key)
+ ensure_cache_path(File.dirname(file_name))
+ File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)}
+ true
+ end
+
+ def delete_entry(key, options)
+ file_name = key_file_path(key)
+ if File.exist?(file_name)
+ begin
+ File.delete(file_name)
+ delete_empty_directories(File.dirname(file_name))
+ true
+ rescue => e
+ # Just in case the error was caused by another process deleting the file first.
+ raise e if File.exist?(file_name)
+ false
+ end
+ end
end
- end
private
- def real_file_path(name)
- '%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
+ # Lock a file for a block so only one process can modify it at a time.
+ def lock_file(file_name, &block) # :nodoc:
+ if File.exist?(file_name)
+ File.open(file_name, 'r') do |f|
+ begin
+ f.flock File::LOCK_EX
+ yield
+ ensure
+ f.flock File::LOCK_UN
+ end
+ end
+ else
+ yield
+ end
+ end
+
+ # Translate a key into a file path.
+ def key_file_path(key)
+ fname = key.to_s.gsub(ESCAPE_FILENAME_CHARS){|match| "%#{match.ord.to_s(16).upcase}"}
+ hash = Zlib.adler32(fname)
+ hash, dir_1 = hash.divmod(0x1000)
+ dir_2 = hash.modulo(0x1000)
+ fname_paths = []
+ # Make sure file name is < 255 characters so it doesn't exceed file system limits.
+ if fname.size <= 255
+ fname_paths << fname
+ else
+ while fname.size <= 255
+ fname_path << fname[0, 255]
+ fname = fname[255, -1]
+ end
+ end
+ File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths)
+ end
+
+ # Translate a file path into a key.
+ def file_path_key(path)
+ fname = path[cache_path.size, path.size].split(File::SEPARATOR, 4).last
+ fname.gsub(UNESCAPE_FILENAME_CHARS){|match| $1.ord.to_s(16)}
+ end
+
+ # Delete empty directories in the cache.
+ def delete_empty_directories(dir)
+ return if dir == cache_path
+ if Dir.entries(dir).reject{|f| ['.', '..'].include?(f)}.empty?
+ File.delete(dir) rescue nil
+ delete_empty_directories(File.dirname(dir))
+ end
end
+ # Make sure a file path's directories exist.
def ensure_cache_path(path)
FileUtils.makedirs(path) unless File.exist?(path)
end
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index c56fedc12e..d8377a208f 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -1,5 +1,5 @@
require 'memcache'
-require 'active_support/core_ext/array/extract_options'
+require 'digest/md5'
module ActiveSupport
module Cache
@@ -13,8 +13,9 @@ module ActiveSupport
# and MemCacheStore will load balance between all available servers. If a
# server goes down, then MemCacheStore will ignore it until it goes back
# online.
- # - Time-based expiry support. See #write and the <tt>:expires_in</tt> option.
- # - Per-request in memory cache for all communication with the MemCache server(s).
+ #
+ # MemCacheStore implements the Strategy::LocalCache strategy which implements
+ # an in memory cache inside of a block.
class MemCacheStore < Store
module Response # :nodoc:
STORED = "STORED\r\n"
@@ -24,6 +25,8 @@ module ActiveSupport
DELETED = "DELETED\r\n"
end
+ ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/
+
def self.build_mem_cache(*addresses)
addresses = addresses.flatten
options = addresses.extract_options!
@@ -45,108 +48,139 @@ module ActiveSupport
# require 'memcached' # gem install memcached; uses C bindings to libmemcached
# ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211"))
def initialize(*addresses)
+ addresses = addresses.flatten
+ options = addresses.extract_options!
+ super(options)
+
if addresses.first.respond_to?(:get)
@data = addresses.first
else
- @data = self.class.build_mem_cache(*addresses)
+ mem_cache_options = options.dup
+ UNIVERSAL_OPTIONS.each{|name| mem_cache_options.delete(name)}
+ @data = self.class.build_mem_cache(*(addresses + [mem_cache_options]))
end
extend Strategy::LocalCache
+ extend LocalCacheWithRaw
end
- # Reads multiple keys from the cache.
- def read_multi(*keys)
- @data.get_multi keys
- end
-
- def read(key, options = nil) # :nodoc:
- super do
- @data.get(key, raw?(options))
- end
- rescue MemCache::MemCacheError => e
- logger.error("MemCacheError (#{e}): #{e.message}") if logger
- nil
- end
-
- # Writes a value to the cache.
- #
- # Possible options:
- # - <tt>:unless_exist</tt> - set to true if you don't want to update the cache
- # if the key is already set.
- # - <tt>:expires_in</tt> - the number of seconds that this value may stay in
- # the cache. See ActiveSupport::Cache::Store#write for an example.
- def write(key, value, options = nil)
- super do
- method = options && options[:unless_exist] ? :add : :set
- # memcache-client will break the connection if you send it an integer
- # in raw mode, so we convert it to a string to be sure it continues working.
- value = value.to_s if raw?(options)
- response = @data.send(method, key, value, expires_in(options), raw?(options))
- response == Response::STORED
- end
- rescue MemCache::MemCacheError => e
- logger.error("MemCacheError (#{e}): #{e.message}") if logger
- false
- end
-
- def delete(key, options = nil) # :nodoc:
- super do
- response = @data.delete(key)
- response == Response::DELETED
- end
- rescue MemCache::MemCacheError => e
- logger.error("MemCacheError (#{e}): #{e.message}") if logger
- false
- end
-
- def exist?(key, options = nil) # :nodoc:
- # Doesn't call super, cause exist? in memcache is in fact a read
- # But who cares? Reading is very fast anyway
- # Local cache is checked first, if it doesn't know then memcache itself is read from
- super do
- !read(key, options).nil?
+ # Reads multiple keys from the cache using a single call to the
+ # servers for all keys. Options can be passed in the last argument.
+ def read_multi(*names)
+ options = names.extract_options!
+ options = merged_options(options)
+ keys_to_names = names.inject({}){|map, name| map[escape_key(namespaced_key(name, options))] = name; map}
+ raw_values = @data.get_multi(keys_to_names.keys, :raw => true)
+ values = {}
+ raw_values.each do |key, value|
+ entry = deserialize_entry(value)
+ values[keys_to_names[key]] = entry.value unless entry.expired?
end
+ values
end
- def increment(key, amount = 1) # :nodoc:
- response = instrument(:increment, key, :amount => amount) do
- @data.incr(key, amount)
+ # Increment a cached value. This method uses the memcached incr atomic
+ # operator and can only be used on values written with the :raw option.
+ # Calling it on a value not stored with :raw will initialize that value
+ # to zero.
+ def increment(name, amount = 1, options = nil) # :nodoc:
+ options = merged_options(options)
+ response = instrument(:increment, name, :amount => amount) do
+ @data.incr(escape_key(namespaced_key(name, options)), amount)
end
-
- response == Response::NOT_FOUND ? nil : response
+ response == Response::NOT_FOUND ? nil : response.to_i
rescue MemCache::MemCacheError
nil
end
- def decrement(key, amount = 1) # :nodoc:
- response = instrument(:decrement, key, :amount => amount) do
- @data.decr(key, amount)
+ # Decrement a cached value. This method uses the memcached decr atomic
+ # operator and can only be used on values written with the :raw option.
+ # Calling it on a value not stored with :raw will initialize that value
+ # to zero.
+ def decrement(name, amount = 1, options = nil) # :nodoc:
+ options = merged_options(options)
+ response = instrument(:decrement, name, :amount => amount) do
+ @data.decr(escape_key(namespaced_key(name, options)), amount)
end
-
- response == Response::NOT_FOUND ? nil : response
+ response == Response::NOT_FOUND ? nil : response.to_i
rescue MemCache::MemCacheError
nil
end
- def delete_matched(matcher, options = nil) # :nodoc:
- # don't do any local caching at present, just pass
- # through and let the error happen
- super
- raise "Not supported by Memcache"
- end
-
- def clear
+ # Clear the entire cache on all memcached servers. This method should
+ # be used with care when using a shared cache.
+ def clear(options = nil)
@data.flush_all
end
+ # Get the statistics from the memcached servers.
def stats
@data.stats
end
+ protected
+ # Read an entry from the cache.
+ def read_entry(key, options) # :nodoc:
+ deserialize_entry(@data.get(escape_key(key), true))
+ rescue MemCache::MemCacheError => e
+ logger.error("MemCacheError (#{e}): #{e.message}") if logger
+ nil
+ end
+
+ # Write an entry to the cache.
+ def write_entry(key, entry, options) # :nodoc:
+ method = options && options[:unless_exist] ? :add : :set
+ value = options[:raw] ? entry.value.to_s : entry
+ expires_in = options[:expires_in].to_i
+ if expires_in > 0 && !options[:raw]
+ # Set the memcache expire a few minutes in the future to support race condition ttls on read
+ expires_in += 5.minutes
+ end
+ response = @data.send(method, escape_key(key), value, expires_in, options[:raw])
+ response == Response::STORED
+ rescue MemCache::MemCacheError => e
+ logger.error("MemCacheError (#{e}): #{e.message}") if logger
+ false
+ end
+
+ # Delete an entry from the cache.
+ def delete_entry(key, options) # :nodoc:
+ response = @data.delete(escape_key(key))
+ response == Response::DELETED
+ rescue MemCache::MemCacheError => e
+ logger.error("MemCacheError (#{e}): #{e.message}") if logger
+ false
+ end
+
private
- def raw?(options)
- options && options[:raw]
+ def escape_key(key)
+ key = key.to_s.gsub(ESCAPE_KEY_CHARS){|match| "%#{match[0].to_s(16).upcase}"}
+ key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
+ key
end
+
+ def deserialize_entry(raw_value)
+ if raw_value
+ entry = Marshal.load(raw_value) rescue raw_value
+ entry.is_a?(Entry) ? entry : Entry.new(entry)
+ else
+ nil
+ end
+ end
+
+ # Provide support for raw values in the local cache strategy.
+ module LocalCacheWithRaw # :nodoc:
+ protected
+ def write_entry(key, entry, options) # :nodoc:
+ retval = super
+ if options[:raw] && local_cache && retval
+ raw_entry = Entry.new(entry.value.to_s)
+ raw_entry.expires_at = entry.expires_at
+ local_cache.write_entry(key, raw_entry, options)
+ end
+ retval
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb
index 379922f986..b1d14a0d8f 100644
--- a/activesupport/lib/active_support/cache/memory_store.rb
+++ b/activesupport/lib/active_support/cache/memory_store.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/object/duplicable'
+require 'monitor'
module ActiveSupport
module Cache
@@ -6,60 +6,154 @@ module ActiveSupport
# same process. If you're running multiple Ruby on Rails server processes
# (which is the case if you're using mongrel_cluster or Phusion Passenger),
# then this means that your Rails server process instances won't be able
- # to share cache data with each other. If your application never performs
- # manual cache item expiry (e.g. when you're using generational cache keys),
- # then using MemoryStore is ok. Otherwise, consider carefully whether you
- # should be using this cache store.
+ # to share cache data with each other and this may not be the most
+ # appropriate cache for you.
#
- # MemoryStore is not only able to store strings, but also arbitrary Ruby
- # objects.
+ # This cache has a bounded size specified by the :size options to the
+ # initializer (default is 32Mb). When the cache exceeds the alotted size,
+ # a cleanup will occur which tries to prune the cache down to three quarters
+ # of the maximum size by removing the least recently used entries.
#
- # MemoryStore is not thread-safe. Use SynchronizedMemoryStore instead
- # if you need thread-safety.
+ # MemoryStore is thread-safe.
class MemoryStore < Store
- def initialize
+ def initialize(options = nil)
+ options ||= {}
+ super(options)
@data = {}
+ @key_access = {}
+ @max_size = options[:size] || 32.megabytes
+ @max_prune_time = options[:max_prune_time] || 2
+ @cache_size = 0
+ @monitor = Monitor.new
+ @pruning = false
end
- def read_multi(*names)
- results = {}
- names.each { |n| results[n] = read(n) }
- results
+ def clear(options = nil)
+ synchronize do
+ @data.clear
+ @key_access.clear
+ @cache_size = 0
+ end
end
- def read(name, options = nil)
- super do
- @data[name]
+ def cleanup(options = nil)
+ options = merged_options(options)
+ instrument(:cleanup, :size => @data.size) do
+ keys = synchronize{ @data.keys }
+ keys.each do |key|
+ entry = @data[key]
+ delete_entry(key, options) if entry && entry.expired?
+ end
end
end
- def write(name, value, options = nil)
- super do
- @data[name] = (value.duplicable? ? value.dup : value).freeze
+ # Prune the cache down so the entries fit within the specified memory size by removing
+ # the least recently accessed entries.
+ def prune(target_size, max_time = nil)
+ return if pruning?
+ @pruning = true
+ begin
+ start_time = Time.now
+ cleanup
+ instrument(:prune, target_size, :from => @cache_size) do
+ keys = synchronize{ @key_access.keys.sort{|a,b| @key_access[a].to_f <=> @key_access[b].to_f} }
+ keys.each do |key|
+ delete_entry(key, options)
+ return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time)
+ end
+ end
+ ensure
+ @pruning = false
end
end
- def delete(name, options = nil)
- super do
- @data.delete(name)
+ # Return true if the cache is currently be pruned to remove older entries.
+ def pruning?
+ @pruning
+ end
+
+ # Increment an integer value in the cache.
+ def increment(name, amount = 1, options = nil)
+ synchronize do
+ options = merged_options(options)
+ if num = read(name, options)
+ num = num.to_i + amount
+ write(name, num, options)
+ num
+ else
+ nil
+ end
end
end
- def delete_matched(matcher, options = nil)
- super do
- @data.delete_if { |k,v| k =~ matcher }
+ # Decrement an integer value in the cache.
+ def decrement(name, amount = 1, options = nil)
+ synchronize do
+ options = merged_options(options)
+ if num = read(name, options)
+ num = num.to_i - amount
+ write(name, num, options)
+ num
+ else
+ nil
+ end
end
end
- def exist?(name, options = nil)
- super do
- @data.has_key?(name)
+ def delete_matched(matcher, options = nil)
+ options = merged_options(options)
+ instrument(:delete_matched, matcher.inspect) do
+ matcher = key_matcher(matcher, options)
+ keys = synchronize { @data.keys }
+ keys.each do |key|
+ delete_entry(key, options) if key.match(matcher)
+ end
end
end
- def clear
- @data.clear
+ def inspect # :nodoc:
+ "<##{self.class.name} entries=#{@data.size}, size=#{@cache_size}, options=#{@options.inspect}>"
+ end
+
+ # Synchronize calls to the cache. This should be called wherever the underlying cache implementation
+ # is not thread safe.
+ def synchronize(&block) # :nodoc:
+ @monitor.synchronize(&block)
end
+
+ protected
+ def read_entry(key, options) # :nodoc:
+ entry = @data[key]
+ synchronize do
+ if entry
+ @key_access[key] = Time.now.to_f
+ else
+ @key_access.delete(key)
+ end
+ end
+ entry
+ end
+
+ def write_entry(key, entry, options) # :nodoc:
+ synchronize do
+ old_entry = @data[key]
+ @cache_size -= old_entry.size if old_entry
+ @cache_size += entry.size
+ @key_access[key] = Time.now.to_f
+ @data[key] = entry
+ prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
+ true
+ end
+ end
+
+ def delete_entry(key, options) # :nodoc:
+ synchronize do
+ @key_access.delete(key)
+ entry = @data.delete(key)
+ @cache_size -= entry.size if entry
+ !!entry
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index bbbd643736..efb5ad26ab 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -4,108 +4,160 @@ require 'active_support/core_ext/string/inflections'
module ActiveSupport
module Cache
module Strategy
+ # Caches that implement LocalCache will be backed by an in memory cache for the
+ # duration of a block. Repeated calls to the cache for the same key will hit the
+ # in memory cache for faster access.
module LocalCache
- # this allows caching of the fact that there is nothing in the remote cache
- NULL = 'remote_cache_store:null'
+ # Simple memory backed cache. This cache is not thread safe but is intended only
+ # for serving as a temporary memory cache for a single thread.
+ class LocalStore < Store
+ def initialize
+ super
+ @data = {}
+ end
+
+ # Since it isn't thread safe, don't allow synchronizing.
+ def synchronize # :nodoc:
+ yield
+ end
+
+ def clear(options = nil)
+ @data.clear
+ end
+
+ def read_entry(key, options)
+ @data[key]
+ end
+
+ def write_entry(key, value, options)
+ @data[key] = value
+ true
+ end
+ def delete_entry(key, options)
+ !!@data.delete(key)
+ end
+ end
+
+ # Use a local cache to front for the cache for the duration of a block.
def with_local_cache
- Thread.current[thread_local_key] = MemoryStore.new
- yield
- ensure
- Thread.current[thread_local_key] = nil
+ save_val = Thread.current[thread_local_key]
+ begin
+ Thread.current[thread_local_key] = LocalStore.new
+ yield
+ ensure
+ Thread.current[thread_local_key] = save_val
+ end
end
+ # Middleware class can be inserted as a Rack handler to use a local cache for the
+ # duration of a request.
def middleware
@middleware ||= begin
klass = Class.new
klass.class_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ class << self
+ def name
+ "ActiveSupport::Cache::Strategy::LocalCache"
+ end
+ alias :to_s :name
+ end
+
def initialize(app)
@app = app
end
def call(env)
- Thread.current[:#{thread_local_key}] = MemoryStore.new
+ Thread.current[:#{thread_local_key}] = LocalStore.new
@app.call(env)
ensure
Thread.current[:#{thread_local_key}] = nil
end
EOS
-
- def klass.to_s
- "ActiveSupport::Cache::Strategy::LocalCache"
- end
-
klass
end
end
- def read(key, options = nil)
- value = local_cache && local_cache.read(key)
- if value == NULL
- nil
- elsif value.nil?
- value = super
- local_cache.mute { local_cache.write(key, value || NULL) } if local_cache
- value.duplicable? ? value.dup : value
- else
- # forcing the value to be immutable
- value.duplicable? ? value.dup : value
- end
- end
-
- def write(key, value, options = nil)
- value = value.to_s if respond_to?(:raw?) && raw?(options)
- local_cache.mute { local_cache.write(key, value || NULL) } if local_cache
+ def clear(options = nil) # :nodoc:
+ local_cache.clear(options) if local_cache
super
end
- def delete(key, options = nil)
- local_cache.mute { local_cache.write(key, NULL) } if local_cache
+ def cleanup(options = nil) # :nodoc:
+ local_cache.clear(options) if local_cache
super
end
- def exist(key, options = nil)
- value = local_cache.read(key) if local_cache
- if value == NULL
- false
- elsif value
- true
- else
- super
+ def increment(name, amount = 1, options = nil) # :nodoc:
+ value = bypass_local_cache{super}
+ if local_cache
+ local_cache.mute do
+ if value
+ local_cache.write(name, value, options)
+ else
+ local_cache.delete(name, options)
+ end
+ end
end
+ value
end
- def increment(key, amount = 1)
- if value = super
- local_cache.mute { local_cache.write(key, value.to_s) } if local_cache
- value
- else
- nil
+ def decrement(name, amount = 1, options = nil) # :nodoc:
+ value = bypass_local_cache{super}
+ if local_cache
+ local_cache.mute do
+ if value
+ local_cache.write(name, value, options)
+ else
+ local_cache.delete(name, options)
+ end
+ end
end
+ value
end
- def decrement(key, amount = 1)
- if value = super
- local_cache.mute { local_cache.write(key, value.to_s) } if local_cache
- value
- else
- nil
+ protected
+ def read_entry(key, options) # :nodoc:
+ if local_cache
+ entry = local_cache.read_entry(key, options)
+ unless entry
+ entry = super
+ local_cache.write_entry(key, entry, options)
+ end
+ entry
+ else
+ super
+ end
end
- end
- def clear
- local_cache.clear if local_cache
- super
- end
+ def write_entry(key, entry, options) # :nodoc:
+ local_cache.write_entry(key, entry, options) if local_cache
+ super
+ end
+
+ def delete_entry(key, options) # :nodoc:
+ local_cache.delete_entry(key, options) if local_cache
+ super
+ end
private
def thread_local_key
- @thread_local_key ||= "#{self.class.name.underscore}_local_cache".gsub("/", "_").to_sym
+ @thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
end
def local_cache
Thread.current[thread_local_key]
end
+
+ def bypass_local_cache
+ save_cache = Thread.current[thread_local_key]
+ begin
+ Thread.current[thread_local_key] = nil
+ yield
+ ensure
+ Thread.current[thread_local_key] = save_cache
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/cache/synchronized_memory_store.rb b/activesupport/lib/active_support/cache/synchronized_memory_store.rb
index ea03a119c6..37caa6b6f1 100644
--- a/activesupport/lib/active_support/cache/synchronized_memory_store.rb
+++ b/activesupport/lib/active_support/cache/synchronized_memory_store.rb
@@ -2,45 +2,9 @@ module ActiveSupport
module Cache
# Like MemoryStore, but thread-safe.
class SynchronizedMemoryStore < MemoryStore
- def initialize
+ def initialize(*args)
+ ActiveSupport::Deprecation.warn('ActiveSupport::Cache::SynchronizedMemoryStore has been deprecated in favor of ActiveSupport::Cache::MemoryStore.', caller)
super
- @guard = Monitor.new
- end
-
- def fetch(key, options = {})
- @guard.synchronize { super }
- end
-
- def read(name, options = nil)
- @guard.synchronize { super }
- end
-
- def write(name, value, options = nil)
- @guard.synchronize { super }
- end
-
- def delete(name, options = nil)
- @guard.synchronize { super }
- end
-
- def delete_matched(matcher, options = nil)
- @guard.synchronize { super }
- end
-
- def exist?(name,options = nil)
- @guard.synchronize { super }
- end
-
- def increment(key, amount = 1)
- @guard.synchronize { super }
- end
-
- def decrement(key, amount = 1)
- @guard.synchronize { super }
- end
-
- def clear
- @guard.synchronize { super }
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index 5d8e78e6e5..2b07f05d27 100644
--- a/activesupport/lib/active_support/core_ext/array/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -1,6 +1,7 @@
+require 'active_support/xml_mini'
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/reverse_merge'
-require 'active_support/inflector'
+require 'active_support/core_ext/string/inflections'
class Array
# Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
@@ -127,34 +128,31 @@ class Array
# </messages>
#
def to_xml(options = {})
- raise "Not all elements respond to to_xml" unless all? { |e| e.respond_to? :to_xml }
require 'builder' unless defined?(Builder)
options = options.dup
- options[:root] ||= all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ? ActiveSupport::Inflector.pluralize(ActiveSupport::Inflector.underscore(first.class.name)).tr('/', '_') : "records"
- options[:children] ||= options[:root].singularize
- options[:indent] ||= 2
- options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
-
- root = options.delete(:root).to_s
- children = options.delete(:children)
-
- if !options.has_key?(:dasherize) || options[:dasherize]
- root = root.dasherize
+ options[:indent] ||= 2
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+ options[:root] ||= if first.class.to_s != "Hash" && all? { |e| e.is_a?(first.class) }
+ underscored = ActiveSupport::Inflector.underscore(first.class.name)
+ ActiveSupport::Inflector.pluralize(underscored).tr('/', '_')
+ else
+ "objects"
end
- options[:builder].instruct! unless options.delete(:skip_instruct)
+ builder = options[:builder]
+ builder.instruct! unless options.delete(:skip_instruct)
- opts = options.merge({ :root => children })
+ root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
+ children = options.delete(:children) || root.singularize
- xml = options[:builder]
- if empty?
- xml.tag!(root, options[:skip_types] ? {} : {:type => "array"})
- else
- xml.tag!(root, options[:skip_types] ? {} : {:type => "array"}) {
- yield xml if block_given?
- each { |e| e.to_xml(opts.merge({ :skip_instruct => true })) }
- }
+ attributes = options[:skip_types] ? {} : {:type => "array"}
+ return builder.tag!(root, attributes) if empty?
+
+ builder.__send__(:method_missing, root, attributes) do
+ each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) }
+ yield builder if block_given?
end
end
+
end
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index c882434f78..14e5d2f8ac 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -1,3 +1,4 @@
+require 'active_support/xml_mini'
require 'active_support/time'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/hash/reverse_merge'
@@ -5,79 +6,6 @@ require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/inflections'
class Hash
- # This module exists to decorate files deserialized using Hash.from_xml with
- # the <tt>original_filename</tt> and <tt>content_type</tt> methods.
- module FileLike #:nodoc:
- attr_writer :original_filename, :content_type
-
- def original_filename
- @original_filename || 'untitled'
- end
-
- def content_type
- @content_type || 'application/octet-stream'
- end
- end
-
- XML_TYPE_NAMES = {
- "Symbol" => "symbol",
- "Fixnum" => "integer",
- "Bignum" => "integer",
- "BigDecimal" => "decimal",
- "Float" => "float",
- "TrueClass" => "boolean",
- "FalseClass" => "boolean",
- "Date" => "date",
- "DateTime" => "datetime",
- "Time" => "datetime"
- } unless defined?(XML_TYPE_NAMES)
-
- XML_FORMATTING = {
- "symbol" => Proc.new { |symbol| symbol.to_s },
- "date" => Proc.new { |date| date.to_s(:db) },
- "datetime" => Proc.new { |time| time.xmlschema },
- "binary" => Proc.new { |binary| ActiveSupport::Base64.encode64(binary) },
- "yaml" => Proc.new { |yaml| yaml.to_yaml }
- } unless defined?(XML_FORMATTING)
-
- # TODO: use Time.xmlschema instead of Time.parse;
- # use regexp instead of Date.parse
- unless defined?(XML_PARSING)
- XML_PARSING = {
- "symbol" => Proc.new { |symbol| symbol.to_sym },
- "date" => Proc.new { |date| ::Date.parse(date) },
- "datetime" => Proc.new { |time| ::Time.parse(time).utc rescue ::DateTime.parse(time).utc },
- "integer" => Proc.new { |integer| integer.to_i },
- "float" => Proc.new { |float| float.to_f },
- "decimal" => Proc.new { |number| BigDecimal(number) },
- "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) },
- "string" => Proc.new { |string| string.to_s },
- "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
- "base64Binary" => Proc.new { |bin| ActiveSupport::Base64.decode64(bin) },
- "binary" => Proc.new do |bin, entity|
- case entity['encoding']
- when 'base64'
- ActiveSupport::Base64.decode64(bin)
- # TODO: Add support for other encodings
- else
- bin
- end
- end,
- "file" => Proc.new do |file, entity|
- f = StringIO.new(ActiveSupport::Base64.decode64(file))
- f.extend(FileLike)
- f.original_filename = entity['name']
- f.content_type = entity['content_type']
- f
- end
- }
-
- XML_PARSING.update(
- "double" => XML_PARSING["float"],
- "dateTime" => XML_PARSING["datetime"]
- )
- end
-
# Returns a string containing an XML representation of its receiver:
#
# {"foo" => 1, "bar" => 2}.to_xml
@@ -130,62 +58,19 @@ class Hash
require 'builder' unless defined?(Builder)
options = options.dup
- options[:indent] ||= 2
- options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]),
- :root => "hash" })
- options[:builder].instruct! unless options.delete(:skip_instruct)
- root = rename_key(options[:root].to_s, options)
+ options[:indent] ||= 2
+ options[:root] ||= "hash"
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
- options[:builder].__send__(:method_missing, root) do
- each do |key, value|
- case value
- when ::Hash
- value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
- when ::Array
- value.to_xml(options.merge({ :root => key, :children => key.to_s.singularize, :skip_instruct => true}))
- when ::Method, ::Proc
- # If the Method or Proc takes two arguments, then
- # pass the suggested child element name. This is
- # used if the Method or Proc will be operating over
- # multiple records and needs to create an containing
- # element that will contain the objects being
- # serialized.
- if 1 == value.arity
- value.call(options.merge({ :root => key, :skip_instruct => true }))
- else
- value.call(options.merge({ :root => key, :skip_instruct => true }), key.to_s.singularize)
- end
- else
- if value.respond_to?(:to_xml)
- value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
- else
- type_name = XML_TYPE_NAMES[value.class.name]
+ builder = options[:builder]
+ builder.instruct! unless options.delete(:skip_instruct)
- key = rename_key(key.to_s, options)
-
- attributes = options[:skip_types] || value.nil? || type_name.nil? ? { } : { :type => type_name }
- if value.nil?
- attributes[:nil] = true
- end
+ root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
- options[:builder].tag!(key,
- XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value,
- attributes
- )
- end
- end
- end
-
- yield options[:builder] if block_given?
+ builder.__send__(:method_missing, root) do
+ each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options) }
+ yield builder if block_given?
end
-
- end
-
- def rename_key(key, options = {})
- camelize = options.has_key?(:camelize) && options[:camelize]
- dasherize = !options.has_key?(:dasherize) || options[:dasherize]
- key = key.camelize if camelize
- dasherize ? key.dasherize : key
end
class << self
@@ -213,12 +98,8 @@ class Hash
end
elsif value.has_key?("__content__")
content = value["__content__"]
- if parser = XML_PARSING[value["type"]]
- if parser.arity == 2
- XML_PARSING[value["type"]].call(content, value)
- else
- XML_PARSING[value["type"]].call(content)
- end
+ if parser = ActiveSupport::XmlMini::PARSING[value["type"]]
+ parser.arity == 1 ? parser.call(content) : parser.call(content, value)
else
content
end
@@ -244,11 +125,7 @@ class Hash
end
when 'Array'
value.map! { |i| typecast_xml_value(i) }
- case value.length
- when 0 then nil
- when 1 then value.first
- else value
- end
+ value.length > 1 ? value : value.first
when 'String'
value
else
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index 9c99dcfb01..5ec87372d0 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -3,45 +3,62 @@ require 'active_support/core_ext/string/multibyte'
module ActiveSupport
module Inflector
- extend self
- # UTF-8 byte => ASCII approximate UTF-8 byte(s)
- ASCII_APPROXIMATIONS = {
- 198 => [65, 69], # Æ => AE
- 208 => 68, # Ð => D
- 216 => 79, # Ø => O
- 222 => [84, 104], # Þ => Þ
- 223 => [115, 115], # ß => ss
- 230 => [97, 101], # æ => ae
- 240 => 100, # ð => d
- 248 => 111, # ø => o
- 254 => [116, 104], # þ => th
- 272 => 68, # Đ => D
- 273 => 100, # đ => đ
- 294 => 72, # Ħ => H
- 295 => 104, # ħ => h
- 305 => 105, # ı => i
- 306 => [73, 74], # IJ =>IJ
- 307 => [105, 106], # ij => ij
- 312 => 107, # ĸ => k
- 319 => 76, # Ŀ => L
- 320 => 108, # ŀ => l
- 321 => 76, # Ł => L
- 322 => 108, # ł => l
- 329 => 110, # ʼn => n
- 330 => [78, 71], # Ŋ => NG
- 331 => [110, 103], # ŋ => ng
- 338 => [79, 69], # Π=> OE
- 339 => [111, 101], # œ => oe
- 358 => 84, # Ŧ => T
- 359 => 116 # ŧ => t
- }
-
- # Replaces accented characters with an ASCII approximation, or deletes it if none exsits.
- def transliterate(string)
- ActiveSupport::Multibyte::Chars.new(string).tidy_bytes.normalize(:d).unpack("U*").map do |char|
- ASCII_APPROXIMATIONS[char] || (char if char < 128)
- end.compact.flatten.pack("U*")
+ # Replaces non-ASCII characters with an ASCII approximation, or if none
+ # exists, a replacement character which defaults to "?".
+ #
+ # transliterate("Ærøskøbing")
+ # # => "AEroskobing"
+ #
+ # Default approximations are provided for Western/Latin characters,
+ # e.g, "ø", "ñ", "é", "ß", etc.
+ #
+ # This method is I18n aware, so you can set up custom approximations for a
+ # locale. This can be useful, for example, to transliterate German's "ü"
+ # and "ö" to "ue" and "oe", or to add support for transliterating Russian
+ # to ASCII.
+ #
+ # In order to make your custom transliterations available, you must set
+ # them as the <tt>i18n.transliterate.rule</tt> i18n key:
+ #
+ # # Store the transliterations in locales/de.yml
+ # i18n:
+ # transliterate:
+ # ü: "ue"
+ # ö: "oe"
+ #
+ # # Or set them using Ruby
+ # I18n.backend.store_translations(:de, :i18n => {
+ # :transliterate => {
+ # :rule => {
+ # "ü" => "ue",
+ # "ö" => "oe"
+ # }
+ # }
+ # })
+ #
+ # The value for <tt>i18n.transliterate.rule</tt> can be a simple Hash that maps
+ # characters to ASCII approximations as shown above, or, for more complex
+ # requirements, a Proc:
+ #
+ # I18n.backend.store_translations(:de, :i18n => {
+ # :transliterate => {
+ # :rule => lambda {|string| MyTransliterator.transliterate(string)}
+ # }
+ # })
+ #
+ # Now you can have different transliterations for each locale:
+ #
+ # I18n.locale = :en
+ # transliterate("Jürgen")
+ # # => "Jurgen"
+ #
+ # I18n.locale = :de
+ # transliterate("Jürgen")
+ # # => "Juergen"
+ def transliterate(string, replacement = "?")
+ I18n.transliterate(Multibyte::Chars.normalize(
+ Multibyte::Chars.tidy_bytes(string), :c), :replacement => replacement)
end
# Replaces special characters in a string so that it may be used as part of a 'pretty' URL.
@@ -73,5 +90,6 @@ module ActiveSupport
end
parameterized_string.downcase
end
+
end
end
diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb
index 0f38fd0e89..e692f6d142 100644
--- a/activesupport/lib/active_support/json/encoding.rb
+++ b/activesupport/lib/active_support/json/encoding.rb
@@ -104,7 +104,7 @@ module ActiveSupport
def escape(string)
if string.respond_to?(:force_encoding)
- string = string.encode(::Encoding::UTF_8, undef: :replace).force_encoding(::Encoding::BINARY)
+ string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY)
end
json = string.
gsub(escape_regex) { |s| ESCAPED_CHARS[s] }.
diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb
index 4ade1158fd..cca30d1141 100644
--- a/activesupport/lib/active_support/multibyte/chars.rb
+++ b/activesupport/lib/active_support/multibyte/chars.rb
@@ -75,8 +75,6 @@ module ActiveSupport #:nodoc:
UNICODE_TRAILERS_PAT = /(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+\Z/u
UNICODE_LEADERS_PAT = /\A(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+/u
- UTF8_PAT = ActiveSupport::Multibyte::VALID_CHARACTER['UTF-8']
-
attr_reader :wrapped_string
alias to_s wrapped_string
alias to_str wrapped_string
@@ -409,25 +407,11 @@ module ActiveSupport #:nodoc:
# Returns the KC normalization of the string by default. NFKC is considered the best normalization form for
# passing strings to databases and validations.
#
- # * <tt>str</tt> - The string to perform normalization on.
# * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
# <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
# ActiveSupport::Multibyte.default_normalization_form
def normalize(form=ActiveSupport::Multibyte.default_normalization_form)
- # See http://www.unicode.org/reports/tr15, Table 1
- codepoints = self.class.u_unpack(@wrapped_string)
- chars(case form
- when :d
- self.class.reorder_characters(self.class.decompose_codepoints(:canonical, codepoints))
- when :c
- self.class.compose_codepoints(self.class.reorder_characters(self.class.decompose_codepoints(:canonical, codepoints)))
- when :kd
- self.class.reorder_characters(self.class.decompose_codepoints(:compatability, codepoints))
- when :kc
- self.class.compose_codepoints(self.class.reorder_characters(self.class.decompose_codepoints(:compatability, codepoints)))
- else
- raise ArgumentError, "#{form} is not a valid normalization variant", caller
- end.pack('U*'))
+ chars(self.class.normalize(@wrapped_string, form))
end
# Performs canonical decomposition on all the characters.
@@ -659,7 +643,7 @@ module ActiveSupport #:nodoc:
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string.
#
- # Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP-1252 or ISO-8859-1.
+ # Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP1252 or ISO-8859-1.
def tidy_bytes(string, force = false)
if force
return string.unpack("C*").map do |b|
@@ -708,6 +692,31 @@ module ActiveSupport #:nodoc:
end
bytes.empty? ? "" : bytes.flatten.compact.pack("C*").unpack("U*").pack("U*")
end
+
+ # Returns the KC normalization of the string by default. NFKC is considered the best normalization form for
+ # passing strings to databases and validations.
+ #
+ # * <tt>string</tt> - The string to perform normalization on.
+ # * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
+ # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
+ # ActiveSupport::Multibyte.default_normalization_form
+ def normalize(string, form=ActiveSupport::Multibyte.default_normalization_form)
+ # See http://www.unicode.org/reports/tr15, Table 1
+ codepoints = u_unpack(string)
+ case form
+ when :d
+ reorder_characters(decompose_codepoints(:canonical, codepoints))
+ when :c
+ compose_codepoints(reorder_characters(decompose_codepoints(:canonical, codepoints)))
+ when :kd
+ reorder_characters(decompose_codepoints(:compatability, codepoints))
+ when :kc
+ compose_codepoints(reorder_characters(decompose_codepoints(:compatability, codepoints)))
+ else
+ raise ArgumentError, "#{form} is not a valid normalization variant", caller
+ end.pack('U*')
+ end
+
end
protected
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index f22fbcc0e1..7594d7b68b 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -9,6 +9,71 @@ module ActiveSupport
module XmlMini
extend self
+ # This module exists to decorate files deserialized using Hash.from_xml with
+ # the <tt>original_filename</tt> and <tt>content_type</tt> methods.
+ module FileLike #:nodoc:
+ attr_writer :original_filename, :content_type
+
+ def original_filename
+ @original_filename || 'untitled'
+ end
+
+ def content_type
+ @content_type || 'application/octet-stream'
+ end
+ end
+
+ DEFAULT_ENCODINGS = {
+ "binary" => "base64"
+ } unless defined?(TYPE_NAMES)
+
+ TYPE_NAMES = {
+ "Symbol" => "symbol",
+ "Fixnum" => "integer",
+ "Bignum" => "integer",
+ "BigDecimal" => "decimal",
+ "Float" => "float",
+ "TrueClass" => "boolean",
+ "FalseClass" => "boolean",
+ "Date" => "date",
+ "DateTime" => "datetime",
+ "Time" => "datetime",
+ "Array" => "array",
+ "Hash" => "hash"
+ } unless defined?(TYPE_NAMES)
+
+ FORMATTING = {
+ "symbol" => Proc.new { |symbol| symbol.to_s },
+ "date" => Proc.new { |date| date.to_s(:db) },
+ "datetime" => Proc.new { |time| time.xmlschema },
+ "binary" => Proc.new { |binary| ActiveSupport::Base64.encode64(binary) },
+ "yaml" => Proc.new { |yaml| yaml.to_yaml }
+ } unless defined?(FORMATTING)
+
+ # TODO: use Time.xmlschema instead of Time.parse;
+ # use regexp instead of Date.parse
+ unless defined?(PARSING)
+ PARSING = {
+ "symbol" => Proc.new { |symbol| symbol.to_sym },
+ "date" => Proc.new { |date| ::Date.parse(date) },
+ "datetime" => Proc.new { |time| ::Time.parse(time).utc rescue ::DateTime.parse(time).utc },
+ "integer" => Proc.new { |integer| integer.to_i },
+ "float" => Proc.new { |float| float.to_f },
+ "decimal" => Proc.new { |number| BigDecimal(number) },
+ "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) },
+ "string" => Proc.new { |string| string.to_s },
+ "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
+ "base64Binary" => Proc.new { |bin| ActiveSupport::Base64.decode64(bin) },
+ "binary" => Proc.new { |bin, entity| _parse_binary(bin, entity) },
+ "file" => Proc.new { |file, entity| _parse_file(file, entity) }
+ }
+
+ PARSING.update(
+ "double" => PARSING["float"],
+ "dateTime" => PARSING["datetime"]
+ )
+ end
+
attr_reader :backend
delegate :parse, :to => :backend
@@ -16,7 +81,7 @@ module ActiveSupport
if name.is_a?(Module)
@backend = name
else
- require "active_support/xml_mini/#{name.to_s.downcase}.rb"
+ require "active_support/xml_mini/#{name.to_s.downcase}"
@backend = ActiveSupport.const_get("XmlMini_#{name}")
end
end
@@ -27,6 +92,66 @@ module ActiveSupport
ensure
self.backend = old_backend
end
+
+ def to_tag(key, value, options)
+ type_name = options.delete(:type)
+ merged_options = options.merge(:root => key, :skip_instruct => true)
+
+ if value.is_a?(::Method) || value.is_a?(::Proc)
+ if value.arity == 1
+ value.call(merged_options)
+ else
+ value.call(merged_options, key.to_s.singularize)
+ end
+ elsif value.respond_to?(:to_xml)
+ value.to_xml(merged_options)
+ else
+ type_name ||= TYPE_NAMES[value.class.name]
+ type_name ||= value.class.name if value && !value.respond_to?(:to_str)
+ type_name = type_name.to_s if type_name
+
+ key = rename_key(key.to_s, options)
+
+ attributes = options[:skip_types] || type_name.nil? ? { } : { :type => type_name }
+ attributes[:nil] = true if value.nil?
+
+ encoding = options[:encoding] || DEFAULT_ENCODINGS[type_name]
+ attributes[:encoding] = encoding if encoding
+
+ formatted_value = FORMATTING[type_name] && !value.nil? ?
+ FORMATTING[type_name].call(value) : value
+
+ options[:builder].tag!(key, formatted_value, attributes)
+ end
+ end
+
+ def rename_key(key, options = {})
+ camelize = options.has_key?(:camelize) && options[:camelize]
+ dasherize = !options.has_key?(:dasherize) || options[:dasherize]
+ key = key.camelize if camelize
+ key = key.dasherize if dasherize
+ key
+ end
+
+ protected
+
+ # TODO: Add support for other encodings
+ def _parse_binary(bin, entity) #:nodoc:
+ case entity['encoding']
+ when 'base64'
+ ActiveSupport::Base64.decode64(bin)
+ else
+ bin
+ end
+ end
+
+ def _parse_file(file, entity)
+ f = StringIO.new(ActiveSupport::Base64.decode64(file))
+ f.extend(FileLike)
+ f.original_filename = entity['name']
+ f.content_type = entity['content_type']
+ f
+ end
end
XmlMini.backend = 'REXML'
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index e62e7ef9aa..d9ff1207e7 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -4,6 +4,7 @@ require 'active_support/cache'
class CacheKeyTest < ActiveSupport::TestCase
def test_expand_cache_key
+ assert_equal '1/2/true', ActiveSupport::Cache.expand_cache_key([1, '2', true])
assert_equal 'name/1/2/true', ActiveSupport::Cache.expand_cache_key([1, '2', true], :name)
end
end
@@ -43,9 +44,10 @@ class CacheStoreSettingTest < ActiveSupport::TestCase
end
def test_mem_cache_fragment_cache_store_with_options
- MemCache.expects(:new).with(%w[localhost 192.168.1.1], { :namespace => "foo" })
- store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo'
+ MemCache.expects(:new).with(%w[localhost 192.168.1.1], { :timeout => 10 })
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo', :timeout => 10
assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
+ assert_equal 'foo', store.options[:namespace]
end
def test_object_assigned_fragment_cache_store
@@ -55,124 +57,170 @@ class CacheStoreSettingTest < ActiveSupport::TestCase
end
end
-class CacheStoreTest < ActiveSupport::TestCase
- def setup
- @cache = ActiveSupport::Cache.lookup_store(:memory_store)
+class CacheStoreNamespaceTest < ActiveSupport::TestCase
+ def test_static_namespace
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "tester")
+ cache.write("foo", "bar")
+ assert_equal "bar", cache.read("foo")
+ assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value
+ end
+
+ def test_proc_namespace
+ test_val = "tester"
+ proc = lambda{test_val}
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => proc)
+ cache.write("foo", "bar")
+ assert_equal "bar", cache.read("foo")
+ assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value
+ end
+
+ def test_delete_matched_key_start
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "tester")
+ cache.write("foo", "bar")
+ cache.write("fu", "baz")
+ cache.delete_matched(/^fo/)
+ assert_equal false, cache.exist?("foo")
+ assert_equal true, cache.exist?("fu")
+ end
+
+ def test_delete_matched_key
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "foo")
+ cache.write("foo", "bar")
+ cache.write("fu", "baz")
+ cache.delete_matched(/OO/i)
+ assert_equal false, cache.exist?("foo")
+ assert_equal true, cache.exist?("fu")
+ end
+end
+
+# Tests the base functionality that should be identical across all cache stores.
+module CacheStoreBehavior
+ def test_should_read_and_write_strings
+ assert_equal true, @cache.write('foo', 'bar')
+ assert_equal 'bar', @cache.read('foo')
+ end
+
+ def test_should_overwrite
+ @cache.write('foo', 'bar')
+ @cache.write('foo', 'baz')
+ assert_equal 'baz', @cache.read('foo')
end
def test_fetch_without_cache_miss
- @cache.stubs(:read).with('foo', {}).returns('bar')
+ @cache.write('foo', 'bar')
@cache.expects(:write).never
assert_equal 'bar', @cache.fetch('foo') { 'baz' }
end
def test_fetch_with_cache_miss
- @cache.stubs(:read).with('foo', {}).returns(nil)
- @cache.expects(:write).with('foo', 'baz', {})
+ @cache.expects(:write).with('foo', 'baz', @cache.options)
assert_equal 'baz', @cache.fetch('foo') { 'baz' }
end
def test_fetch_with_forced_cache_miss
+ @cache.write('foo', 'bar')
@cache.expects(:read).never
- @cache.expects(:write).with('foo', 'bar', :force => true)
+ @cache.expects(:write).with('foo', 'bar', @cache.options.merge(:force => true))
@cache.fetch('foo', :force => true) { 'bar' }
end
-end
-# Tests the base functionality that should be identical across all cache stores.
-module CacheStoreBehavior
- def test_should_read_and_write_strings
- @cache.write('foo', 'bar')
- assert_equal 'bar', @cache.read('foo')
+ def test_fetch_with_cached_nil
+ @cache.write('foo', nil)
+ @cache.expects(:write).never
+ assert_nil @cache.fetch('foo') { 'baz' }
end
def test_should_read_and_write_hash
- @cache.write('foo', {:a => "b"})
+ assert_equal true, @cache.write('foo', {:a => "b"})
assert_equal({:a => "b"}, @cache.read('foo'))
end
def test_should_read_and_write_integer
- @cache.write('foo', 1)
+ assert_equal true, @cache.write('foo', 1)
assert_equal 1, @cache.read('foo')
end
def test_should_read_and_write_nil
- @cache.write('foo', nil)
+ assert_equal true, @cache.write('foo', nil)
assert_equal nil, @cache.read('foo')
end
- def test_fetch_without_cache_miss
+ def test_read_multi
@cache.write('foo', 'bar')
- assert_equal 'bar', @cache.fetch('foo') { 'baz' }
+ @cache.write('fu', 'baz')
+ @cache.write('fud', 'biz')
+ assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu'))
end
- def test_fetch_with_cache_miss
- assert_equal 'baz', @cache.fetch('foo') { 'baz' }
+ def test_read_and_write_compressed_small_data
+ @cache.write('foo', 'bar', :compress => true)
+ raw_value = @cache.send(:read_entry, 'foo', {}).raw_value
+ assert_equal 'bar', @cache.read('foo')
+ assert_equal 'bar', raw_value
end
- def test_fetch_with_forced_cache_miss
- @cache.fetch('foo', :force => true) { 'bar' }
+ def test_read_and_write_compressed_large_data
+ @cache.write('foo', 'bar', :compress => true, :compress_threshold => 2)
+ raw_value = @cache.send(:read_entry, 'foo', {}).raw_value
+ assert_equal 'bar', @cache.read('foo')
+ assert_equal 'bar', Marshal.load(Zlib::Inflate.inflate(raw_value))
end
- def test_increment
- @cache.write('foo', 1, :raw => true)
- assert_equal 1, @cache.read('foo', :raw => true).to_i
- assert_equal 2, @cache.increment('foo')
- assert_equal 2, @cache.read('foo', :raw => true).to_i
- assert_equal 3, @cache.increment('foo')
- assert_equal 3, @cache.read('foo', :raw => true).to_i
+ def test_read_and_write_compressed_nil
+ @cache.write('foo', nil, :compress => true)
+ assert_nil @cache.read('foo')
end
- def test_decrement
- @cache.write('foo', 3, :raw => true)
- assert_equal 3, @cache.read('foo', :raw => true).to_i
- assert_equal 2, @cache.decrement('foo')
- assert_equal 2, @cache.read('foo', :raw => true).to_i
- assert_equal 1, @cache.decrement('foo')
- assert_equal 1, @cache.read('foo', :raw => true).to_i
+ def test_cache_key
+ obj = Object.new
+ def obj.cache_key
+ :foo
+ end
+ @cache.write(obj, "bar")
+ assert_equal "bar", @cache.read("foo")
end
- def test_exist
- @cache.write('foo', 'bar')
- assert @cache.exist?('foo')
- assert !@cache.exist?('bar')
+ def test_param_as_cache_key
+ obj = Object.new
+ def obj.to_param
+ "foo"
+ end
+ @cache.write(obj, "bar")
+ assert_equal "bar", @cache.read("foo")
end
-end
-class FileStoreTest < ActiveSupport::TestCase
- def setup
- @cache = ActiveSupport::Cache.lookup_store(:file_store, Dir.pwd)
+ def test_array_as_cache_key
+ @cache.write([:fu, "foo"], "bar")
+ assert_equal "bar", @cache.read("fu/foo")
end
- def teardown
- File.delete("foo.cache")
+ def test_hash_as_cache_key
+ @cache.write({:foo => 1, :fu => 2}, "bar")
+ assert_equal "bar", @cache.read("foo=1/fu=2")
end
- include CacheStoreBehavior
-
- def test_expires_in
- time = Time.local(2008, 4, 24)
- Time.stubs(:now).returns(time)
- File.stubs(:mtime).returns(time)
+ def test_keys_are_case_sensitive
+ @cache.write("foo", "bar")
+ assert_nil @cache.read("FOO")
+ end
+ def test_exist
@cache.write('foo', 'bar')
- cache_read = lambda { @cache.read('foo', :expires_in => 60) }
- assert_equal 'bar', cache_read.call
-
- Time.stubs(:now).returns(time + 30)
- assert_equal 'bar', cache_read.call
-
- Time.stubs(:now).returns(time + 120)
- assert_nil cache_read.call
+ assert_equal true, @cache.exist?('foo')
+ assert_equal false, @cache.exist?('bar')
end
-end
-class MemoryStoreTest < ActiveSupport::TestCase
- def setup
- @cache = ActiveSupport::Cache.lookup_store(:memory_store)
+ def test_nil_exist
+ @cache.write('foo', nil)
+ assert_equal true, @cache.exist?('foo')
end
- include CacheStoreBehavior
+ def test_delete
+ @cache.write('foo', 'bar')
+ assert @cache.exist?('foo')
+ assert_equal true, @cache.delete('foo')
+ assert !@cache.exist?('foo')
+ end
def test_store_objects_should_be_immutable
@cache.write('foo', 'bar')
@@ -186,175 +234,365 @@ class MemoryStoreTest < ActiveSupport::TestCase
assert_nothing_raised { bar.gsub!(/.*/, 'baz') }
end
- def test_multi_get
- @cache.write('foo', 1)
- @cache.write('goo', 2)
- result = @cache.read_multi('foo', 'goo')
- assert_equal({'foo' => 1, 'goo' => 2}, result)
+ def test_expires_in
+ time = Time.local(2008, 4, 24)
+ Time.stubs(:now).returns(time)
+
+ @cache.write('foo', 'bar')
+ assert_equal 'bar', @cache.read('foo')
+
+ Time.stubs(:now).returns(time + 30)
+ assert_equal 'bar', @cache.read('foo')
+
+ Time.stubs(:now).returns(time + 61)
+ assert_nil @cache.read('foo')
end
-end
-uses_memcached 'memcached backed store' do
- class MemCacheStoreTest < ActiveSupport::TestCase
- def setup
- @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store)
- @data = @cache.instance_variable_get(:@data)
- @cache.clear
- @cache.silence!
- @cache.logger = Logger.new("/dev/null")
+ def test_race_condition_protection
+ time = Time.now
+ @cache.write('foo', 'bar', :expires_in => 60)
+ Time.stubs(:now).returns(time + 61)
+ result = @cache.fetch('foo', :race_condition_ttl => 10) do
+ assert_equal 'bar', @cache.read('foo')
+ "baz"
end
+ assert_equal "baz", result
+ end
- include CacheStoreBehavior
+ def test_race_condition_protection_is_limited
+ time = Time.now
+ @cache.write('foo', 'bar', :expires_in => 60)
+ Time.stubs(:now).returns(time + 71)
+ result = @cache.fetch('foo', :race_condition_ttl => 10) do
+ assert_equal nil, @cache.read('foo')
+ "baz"
+ end
+ assert_equal "baz", result
+ end
- def test_store_objects_should_be_immutable
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- @cache.read('foo').gsub!(/.*/, 'baz')
+ def test_race_condition_protection_is_safe
+ time = Time.now
+ @cache.write('foo', 'bar', :expires_in => 60)
+ Time.stubs(:now).returns(time + 61)
+ begin
+ @cache.fetch('foo', :race_condition_ttl => 10) do
assert_equal 'bar', @cache.read('foo')
+ raise ArgumentError.new
end
+ rescue ArgumentError => e
end
+ assert_equal "bar", @cache.read('foo')
+ Time.stubs(:now).returns(time + 71)
+ assert_nil @cache.read('foo')
+ end
+
+ def test_crazy_key_characters
+ crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-"
+ assert_equal true, @cache.write(crazy_key, "1", :raw => true)
+ assert_equal "1", @cache.read(crazy_key)
+ assert_equal "1", @cache.fetch(crazy_key)
+ assert_equal true, @cache.delete(crazy_key)
+ assert_equal "2", @cache.fetch(crazy_key, :raw => true) { "2" }
+ assert_equal 3, @cache.increment(crazy_key)
+ assert_equal 2, @cache.decrement(crazy_key)
+ end
+
+ def test_really_long_keys
+ key = ""
+ 1000.times{key << "x"}
+ assert_equal true, @cache.write(key, "bar")
+ assert_equal "bar", @cache.read(key)
+ assert_equal "bar", @cache.fetch(key)
+ assert_nil @cache.read("#{key}x")
+ assert_equal({key => "bar"}, @cache.read_multi(key))
+ assert_equal true, @cache.delete(key)
+ end
+end
- def test_stored_objects_should_not_be_frozen
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- end
- @cache.with_local_cache do
- assert !@cache.read('foo').frozen?
- end
+module CacheDeleteMatchedBehavior
+ def test_delete_matched
+ @cache.write("foo", "bar")
+ @cache.write("fu", "baz")
+ @cache.delete_matched(/oo/)
+ assert_equal false, @cache.exist?("foo")
+ assert_equal true, @cache.exist?("fu")
+ end
+end
+
+module CacheIncrementDecrementBehavior
+ def test_increment
+ @cache.write('foo', 1, :raw => true)
+ assert_equal 1, @cache.read('foo').to_i
+ assert_equal 2, @cache.increment('foo')
+ assert_equal 2, @cache.read('foo').to_i
+ assert_equal 3, @cache.increment('foo')
+ assert_equal 3, @cache.read('foo').to_i
+ end
+
+ def test_decrement
+ @cache.write('foo', 3, :raw => true)
+ assert_equal 3, @cache.read('foo').to_i
+ assert_equal 2, @cache.decrement('foo')
+ assert_equal 2, @cache.read('foo').to_i
+ assert_equal 1, @cache.decrement('foo')
+ assert_equal 1, @cache.read('foo').to_i
+ end
+end
+
+module LocalCacheBehavior
+ def test_local_writes_are_persistent_on_the_remote_cache
+ retval = @cache.with_local_cache do
+ @cache.write('foo', 'bar')
end
+ assert_equal true, retval
+ assert_equal 'bar', @cache.read('foo')
+ end
- def test_write_should_return_true_on_success
- @cache.with_local_cache do
- result = @cache.write('foo', 'bar')
- assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written
- assert result
- end
+ def test_clear_also_clears_local_cache
+ @cache.with_local_cache do
+ @cache.write('foo', 'bar')
+ @cache.clear
+ assert_nil @cache.read('foo')
end
- def test_local_writes_are_persistent_on_the_remote_cache
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- end
+ assert_nil @cache.read('foo')
+ end
+ def test_local_cache_of_write
+ @cache.with_local_cache do
+ @cache.write('foo', 'bar')
+ @peek.delete('foo')
assert_equal 'bar', @cache.read('foo')
end
+ end
- def test_clear_also_clears_local_cache
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- @cache.clear
- assert_nil @cache.read('foo')
- end
+ def test_local_cache_of_read
+ @cache.write('foo', 'bar')
+ @cache.with_local_cache do
+ assert_equal 'bar', @cache.read('foo')
end
+ end
- def test_local_cache_of_read_and_write
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- @data.flush_all # Clear remote cache
- assert_equal 'bar', @cache.read('foo')
- end
+ def test_local_cache_of_write_nil
+ @cache.with_local_cache do
+ assert true, @cache.write('foo', nil)
+ assert_nil @cache.read('foo')
+ @peek.write('foo', 'bar')
+ assert_nil @cache.read('foo')
end
+ end
- def test_local_cache_should_read_and_write_integer
- @cache.with_local_cache do
- @cache.write('foo', 1)
- assert_equal 1, @cache.read('foo')
- end
+ def test_local_cache_of_delete
+ @cache.with_local_cache do
+ @cache.write('foo', 'bar')
+ @cache.delete('foo')
+ assert_nil @cache.read('foo')
end
+ end
- def test_local_cache_of_delete
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- @cache.delete('foo')
- @data.flush_all # Clear remote cache
- assert_nil @cache.read('foo')
- end
+ def test_local_cache_of_exist
+ @cache.with_local_cache do
+ @cache.write('foo', 'bar')
+ @peek.delete('foo')
+ assert true, @cache.exist?('foo')
end
+ end
- def test_local_cache_of_exist
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- @cache.instance_variable_set(:@data, nil)
- @data.flush_all # Clear remote cache
- assert @cache.exist?('foo')
- end
+ def test_local_cache_of_increment
+ @cache.with_local_cache do
+ @cache.write('foo', 1, :raw => true)
+ @peek.write('foo', 2, :raw => true)
+ @cache.increment('foo')
+ assert_equal 3, @cache.read('foo')
end
+ end
- def test_local_cache_of_increment
- @cache.with_local_cache do
- @cache.write('foo', 1, :raw => true)
- @cache.increment('foo')
- @data.flush_all # Clear remote cache
- assert_equal 2, @cache.read('foo', :raw => true).to_i
- end
+ def test_local_cache_of_decrement
+ @cache.with_local_cache do
+ @cache.write('foo', 1, :raw => true)
+ @peek.write('foo', 3, :raw => true)
+ @cache.decrement('foo')
+ assert_equal 2, @cache.read('foo')
end
+ end
- def test_local_cache_of_decrement
- @cache.with_local_cache do
- @cache.write('foo', 1, :raw => true)
- @cache.decrement('foo')
- @data.flush_all # Clear remote cache
- assert_equal 0, @cache.read('foo', :raw => true).to_i
- end
- end
+ def test_middleware
+ app = lambda { |env|
+ result = @cache.write('foo', 'bar')
+ assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written
+ assert result
+ }
+ app = @cache.middleware.new(app)
+ app.call({})
+ end
+end
- def test_exist_with_nulls_cached_locally
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- @cache.delete('foo')
- assert !@cache.exist?('foo')
- end
+class FileStoreTest < ActiveSupport::TestCase
+ def setup
+ Dir.mkdir(cache_dir) unless File.exist?(cache_dir)
+ @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, :expires_in => 60)
+ @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, :expires_in => 60)
+ end
+
+ def teardown
+ FileUtils.rm_r(cache_dir)
+ end
+
+ def cache_dir
+ File.join(Dir.pwd, 'tmp_cache')
+ end
+
+ include CacheStoreBehavior
+ include LocalCacheBehavior
+ include CacheDeleteMatchedBehavior
+ include CacheIncrementDecrementBehavior
+
+ def test_deprecated_expires_in_on_read
+ ActiveSupport::Deprecation.silence do
+ old_cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir)
+
+ time = Time.local(2008, 4, 24)
+ Time.stubs(:now).returns(time)
+
+ old_cache.write("foo", "bar")
+ assert_equal 'bar', old_cache.read('foo', :expires_in => 60)
+
+ Time.stubs(:now).returns(time + 30)
+ assert_equal 'bar', old_cache.read('foo', :expires_in => 60)
+
+ Time.stubs(:now).returns(time + 61)
+ assert_equal 'bar', old_cache.read('foo')
+ assert_nil old_cache.read('foo', :expires_in => 60)
+ assert_nil old_cache.read('foo')
end
+ end
+end
- def test_multi_get
- @cache.with_local_cache do
- @cache.write('foo', 1)
- @cache.write('goo', 2)
- result = @cache.read_multi('foo', 'goo')
- assert_equal({'foo' => 1, 'goo' => 2}, result)
- end
+class MemoryStoreTest < ActiveSupport::TestCase
+ def setup
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => 100)
+ end
+
+ include CacheStoreBehavior
+ include CacheDeleteMatchedBehavior
+ include CacheIncrementDecrementBehavior
+
+ def test_prune_size
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
+ @cache.write(3, "cccccccccc") && sleep(0.001)
+ @cache.write(4, "dddddddddd") && sleep(0.001)
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
+ @cache.read(2) && sleep(0.001)
+ @cache.read(4)
+ @cache.prune(30)
+ assert_equal true, @cache.exist?(5)
+ assert_equal true, @cache.exist?(4)
+ assert_equal false, @cache.exist?(3)
+ assert_equal true, @cache.exist?(2)
+ assert_equal false, @cache.exist?(1)
+ end
+
+ def test_prune_size_on_write
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
+ @cache.write(3, "cccccccccc") && sleep(0.001)
+ @cache.write(4, "dddddddddd") && sleep(0.001)
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
+ @cache.write(6, "ffffffffff") && sleep(0.001)
+ @cache.write(7, "gggggggggg") && sleep(0.001)
+ @cache.write(8, "hhhhhhhhhh") && sleep(0.001)
+ @cache.write(9, "iiiiiiiiii") && sleep(0.001)
+ @cache.write(10, "kkkkkkkkkk") && sleep(0.001)
+ @cache.read(2) && sleep(0.001)
+ @cache.read(4) && sleep(0.001)
+ @cache.write(11, "llllllllll")
+ assert_equal true, @cache.exist?(11)
+ assert_equal true, @cache.exist?(10)
+ assert_equal true, @cache.exist?(9)
+ assert_equal true, @cache.exist?(8)
+ assert_equal true, @cache.exist?(7)
+ assert_equal false, @cache.exist?(6)
+ assert_equal false, @cache.exist?(5)
+ assert_equal true, @cache.exist?(4)
+ assert_equal false, @cache.exist?(3)
+ assert_equal true, @cache.exist?(2)
+ assert_equal false, @cache.exist?(1)
+ end
+
+ def test_pruning_is_capped_at_a_max_time
+ def @cache.delete_entry (*args)
+ sleep(0.01)
+ super
end
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
+ @cache.write(3, "cccccccccc") && sleep(0.001)
+ @cache.write(4, "dddddddddd") && sleep(0.001)
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
+ @cache.prune(30, 0.001)
+ assert_equal true, @cache.exist?(5)
+ assert_equal true, @cache.exist?(4)
+ assert_equal true, @cache.exist?(3)
+ assert_equal true, @cache.exist?(2)
+ assert_equal false, @cache.exist?(1)
+ end
+end
- def test_middleware
- app = lambda { |env|
- result = @cache.write('foo', 'bar')
- assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written
- assert result
- }
- app = @cache.middleware.new(app)
- app.call({})
+class SynchronizedStoreTest < ActiveSupport::TestCase
+ def setup
+ ActiveSupport::Deprecation.silence do
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60)
end
+ end
- def test_expires_in
- result = @cache.write('foo', 'bar', :expires_in => 1)
- assert_equal 'bar', @cache.read('foo')
- sleep 2
- assert_equal nil, @cache.read('foo')
+ include CacheStoreBehavior
+ include CacheDeleteMatchedBehavior
+ include CacheIncrementDecrementBehavior
+end
+
+uses_memcached 'memcached backed store' do
+ class MemCacheStoreTest < ActiveSupport::TestCase
+ def setup
+ @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :expires_in => 60)
+ @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store)
+ @data = @cache.instance_variable_get(:@data)
+ @cache.clear
+ @cache.silence!
+ @cache.logger = Logger.new("/dev/null")
end
- def test_expires_in_with_invalid_value
- @cache.write('baz', 'bat')
- assert_raise(RuntimeError) do
- @cache.write('foo', 'bar', :expires_in => 'Mon Jun 29 13:10:40 -0700 2150')
- end
- assert_equal 'bat', @cache.read('baz')
- assert_equal nil, @cache.read('foo')
+ include CacheStoreBehavior
+ include LocalCacheBehavior
+ include CacheIncrementDecrementBehavior
+
+ def test_raw_values
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
+ cache.clear
+ cache.write("foo", 2)
+ assert_equal "2", cache.read("foo")
end
- def test_delete_should_only_pass_key_to_data
- key = 'foo'
- @data.expects(:delete).with(key)
- @cache.delete(key)
+ def test_local_cache_raw_values
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
+ cache.clear
+ cache.with_local_cache do
+ cache.write("foo", 2)
+ assert_equal "2", cache.read("foo")
+ end
end
end
class CompressedMemCacheStore < ActiveSupport::TestCase
def setup
- @cache = ActiveSupport::Cache.lookup_store(:compressed_mem_cache_store)
- @cache.clear
+ ActiveSupport::Deprecation.silence do
+ @cache = ActiveSupport::Cache.lookup_store(:compressed_mem_cache_store, :expires_in => 60)
+ @cache.clear
+ end
end
include CacheStoreBehavior
+ include CacheIncrementDecrementBehavior
end
end
@@ -376,3 +614,38 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase
assert @buffer.string.blank?
end
end
+
+class CacheEntryTest < ActiveSupport::TestCase
+ def test_create_raw_entry
+ time = Time.now
+ entry = ActiveSupport::Cache::Entry.create("raw", time, :compress => false, :expires_in => 300)
+ assert_equal "raw", entry.raw_value
+ assert_equal time.to_f, entry.created_at
+ assert_equal false, entry.compressed?
+ assert_equal 300, entry.expires_in
+ end
+
+ def test_expired
+ entry = ActiveSupport::Cache::Entry.new("value")
+ assert_equal false, entry.expired?
+ entry = ActiveSupport::Cache::Entry.new("value", :expires_in => 60)
+ assert_equal false, entry.expired?
+ time = Time.now + 61
+ Time.stubs(:now).returns(time)
+ assert_equal true, entry.expired?
+ end
+
+ def test_compress_values
+ entry = ActiveSupport::Cache::Entry.new("value", :compress => true, :compress_threshold => 1)
+ assert_equal "value", entry.value
+ assert_equal true, entry.compressed?
+ assert_equal "value", Marshal.load(Zlib::Inflate.inflate(entry.raw_value))
+ end
+
+ def test_non_compress_values
+ entry = ActiveSupport::Cache::Entry.new("value")
+ assert_equal "value", entry.value
+ assert_equal "value", entry.raw_value
+ assert_equal false, entry.compressed?
+ end
+end
diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb
index aecc644549..e7617466c2 100644
--- a/activesupport/test/core_ext/array_ext_test.rb
+++ b/activesupport/test/core_ext/array_ext_test.rb
@@ -211,7 +211,7 @@ class ArrayToXmlTests < Test::Unit::TestCase
{ :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') }
].to_xml(:skip_instruct => true, :indent => 0)
- assert_equal '<records type="array"><record>', xml.first(30)
+ assert_equal '<objects type="array"><object>', xml.first(30)
assert xml.include?(%(<age type="integer">26</age>)), xml
assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml
assert xml.include?(%(<name>David</name>)), xml
@@ -233,7 +233,7 @@ class ArrayToXmlTests < Test::Unit::TestCase
{ :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0)
- assert_equal "<records><record>", xml.first(17)
+ assert_equal "<objects><object>", xml.first(17)
assert xml.include?(%(<street-address>Paulina</street-address>))
assert xml.include?(%(<name>David</name>))
assert xml.include?(%(<street-address>Evergreen</street-address>))
@@ -245,7 +245,7 @@ class ArrayToXmlTests < Test::Unit::TestCase
{ :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => false)
- assert_equal "<records><record>", xml.first(17)
+ assert_equal "<objects><object>", xml.first(17)
assert xml.include?(%(<street_address>Paulina</street_address>))
assert xml.include?(%(<street_address>Evergreen</street_address>))
end
@@ -255,7 +255,7 @@ class ArrayToXmlTests < Test::Unit::TestCase
{ :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => true)
- assert_equal "<records><record>", xml.first(17)
+ assert_equal "<objects><object>", xml.first(17)
assert xml.include?(%(<street-address>Paulina</street-address>))
assert xml.include?(%(<street-address>Evergreen</street-address>))
end
@@ -319,7 +319,7 @@ class ArrayExtractOptionsTests < Test::Unit::TestCase
assert_equal({}, options)
assert_equal [hash], array
end
-
+
def test_extract_options_extracts_extractable_subclass
hash = ExtractableHashSubclass.new
hash[:foo] = 1
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index ff95c0ca18..ac7ca96c4d 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -26,7 +26,7 @@ class TestJSONEncoding < Test::Unit::TestCase
NilTests = [[ nil, %(null) ]]
NumericTests = [[ 1, %(1) ],
[ 2.5, %(2.5) ],
- [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s}") ]]
+ [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s('F')}") ]]
StringTests = [[ 'this is the <string>', %("this is the \\u003Cstring\\u003E")],
[ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],
@@ -126,7 +126,7 @@ class TestJSONEncoding < Test::Unit::TestCase
def test_hash_should_allow_key_filtering_with_except
assert_equal %({"b":2}), ActiveSupport::JSON.encode({'foo' => 'bar', :b => 2, :c => 3}, :except => ['foo', :c])
end
-
+
def test_time_to_json_includes_local_offset
ActiveSupport.use_standard_json_time_format = true
with_env_tz 'US/Eastern' do
@@ -153,7 +153,7 @@ class TestJSONEncoding < Test::Unit::TestCase
def object_keys(json_object)
json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
end
-
+
def with_env_tz(new_tz = 'US/Eastern')
old_tz, ENV['TZ'] = ENV['TZ'], new_tz
yield
diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb
index d689b6be73..b054855d08 100644
--- a/activesupport/test/transliterate_test.rb
+++ b/activesupport/test/transliterate_test.rb
@@ -4,36 +4,6 @@ require 'active_support/inflector/transliterate'
class TransliterateTest < Test::Unit::TestCase
- APPROXIMATIONS = {
- "À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE",
- "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
- "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
- "Õ"=>"O", "Ö"=>"O", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U", "Ü"=>"U",
- "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a", "ã"=>"a",
- "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e", "ê"=>"e",
- "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d", "ñ"=>"n",
- "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o", "ù"=>"u",
- "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y", "Ā"=>"A",
- "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C", "ć"=>"c",
- "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c", "Ď"=>"D",
- "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E", "ĕ"=>"e",
- "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e", "Ĝ"=>"G",
- "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G", "ģ"=>"g",
- "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i", "Ī"=>"I",
- "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I", "ı"=>"i",
- "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k", "ĸ"=>"k",
- "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l", "Ŀ"=>"L",
- "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N", "ņ"=>"n",
- "Ň"=>"N", "ň"=>"n", "ʼn"=>"n", "Ŋ"=>"NG", "ŋ"=>"ng", "Ō"=>"O", "ō"=>"o",
- "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE", "œ"=>"oe", "Ŕ"=>"R",
- "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r", "Ś"=>"S", "ś"=>"s",
- "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S", "š"=>"s", "Ţ"=>"T",
- "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t", "Ũ"=>"U", "ũ"=>"u",
- "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U", "ů"=>"u", "Ű"=>"U",
- "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w", "Ŷ"=>"Y", "ŷ"=>"y",
- "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z", "Ž"=>"Z", "ž"=>"z"
- }
-
def test_transliterate_should_not_change_ascii_chars
(0..127).each do |byte|
char = [byte].pack("U")
@@ -41,10 +11,25 @@ class TransliterateTest < Test::Unit::TestCase
end
end
- def test_should_convert_accented_chars_to_approximate_ascii_chars
- APPROXIMATIONS.each do |given, expected|
- assert_equal expected, ActiveSupport::Inflector.transliterate(given)
+ def test_transliterate_should_approximate_ascii
+ # create string with range of Unicode"s western characters with
+ # diacritics, excluding the division and multiplication signs which for
+ # some reason or other are floating in the middle of all the letters.
+ string = (0xC0..0x17E).to_a.reject {|c| [0xD7, 0xF7].include? c}.pack("U*")
+ string.each_char do |char|
+ assert_match %r{^[a-zA-Z']*$}, ActiveSupport::Inflector.transliterate(string)
end
end
+ def test_transliterate_should_work_with_custom_i18n_rules_and_uncomposed_utf8
+ char = [117, 776].pack("U*") # "ü" as ASCII "u" plus COMBINING DIAERESIS
+ I18n.backend.store_translations(:de, :i18n => {:transliterate => {:rule => {"ü" => "ue"}}})
+ I18n.locale = :de
+ assert_equal "ue", ActiveSupport::Inflector.transliterate(char)
+ end
+
+ def test_transliterate_should_allow_a_custom_replacement_char
+ assert_equal "a*b", ActiveSupport::Inflector.transliterate("a索b", "*")
+ end
+
end
diff --git a/railties/guides/source/3_0_release_notes.textile b/railties/guides/source/3_0_release_notes.textile
index da69ada7b4..66fdad8c54 100644
--- a/railties/guides/source/3_0_release_notes.textile
+++ b/railties/guides/source/3_0_release_notes.textile
@@ -102,7 +102,7 @@ $ rails myapp --edge
If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the +--dev+ flag:
<shell>
-$ ruby /path/to/rails/railties/bin/rails myapp --dev
+$ ruby /path/to/rails/bin/rails myapp --dev
</shell>
h3. Rails Architectural Changes
@@ -568,6 +568,8 @@ Action Mailer has been given a new API with TMail being replaced out with the ne
* The +mail+ delivery method acts in a similar way to Action Controller's +respond_to+, and you can explicitly or implicitly render templates. Action Mailer will turn the email into a multipart email as needed.
* You can pass a proc to the <tt>format.mime_type</tt> calls within the mail block and explicitly render specific types of text, or add layouts or different templates. The +render+ call inside the proc is from Abstract Controller and supports the same options.
* What were mailer unit tests have been moved to functional tests.
+* Action Mailer now delegates all auto encoding of header fields and bodies to Mail Gem
+* Action Mailer will auto encode email bodies and headers for you
Deprecations:
diff --git a/railties/guides/source/action_mailer_basics.textile b/railties/guides/source/action_mailer_basics.textile
index 79cb86ee97..a2a748c749 100644
--- a/railties/guides/source/action_mailer_basics.textile
+++ b/railties/guides/source/action_mailer_basics.textile
@@ -55,7 +55,7 @@ class UserMailer < ActionMailer::Base
end
</ruby>
-Here is a quick explanation of the items presented in the preceding method. For a full list of all available options, please have a look further down at the Complete List of ActionMailer user-settable attributes section.
+Here is a quick explanation of the items presented in the preceding method. For a full list of all available options, please have a look further down at the Complete List of Action Mailer user-settable attributes section.
* <tt>default Hash</tt> - This is a hash of default values for any email you send, in this case we are setting the <tt>:from</tt> header to a value for all messages in this class, this can be overridden on a per email basis
* +mail+ - The actual email message, we are passing the <tt>:to</tt> and <tt>:subject</tt> headers in.
@@ -148,6 +148,14 @@ NOTE: In previous versions of Rails, you would call +deliver_welcome_email+ or +
WARNING: Sending out one email should only take a fraction of a second, if you are planning on sending out many emails, or you have a slow domain resolution service, you might want to investigate using a background process like delayed job.
+h4. Auto encoding header values
+
+Action Mailer now handles the auto encoding of multibyte characters inside of headers and bodies.
+
+If you are using UTF-8 as your character set, you do not have to do anything special, just go ahead and send in UTF-8 data to the address fields, subject, keywords, filenames or body of the email and ActionMailer will auto encode it into quoted printable for you in the case of a header field or Base64 encode any body parts that are non US-ASCII.
+
+For more complex examples, such as defining alternate character sets or self encoding text first, please refer to the Mail library.
+
h4. Complete List of Action Mailer Methods
There are just three methods that you need to send pretty much any email message:
diff --git a/railties/guides/source/command_line.textile b/railties/guides/source/command_line.textile
index ebae320ebc..ab024d7fc3 100644
--- a/railties/guides/source/command_line.textile
+++ b/railties/guides/source/command_line.textile
@@ -64,12 +64,13 @@ Without any prodding of any kind, +rails server+ will run our new shiny Rails ap
<shell>
$ cd commandsapp
$ rails server
-=> Booting WEBrick...
-=> Rails 2.2.0 application started on http://0.0.0.0:3000
-=> Ctrl-C to shutdown server; call with --help for options
-[2008-11-04 10:11:38] INFO WEBrick 1.3.1
-[2008-11-04 10:11:38] INFO ruby 1.8.5 (2006-12-04) [i486-linux]
-[2008-11-04 10:11:38] INFO WEBrick::HTTPServer#start: pid=18994 port=3000
+=> Booting WEBrick
+=> Rails 3.0.0 application starting in development on http://0.0.0.0:3000
+=> Call with -d to detach
+=> Ctrl-C to shutdown server
+[2010-04-18 03:20:33] INFO WEBrick 1.3.1
+[2010-04-18 03:20:33] INFO ruby 1.8.7 (2010-01-10) [x86_64-linux]
+[2010-04-18 03:20:33] INFO WEBrick::HTTPServer#start: pid=26086 port=3000
</shell>
With just three commands we whipped up a Rails server listening on port 3000. Go to your browser and open "http://localhost:3000":http://localhost:3000, you will see a basic rails app running.
@@ -237,7 +238,7 @@ The migration requires that we *migrate*, that is, run some Ruby code (living in
<shell>
$ rake db:migrate
-(in /Users/mikel/rails_programs/commandsapp)
+(in /home/foobar/commandsapp)
== CreateHighScores: migrating ===============================================
-- create_table(:high_scores)
-> 0.0026s
@@ -320,21 +321,20 @@ h4. +about+
Check it: Version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version! +about+ is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation.
<shell>
-$ rails about
+$ rake about
About your application's environment
-Ruby version 1.8.6 (i486-linux)
-RubyGems version 1.3.1
-Rails version 2.2.0
-Active Record version 2.2.0
-Action Pack version 2.2.0
-Active Resource version 2.2.0
-Action Mailer version 2.2.0
-Active Support version 2.2.0
-Edge Rails revision unknown
-Application root /home/commandsapp
+Ruby version 1.8.7 (x86_64-linux)
+RubyGems version 1.3.6
+Rack version 1.1
+Rails version 3.0.0
+Active Record version 3.0.0
+Action Pack version 3.0.0
+Active Resource version 3.0.0
+Action Mailer version 3.0.0
+Active Support version 3.0.0
+Middleware ActionDispatch::Static, Rack::Lock, Rack::Runtime, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::RemoteIp, Rack::Sendfile, ActionDispatch::Callbacks, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::MethodOverride, ActionDispatch::Head
+Application root /home/foobar/commandsapp
Environment development
-Database adapter sqlite3
-Database schema version 20081217073400
</shell>
h3. The Rails Advanced Command Line
@@ -526,7 +526,7 @@ You can get a list of Rake tasks available to you, which will often depend on yo
<shell>
rake --tasks
-(in /home/developer/commandsapp)
+(in /home/foobar/commandsapp)
rake db:abort_if_pending_migrations # Raises an error if there are pending migrations
rake db:charset # Retrieves the charset for the current environment's database
rake db:collation # Retrieves the collation for the current environment's database
diff --git a/railties/guides/source/credits.html.erb b/railties/guides/source/credits.html.erb
index 4d0d1fe782..e9eb31bcae 100644
--- a/railties/guides/source/credits.html.erb
+++ b/railties/guides/source/credits.html.erb
@@ -43,6 +43,10 @@ Ruby on Rails Guides: Credits
Jeff Dean is a software engineer with <a href="http://pivotallabs.com">Pivotal Labs</a>.
<% end %>
+<%= author('Mikel Lindsaar', 'raasdnil') do %>
+ Mikel Lindsaar has been working with Rails since 2006 and is the author of the Ruby <a hred="http://github.com/mikel/mail">Mail gem</a> and core contributor (he helped re-write Action Mailer's API). Mikel is the founder of <a href="http://rubyx.com/">RubyX</a>, has a <a href="http://lindsaar.net/">blog</a> and <a href="http://twitter.com/raasdnil">tweets</a>.
+<% end %>
+
<%= author('Cássio Marques', 'cmarques') do %>
Cássio Marques is a Brazilian software developer working with different programming languages such as Ruby, JavaScript, CPP and Java, as an independent consultant. He blogs at <a href="http://cassiomarques.wordpress.com">/* CODIFICANDO */</a>, which is mainly written in Portuguese, but will soon get a new section for posts with English translation.
<% end %>
@@ -58,7 +62,3 @@ Ruby on Rails Guides: Credits
<%= author('Heiko Webers', 'hawe') do %>
Heiko Webers is the founder of <a href="http://www.bauland42.de">bauland42</a>, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the <a href="http://www.rorsecurity.info">Ruby on Rails Security Project</a>. After 10 years of desktop application development, Heiko has rarely looked back.
<% end %>
-
-<%= author('Mikel Lindsaar', 'raasdnil') do %>
- Mikel Lindsaar has been working with Rails since 2006 and is the author of the Ruby Mail gem and core contributor (he helped re-write Action Mailer's API). Mikel has a <a href="http://lindsaar.net/">blog</a> and <a href="http://twitter.com/raasdnil">tweets</a>.
-<% end %>
diff --git a/railties/guides/source/generators.textile b/railties/guides/source/generators.textile
index 4387fe3bd5..d3757e9733 100644
--- a/railties/guides/source/generators.textile
+++ b/railties/guides/source/generators.textile
@@ -88,9 +88,7 @@ And it will create a new generator as follow:
<ruby>
class InitializerGenerator < Rails::Generators::NamedBase
- def self.source_root
- @source_root ||= File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
- end
+ source_root File.expand_path("../templates", __FILE__)
end
</ruby>
@@ -115,9 +113,7 @@ And now let's change the generator to copy this template when invoked:
<ruby>
class InitializerGenerator < Rails::Generators::NamedBase
- def self.source_root
- @source_root ||= File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
- end
+ source_root File.expand_path("../templates", __FILE__)
def copy_initializer_file
copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
@@ -135,21 +131,18 @@ We can see that now a initializer named foo was created at +config/initializers/
h3. Generators lookup
-Now that we know how to create generators, we must know where Rails looks for generators before invoking them. When we invoke the initializer generator, Rails looks at the following paths in the given order:
+With our first generator created, we must discuss briefly generators lookup. The way Rails finds generators is exactly the same way Ruby find files, i.e. using +$LOAD_PATHS+.
+
+For instance, when you say +rails g initializer foo+, rails knows you want to invoke the initializer generator and then search for the following generators in the $LOAD_PATHS:
<shell>
-RAILS_APP/lib/generators
-RAILS_APP/lib/rails_generators
-RAILS_APP/vendor/plugins/*/lib/generators
-RAILS_APP/vendor/plugins/*/lib/rails_generators
-GEMS_PATH/*/lib/generators
-GEMS_PATH/*/lib/rails_generators
-~/rails/generators
-~/rails/rails_generators
-RAILS_GEM/lib/rails/generators
+rails/generators/initializer/initializer_generator.rb
+generators/initializer/initializer_generator.rb
+rails/generators/initializer_generator.rb
+generators/initializer_generator.rb
</shell>
-First Rails looks for generators in your application, then in plugins and/or gems, then in your home and finally the builtin generators. One very important thing to keep in mind is that in Rails 3.0 and after it only looks for generators in gems being used in your application. So if you have rspec installed as a gem, but it's not declared in your application, Rails won't be able to invoke it.
+If none of them is found, it raises an error message.
h3. Customizing your workflow
@@ -183,7 +176,6 @@ $ rails generate scaffold User name:string
create app/views/users/show.html.erb
create app/views/users/new.html.erb
create app/views/users/_form.html.erb
- create app/views/layouts/users.html.erb
invoke test_unit
create test/functional/users_controller_test.rb
invoke helper
@@ -284,7 +276,7 @@ end
end
</ruby>
-Now, when the helper generator is invoked and let's say test unit is configured as test framework, it will try to invoke both +MyHelper::Generators::TestUnitGenerator+ and +TestUnit::Generators::MyHelperGenerator+. Since none of those are defined, we can tell our generator to invoke +TestUnit::Generators::HelperGenerator+ instead, which is defined since it's a Rails hook. To do that, we just need to add:
+Now, when the helper generator is invoked and let's say test unit is configured as test framework, it will try to invoke both +MyHelper::Generators::TestUnitGenerator+ and +TestUnit::Generators::MyHelperGenerator+. Since none of those are defined, we can tell our generator to invoke +TestUnit::Generators::HelperGenerator+ instead, which is defined since it's a Rails generator. To do that, we just need to add:
<ruby>
# Search for :helper instead of :my_helper
@@ -375,4 +367,6 @@ h3. Changelog
"Lighthouse Ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/102
-* November 20, 2009: First release version by José Valim
+* April 30, 2010: Reviewed by José Valim
+
+* November 20, 2009: First version by José Valim
diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile
index 09190f5800..a4f969efe9 100644
--- a/railties/guides/source/getting_started.textile
+++ b/railties/guides/source/getting_started.textile
@@ -91,7 +91,7 @@ Action Dispatch handles routing of web requests and dispatches them as you want,
h5. Action Mailer
-Action Mailer is a framework for building e-mail services. You can use Action Mailer to send emails based on flexible templates, or to receive and process incoming email.
+Action Mailer is a framework for building e-mail services. You can use Action Mailer to receive and process incoming email and send simple plain text or complex multipart emails based on flexible templates.
h5. Active Model
@@ -363,7 +363,6 @@ The scaffold generator will build 15 files in your application, along with some
|app/views/posts/show.html.erb |A view to display a single post|
|app/views/posts/new.html.erb |A view to create a new post|
|app/views/posts/_form.html.erb |A partial to control the overall look and feel of the form used in edit and new views|
-|app/views/layouts/posts.html.erb |A view to control the overall look and feel of the other post views|
|app/helpers/posts_helper.rb |Helper functions to be used from the post views|
|test/unit/post_test.rb |Unit testing harness for the posts model|
|test/functional/posts_controller_test.rb |Functional testing harness for the posts controller|
@@ -395,7 +394,7 @@ class CreatePosts < ActiveRecord::Migration
end
</ruby>
-The above migration creates two methods, +up+, called when you run this migration into the database, and +down+ in case you need to reverse the changes made by this migration at a later date. The +up+ command in this case creates a +posts+ table with two string columns and a text column. It also is creating two timestamp fields to track record creation and updating. More information about Rails migrations can be found in the "Rails Database Migrations":migrations.html guide.
+The above migration creates two methods, +up+, called when you run this migration into the database, and +down+ in case you need to reverse the changes made by this migration at a later date. The +up+ command in this case creates a +posts+ table with two string columns and a text column. It also creates two timestamp fields to track record creation and updating. More information about Rails migrations can be found in the "Rails Database Migrations":migrations.html guide.
At this point, you can use a rake command to run the migration:
@@ -504,7 +503,7 @@ def index
end
</ruby>
-This code sets the +@posts+ instance variable to an array of all posts in the database. +Post.all+ calls the +Post+ model to return all of the posts that are currently in the database, with no limiting conditions.
++Post.all+ calls the +Post+ model to return all of the posts currently in the database. The result of this call is an array containing the posts which has been saved in an instance variable called +@posts+.
TIP: For more information on finding records with Active Record, see "Active Record Query Interface":active_record_querying.html.
@@ -551,19 +550,19 @@ TIP: For more details on the rendering process, see "Layouts and Rendering in Ra
h4. Customizing the Layout
-The view is only part of the story of how HTML is displayed in your web browser. Rails also has the concept of +layouts+, which are containers for views. When Rails renders a view to the browser, it does so by putting the view's HTML into a layout's HTML. The +rails generate scaffold+ command automatically created a default layout, +app/views/layouts/posts.html.erb+, for the posts. Open this layout in your editor and modify the +body+ tag:
+The view is only part of the story of how HTML is displayed in your web browser. Rails also has the concept of +layouts+, which are containers for views. When Rails renders a view to the browser, it does so by putting the view's HTML into a layout's HTML. In previous versions of Rails, the +rails generate scaffold+ command would automatically create a controller specific layout, like +app/views/layouts/posts.html.erb+, for the posts controller. However this has been changed in Rails 3.0. A application specific +layout+ is used for all the controllers and can be found in +app/views/layouts/application.html.erb+. Open this layout in your editor and modify the +body+ tag:
<erb>
<!DOCTYPE html>
<html>
<head>
- <title>Posts: <%= controller.action_name %></title>
- <%= stylesheet_link_tag 'scaffold' %>
+ <title>Blog</title>
+ <%= stylesheet_link_tag :all %>
+ <%= javascript_include_tag :defaults %>
+ <%= csrf_meta_tag %>
</head>
<body style="background: #EEEEEE;">
-<p class="notice"><%= notice %></p>
-
<%= yield %>
</body>
@@ -603,7 +602,16 @@ If you take a look at +views/posts/_form.html.erb+ file, you will see the follow
<erb>
<%= form_for(@post) do |f| %>
- <%= f.error_messages %>
+ <% if @post.errors.any? %>
+ <div id="errorExplanation">
+ <h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2>
+ <ul>
+ <% @post.errors.full_messages.each do |msg| %>
+ <li><%= msg %></li>
+ <% end %>
+ </ul>
+ </div>
+ <% end %>
<div class="field">
<%= f.label :name %><br />
@@ -678,18 +686,20 @@ end
The +show+ action uses +Post.find+ to search for a single record in the database by its id value. After finding the record, Rails displays it by using +show.html.erb+:
<erb>
+<p class="notice"><%= notice %></p>
+
<p>
- <b>Name:</b>
+ <strong>Name:</strong>
<%= @post.name %>
</p>
<p>
- <b>Title:</b>
+ <strong>Title:</strong>
<%= @post.title %>
</p>
<p>
- <b>Content:</b>
+ <strong>Content:</strong>
<%= @post.content %>
</p>
@@ -878,30 +888,33 @@ With the model in hand, you can turn your attention to creating a matching contr
$ rails generate controller Comments
</shell>
-This creates four files:
+This creates four files and one empty directory:
* +app/controllers/comments_controller.rb+ - The controller
* +app/helpers/comments_helper.rb+ - A view helper file
* +test/functional/comments_controller_test.rb+ - The functional tests for the controller
* +test/unit/helpers/comments_helper_test.rb+ - The unit tests for the helper
+* +app/views/comments/+ - Views of the controller are stored here
Like with any blog, our readers will create their comments directly after reading the post, and once they have added their comment, will be sent back to the post show page to see their comment now listed. Due to this, our +CommentsController+ is there to provide a method to create comments and delete SPAM comments when they arrive.
So first, we'll wire up the Post show template (+/app/views/posts/show.html.erb+) to let us make a new comment:
<erb>
+<p class="notice"><%= notice %></p>
+
<p>
- <b>Name:</b>
+ <strong>Name:</strong>
<%= @post.name %>
</p>
<p>
- <b>Title:</b>
+ <strong>Title:</strong>
<%= @post.title %>
</p>
<p>
- <b>Content:</b>
+ <strong>Content:</strong>
<%= @post.content %>
</p>
@@ -940,35 +953,37 @@ end
You'll see a bit more complexity here than you did in the controller for posts. That's a side-effect of the nesting that you've set up; each request for a comment has to keep track of the post to which the comment is attached, thus the initial find action to the Post model to get the post in question.
-In addition, the code takes advantage of some of the methods available for an association. For example, in the +new+ method, it calls
+In addition, the code takes advantage of some of the methods available for an association. We use the +create+ method on +@post.comments+ to create and save the comment. This will automatically link the comment so that it belongs to that particular post.
-Once we have made the new comment, we send the user back to the +post_path(@post)+ URL. This runs the +show+ action of the +PostsController+ which then renders the +show.html.erb+ template where we want the comment to show, so then, we'll add that to the +app/view/posts/show.html.erb+.
+Once we have made the new comment, we send the user back to the original post using the +post_path(@post)+ helper. As we have already seen, this calls the +show+ action of the +PostsController+ which in turn renders the +show.html.erb+ template. This is where we want the comment to show, so let's add that to the +app/view/posts/show.html.erb+.
<erb>
+<p class="notice"><%= notice %></p>
+
<p>
- <b>Name:</b>
+ <strong>Name:</strong>
<%= @post.name %>
</p>
<p>
- <b>Title:</b>
+ <strong>Title:</strong>
<%= @post.title %>
</p>
<p>
- <b>Content:</b>
+ <strong>Content:</strong>
<%= @post.content %>
</p>
<h2>Comments</h2>
<% @post.comments.each do |comment| %>
<p>
- <b>Commenter:</b>
+ <strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
- <b>Comment:</b>
+ <strong>Comment:</strong>
<%= comment.body %>
</p>
<% end %>
@@ -1000,20 +1015,20 @@ Now you can add posts and comments to your blog and have them show up in the rig
h3. Refactorization
-Now that we have Posts and Comments working, we can take a look at the +app/views/posts/show.html.erb+ template, it is getting long and awkward, we can use partials to clean this up.
+Now that we have Posts and Comments working, if we take a look at the +app/views/posts/show.html.erb+ template, it's getting long and awkward. We can use partials to clean this up.
h4. Rendering Partial Collections
-First will make a comment partial to extract showing all the comments for the post, so make a file +app/views/comments/_comment.html.erb+ and put into it:
+First we will make a comment partial to extract showing all the comments for the post. Create the file +app/views/comments/_comment.html.erb+ and put the following into it:
<erb>
<p>
- <b>Commenter:</b>
+ <strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
- <b>Comment:</b>
+ <strong>Comment:</strong>
<%= comment.body %>
</p>
</erb>
@@ -1021,18 +1036,20 @@ First will make a comment partial to extract showing all the comments for the po
Then in the +app/views/posts/show.html.erb+ you can change it to look like the following:
<erb>
+<p class="notice"><%= notice %></p>
+
<p>
- <b>Name:</b>
+ <strong>Name:</strong>
<%= @post.name %>
</p>
<p>
- <b>Title:</b>
+ <strong>Title:</strong>
<%= @post.title %>
</p>
<p>
- <b>Content:</b>
+ <strong>Content:</strong>
<%= @post.content %>
</p>
@@ -1090,18 +1107,20 @@ Lets also move that new comment section out to it's own partial, again, you crea
Then you make the +app/views/posts/show.html.erb+ look like the following:
<erb>
+<p class="notice"><%= notice %></p>
+
<p>
- <b>Name:</b>
+ <strong>Name:</strong>
<%= @post.name %>
</p>
<p>
- <b>Title:</b>
+ <strong>Title:</strong>
<%= @post.title %>
</p>
<p>
- <b>Content:</b>
+ <strong>Content:</strong>
<%= @post.content %>
</p>
@@ -1120,7 +1139,7 @@ Then you make the +app/views/posts/show.html.erb+ look like the following:
The second render just defines the partial template we want to render, <tt>comments/form</tt>, Rails is smart enough to spot the forward slash in that string and realise that you want to render the <tt>_form.html.erb</tt> file in the <tt>app/views/comments</tt> directory.
-The +@post+ object is available any partials rendered in the view because we defined it as an instance variable.
+The +@post+ object is available to any partials rendered in the view because we defined it as an instance variable.
h3. Deleting Comments
@@ -1130,12 +1149,12 @@ So first, let's add the delete link in the +app/views/comments/_comment.html.erb
<erb>
<p>
- <b>Commenter:</b>
+ <strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
- <b>Comment:</b>
+ <strong>Comment:</strong>
<%= comment.body %>
</p>
@@ -1328,23 +1347,25 @@ Now create the folder <tt>app/views/tags</tt> and make a file in there called <t
Finally, we will edit the <tt>app/views/posts/show.html.erb</tt> template to show our tags.
<erb>
+<p class="notice"><%= notice %></p>
+
<p>
- <b>Name:</b>
+ <strong>Name:</strong>
<%= @post.name %>
</p>
<p>
- <b>Title:</b>
+ <strong>Title:</strong>
<%= @post.title %>
</p>
<p>
- <b>Content:</b>
+ <strong>Content:</strong>
<%= @post.content %>
</p>
<p>
- <b>Tags:</b>
+ <strong>Tags:</strong>
<%= @post.tags.map { |t| t.name }.join(", ") %>
</p>
@@ -1381,23 +1402,25 @@ end
Now you can edit the view in <tt>app/views/posts/show.html.erb</tt> to look like this:
<erb>
+<p class="notice"><%= notice %></p>
+
<p>
- <b>Name:</b>
+ <strong>Name:</strong>
<%= @post.name %>
</p>
<p>
- <b>Title:</b>
+ <strong>Title:</strong>
<%= @post.title %>
</p>
<p>
- <b>Content:</b>
+ <strong>Content:</strong>
<%= @post.content %>
</p>
<p>
- <b>Tags:</b>
+ <strong>Tags:</strong>
<%= join_tags(@post) %>
</p>
@@ -1432,6 +1455,7 @@ h3. Changelog
"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/2
+* April 25, 2010: Couple of more minor fixups "Mikel Lindsaar":credits:html#raasdnil
* April 1, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com
* February 8, 2010: Full re-write for Rails 3.0-beta, added helpers and before_filters, refactored code by "Mikel Lindsaar":credits:html#raasdnil
* January 24, 2010: Re-write for Rails 3.0 by "Mikel Lindsaar":credits:html#raasdnil
diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile
index 9182f89f5b..9ce27fa331 100644
--- a/railties/guides/source/initialization.textile
+++ b/railties/guides/source/initialization.textile
@@ -825,11 +825,11 @@ Now that we've covered the extensive process of what the first line does in this
end
</ruby>
-As you may be able to tell from the code, this is going through and loading all the Railties for ActiveRecord, ActionController, ActionMailer, ActiveResource. Two other Railties, one for ActiveSupport and one for ActionDispatch were required earlier, but are still covered in this section for continuity reasons. TODO: link.
+As you may be able to tell from the code, this is going through and loading all the Railties for Active Record, Action Controller, Action Mailer, Active Resource. Two other Railties, one for Active Support and one for Action Dispatch were required earlier, but are still covered in this section for continuity reasons. TODO: link.
h4. ActiveSupport Railtie
-From ActiveSupport's README:
+From Active Support's README:
Active Support is a collection of various utility classes and standard library extensions that were found useful for Rails.
@@ -838,9 +838,9 @@ TODO: Quotify.
h5. +require 'active_support/railtie'+
-h4. ActiveRecord Railtie
+h4. Active Record Railtie
-The ActiveRecord Railtie takes care of hooking ActiveRecord into Rails. This depends on ActiveSupport, ActiveModel and Arel. From ActiveRecord's readme:
+The Active Record Railtie takes care of hooking Active Record into Rails. This depends on Active Support, Active Model and Arel. From Active Record's readme:
TODO: Quotify.
@@ -858,9 +858,9 @@ TODO: Quotify.
h5. +require "active_record/railtie"+
-The _activerecord/lib/active_record/railtie.rb_ file defines the Railtie for ActiveRecord.
+The _activerecord/lib/active_record/railtie.rb_ file defines the Railtie for Active Record.
-This file first requires ActiveRecord, the _railties/lib/rails.rb_ file which has already been required and so will be ignored, and the ActiveModel Railtie:
+This file first requires Active Record, the _railties/lib/rails.rb_ file which has already been required and so will be ignored, and the Active Model Railtie:
<ruby>
require "active_record"
@@ -868,13 +868,13 @@ This file first requires ActiveRecord, the _railties/lib/rails.rb_ file which ha
require "active_model/railtie"
</ruby>
-ActiveModel's Railtie is covered in the next section. TODO: Section.
+Active Model's Railtie is covered in the next section. TODO: Section.
h5. +require "active_record"+
TODO: Why are +activesupport_path+ and +activemodel_path+ defined here?
-The first three requires require ActiveSupport, ActiveModel and ARel in that order:
+The first three requires require ActiveSupport, Active Model and ARel in that order:
<ruby>
require 'active_support'
@@ -885,17 +885,17 @@ The first three requires require ActiveSupport, ActiveModel and ARel in that ord
h5. +require "active_support"+
-This was loaded earlier by _railties/lib/rails.rb_. This line is here as a safeguard for when ActiveRecord is loaded outside the scope of Rails.
+This was loaded earlier by _railties/lib/rails.rb_. This line is here as a safeguard for when Active Record is loaded outside the scope of Rails.
h5. +require "active_model"+
TODO: Again with the +activesupport_path+!
-Here we see another +require "active_support"+ this is again, a safeguard for when ActiveModel is loaded outside the scope of Rails.
+Here we see another +require "active_support"+ this is again, a safeguard for when Active Model is loaded outside the scope of Rails.
-This file defines a few +autoload+'d modules for ActiveModel, requires +active_support/i18n+ and adds the default translation file for ActiveModel to +I18n.load_path+.
+This file defines a few +autoload+'d modules for Active Model, requires +active_support/i18n+ and adds the default translation file for Active Model to +I18n.load_path+.
-The +require 'active_support/i18n'+ just loads I18n and adds ActiveSupport's default translations file to +I18n.load_path+ too:
+The +require 'active_support/i18n'+ just loads I18n and adds Active Support's default translations file to +I18n.load_path+ too:
<ruby>
require 'i18n'
@@ -905,7 +905,7 @@ The +require 'active_support/i18n'+ just loads I18n and adds ActiveSupport's def
h5. +require "arel"+
-This file in _arel/lib/arel.rb_ loads a couple of ActiveSupport things first:
+This file in _arel/lib/arel.rb_ loads a couple of Active Support things first:
<ruby>
require 'active_support/inflector'
@@ -917,7 +917,7 @@ These files are explained in the "Common Includes" section.
h5. +require 'arel'+
-Back in _arel/lib/arel.rb_, the next two lines require ActiveRecord parts:
+Back in _arel/lib/arel.rb_, the next two lines require Active Record parts:
<ruby>
require 'active_record'
@@ -928,7 +928,7 @@ Because we're currently loading _active_record.rb_, it skips right over it.
h5. +require 'active_record/connection_adapters/abstract/quoting'+
-_activerecord/lib/active_record/connection_adapters/abstract/quoting.rb_ defines methods used for quoting fields and table names in ActiveRecord.
+_activerecord/lib/active_record/connection_adapters/abstract/quoting.rb_ defines methods used for quoting fields and table names in Active Record.
TODO: Explain why this is loaded especially.
@@ -936,11 +936,11 @@ h5. +require 'active_record'+
Back the initial require from the _railtie.rb_.
-The _active_support_ and _active_model_ requires are again just an insurance for if we're loading ActiveRecord outside of the scope of Rails. In _active_record.rb_ the ActiveRecord +Module+ is initialized and in it there is defined a couple of +autoloads+ and +eager_autoloads+.
+The _active_support_ and _active_model_ requires are again just an insurance for if we're loading Active Record outside of the scope of Rails. In _active_record.rb_ the ActiveRecord +Module+ is initialized and in it there is defined a couple of +autoloads+ and +eager_autoloads+.
There's a new method here called +autoload_under+ which is defined in +ActiveSupport::Autoload+. This sets the autoload path to temporarily be the specified path, in this case +relation+ for the +autoload+'d classes inside the block.
-Inside this file the +AttributeMethods+, +Locking+ and +ConnectionAdapter+ modules are defined inside the +ActiveRecord+ module. The second to last line tells Arel what SQL engine we want to use. In this case it's +ActiveRecord::Base+. The final line adds in the translations for ActiveRecord which are only for if a record is invalid or non-unique.
+Inside this file the +AttributeMethods+, +Locking+ and +ConnectionAdapter+ modules are defined inside the +ActiveRecord+ module. The second to last line tells Arel what SQL engine we want to use. In this case it's +ActiveRecord::Base+. The final line adds in the translations for Active Record which are only for if a record is invalid or non-unique.
h5. +require 'rails'+
@@ -948,15 +948,15 @@ As mentioned previously this is skipped over as it has been already loaded. If y
h5. +require 'active_model/railtie'+
-This is covered in the ActiveModel Railtie section. TODO: link there.
+This is covered in the Active Model Railtie section. TODO: link there.
h5. +require 'action_controller/railtie'+
-This is covered in the ActionController Railtie section. TODO: link there.
+This is covered in the Action Controller Railtie section. TODO: link there.
-h5. The ActiveRecord Railtie
+h5. The Active Record Railtie
-Inside the ActiveRecord Railtie the +ActiveRecord::Railtie+ class is defined:
+Inside the Active Record Railtie the +ActiveRecord::Railtie+ class is defined:
<ruby>
module ActiveRecord
@@ -983,11 +983,11 @@ By doing this the +ActiveRecord::Railtie+ class gains access to the methods cont
As with the engine initializers, these are explained later.
-h4. ActiveModel Railtie
+h4. Active Model Railtie
-This Railtie is +require+'d by ActiveRecord's Railtie.
+This Railtie is +require+'d by Active Record's Railtie.
-From the ActiveModel readme:
+From the Active Model readme:
<plain>
Prior to Rails 3.0, if a plugin or gem developer wanted to be able to have an object interact with Action Pack helpers, it was required to either copy chunks of code from Rails, or monkey patch entire helpers to make them handle objects that did not look like Active Record. This generated code duplication and fragile applications that broke on upgrades.
@@ -1009,11 +1009,11 @@ This Railtie file, _activemodel/lib/active_model/railtie.rb_ is quite small and
h5. +require "active_model"+
-ActiveModel depends on ActiveSupport and ensures it is required by making a +require 'active_support'+ call. It has already been loaded from _railties/lib/rails.rb_ so will not be reloaded for us here. The file goes on to define the +ActiveModel+ module and all of its autoloaded classes. This file also defines the english translations for some of the validation messages provided by ActiveModel, such as "is not included in the list" and "is reserved".
+Active Model depends on Active Support and ensures it is required by making a +require 'active_support'+ call. It has already been loaded from _railties/lib/rails.rb_ so will not be reloaded for us here. The file goes on to define the +ActiveModel+ module and all of its autoloaded classes. This file also defines the english translations for some of the validation messages provided by Active Model, such as "is not included in the list" and "is reserved".
h4. Action Controller Railtie
-The ActionController Railtie takes care of all the behind-the-scenes code for your controllers; it puts the C into MVC; and does so by implementing the +ActionController::Base+ class which you may recall is where your +ApplicationController+ class descends from.
+The Action Controller Railtie takes care of all the behind-the-scenes code for your controllers; it puts the C into MVC; and does so by implementing the +ActionController::Base+ class which you may recall is where your +ApplicationController+ class descends from.
h5. +require 'action_controller/railtie'+
@@ -1025,17 +1025,17 @@ This first makes a couple of requires:
require "action_view/railtie"
</ruby>
-The _action_controller_ file is explained in the very next section. The require to _rails_ is requiring the already-required _railties/lib/rails.rb_. If you wish to know about the require to _action_view/railtie_ this is explained in the ActionView Railtie section.
+The _action_controller_ file is explained in the very next section. The require to _rails_ is requiring the already-required _railties/lib/rails.rb_. If you wish to know about the require to _action_view/railtie_ this is explained in the Action View Railtie section.
h5. +require 'action_controller+
-This file, _actionpack/lib/action_controller.rb_, defines the ActionController module and its relative autoloads. Before it does any of that it makes two requires: one to _abstract_controller_, explored next, and the other to _action_dispatch_, explored directly after that.
+This file, _actionpack/lib/action_controller.rb_, defines the Action Controller module and its relative autoloads. Before it does any of that it makes two requires: one to _abstract_controller_, explored next, and the other to _action_dispatch_, explored directly after that.
h5. +require 'abstract_controller'+
+AbstractController+ provides the functionality of TODO.
-This file is in _actionpack/lib/abstract_controller.rb_ and begins by attempting to add the path to ActiveSupport to the load path, which it would succeed in if it wasn't already set by anything loaded before it. In this case, it's not going to be set due to Arel already loading it in (TODO: right?).
+This file is in _actionpack/lib/abstract_controller.rb_ and begins by attempting to add the path to Active Support to the load path, which it would succeed in if it wasn't already set by anything loaded before it. In this case, it's not going to be set due to Arel already loading it in (TODO: right?).
The next thing in this file four +require+ calls:
@@ -1059,7 +1059,7 @@ This file was loaded upon the first require of +active_support+ and is not inclu
h5. +require 'active_support/core_ext/module/attr_internal'+
-This file is explained in the "Common Includes" section as it is required again later on. See the TODO: section. I also think this may be explained in the ActiveSupport Extensions guide.
+This file is explained in the "Common Includes" section as it is required again later on. See the TODO: section. I also think this may be explained in the Active Support Core Extensions guide.
h5. +require 'active_support/core_ext/module/delegation'+
@@ -1073,7 +1073,7 @@ After the initial call to +require 'abstract_controller'+, this calls +require '
This file defines the +ActionController+ module and its autoloaded classes.
-Here we have a new method called +autoload_under+. This was covered in the ActiveRecord Railtie but it is covered here also just in case you missed or skimmed over it. The +autoload_under+ method is defined in +ActiveSupport::Autoload+ and it sets the autoload path to temporarily be the specified path, in this case by specifying _metal_ it will load the specified +autoload+'d classes from _lib/action_controller/metal_ inside the block.
+Here we have a new method called +autoload_under+. This was covered in the Active Record Railtie but it is covered here also just in case you missed or skimmed over it. The +autoload_under+ method is defined in +ActiveSupport::Autoload+ and it sets the autoload path to temporarily be the specified path, in this case by specifying _metal_ it will load the specified +autoload+'d classes from _lib/action_controller/metal_ inside the block.
Another new method we have here is called +autoload_at+:
@@ -1092,7 +1092,7 @@ Another new method we have here is called +autoload_at+:
end
</ruby>
-This defines the path of which to find these classes defined at and is most useful for if you have multiple classes defined in a single file, as is the case for this block; all of those classes are defined inside _action_controller/metal/exceptions.rb_ and when ActiveSupport goes looking for them it will look in that file.
+This defines the path of which to find these classes defined at and is most useful for if you have multiple classes defined in a single file, as is the case for this block; all of those classes are defined inside _action_controller/metal/exceptions.rb_ and when Active Support goes looking for them it will look in that file.
At the end of this file there are a couple more requires:
@@ -1101,7 +1101,7 @@ At the end of this file there are a couple more requires:
require 'action_view'
require 'action_controller/vendor/html-scanner'
- # Common ActiveSupport usage in ActionController
+ # Common Active Support usage in ActionController
require 'active_support/concern'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/load_error'
@@ -1113,7 +1113,7 @@ At the end of this file there are a couple more requires:
h5. +require 'action_view'+
-This is best covered in the ActionView Railtie section, so skip there by TODO: Link / page?
+This is best covered in the Action View Railtie section, so skip there by TODO: Link / page?
h5. +require 'action_controller/vendor/html-scanner'+
@@ -1129,7 +1129,7 @@ This file defines, as the path implies, attribute accessors for class. These are
h5. +require 'active_support/core_ext/load_error'+
-The ActiveSupport Core Extensions (TODO: LINK!) guide has a great coverage of what this file precisely provides.
+The Active Support Core Extensions (TODO: LINK!) guide has a great coverage of what this file precisely provides.
h5. +require 'active_support/core_ext/module/attr_internal'+
@@ -1145,7 +1145,7 @@ This file was required earlier by Arel and so is not required again.
h5. +require 'active_support/core_ext/name_error'+
-This file includes extensions to the +NameError+ class, providing the +missing_name+ and +missing_name?+ methods. For more information see the ActiveSupport extensions guide.
+This file includes extensions to the +NameError+ class, providing the +missing_name+ and +missing_name?+ methods. For more information see the Active Support Core Extensions guide.
h5. +require 'active_support/inflector'+
@@ -1153,9 +1153,9 @@ This file is explained in the "Common Includes" section.
This file was earlier required by Arel and so is not required again.
-h5. ActionController Railtie
+h5. Action Controller Railtie
-So now we come back to the ActionController Railtie with a couple more requires to go before +ActionController::Railtie+ is defined:
+So now we come back to the Action Controller Railtie with a couple more requires to go before +ActionController::Railtie+ is defined:
<ruby>
require "action_view/railtie"
@@ -1164,17 +1164,17 @@ So now we come back to the ActionController Railtie with a couple more requires
require "active_support/deprecation"
</ruby>
-As explained previously the +action_view/railtie+ file will be explained in the ActionView Railtie section. TODO: link to it.
+As explained previously the +action_view/railtie+ file will be explained in the Action View Railtie section. TODO: link to it.
h5. +require 'active_support/core_ext/class/subclasses'+
-For an explanation of this file _activesupport/lib/active_support/core_ext/class/subclasses_, see the ActiveSupport Core Extension guide.
+For an explanation of this file _activesupport/lib/active_support/core_ext/class/subclasses_, see the Active Support Core Extension guide.
h5. +require 'active_support/deprecation/proxy_wrappers'+
This file, _activesupport/lib/active_support/deprecation/proxy_wrappers.rb_, defines a couple of deprecation classes, which are +DeprecationProxy+, +DeprecationObjectProxy+, +DeprecationInstanceVariableProxy+, +DeprecationConstantProxy+ which are all namespaced into +ActiveSupport::Deprecation+. These last three are all subclasses of +DeprecationProxy+.
-Why do we mention them here? Beside the obvious-by-now fact that we're covering just about everything about the initialization process in this guide, if you're deprecating something in your library and you use ActiveSupport, you too can use the +DeprecationProxy+ class (and it's subclasses) too.
+Why do we mention them here? Beside the obvious-by-now fact that we're covering just about everything about the initialization process in this guide, if you're deprecating something in your library and you use Active Support, you too can use the +DeprecationProxy+ class (and it's subclasses) too.
h6. +DeprecationProxy+
@@ -1234,7 +1234,7 @@ This makes more sense in the wider scope of the initializer:
h6. +DeprecatedInstanceVariableProxy+
-This isn't actually used anywhere in Rails anymore. It was used previously for when +@request+ and +@params+ were deprecated in Rails 2. It has been kept around as it could be useful for the same purposes in libraries that use ActiveSupport.
+This isn't actually used anywhere in Rails anymore. It was used previously for when +@request+ and +@params+ were deprecated in Rails 2. It has been kept around as it could be useful for the same purposes in libraries that use Active Support.
h6. +DeprecatedConstantProxy+
@@ -1307,11 +1307,11 @@ This sets up some default behavior for the warnings raised by +ActiveSupport::De
}
</ruby>
-In the _test_ environment, we will see the deprecation errors displayed in +$stderr+ and in _development_ mode, these are sent to +Rails.logger+ if it exists, otherwise it is output to +$stderr+ in a very similar fashion to the _test_ environment. These are both defined as procs, so ActiveSupport can pass arguments to the +call+ method we call on it when ActiveSupport +warn+.
+In the _test_ environment, we will see the deprecation errors displayed in +$stderr+ and in _development_ mode, these are sent to +Rails.logger+ if it exists, otherwise it is output to +$stderr+ in a very similar fashion to the _test_ environment. These are both defined as procs, so Active Support can pass arguments to the +call+ method we call on it when Active Support +warn+.
h5. +require 'active_support/deprecation/reporting'+
-This file defines further extensions to the +ActiveSupport::Deprecation+ module, including the +warn+ method which is used from ActiveSupport's +DeprecationProxy+ class and an +attr_accessor+ on the class called +silenced+. This checks that we have a behavior defined, which we do in the _test_ and _development_ environments, and that we're not +silenced+ before warning about deprecations by +call+'ing the +Proc+ time.
+This file defines further extensions to the +ActiveSupport::Deprecation+ module, including the +warn+ method which is used from Active Support's +DeprecationProxy+ class and an +attr_accessor+ on the class called +silenced+. This checks that we have a behavior defined, which we do in the _test_ and _development_ environments, and that we're not +silenced+ before warning about deprecations by +call+'ing the +Proc+ time.
This file also defines a +silence+ method on the module also which you can pass a block to temporarily silence errors:
@@ -1357,9 +1357,9 @@ h5. +require 'action_controller/railties/url_helpers'+
This file defines a +with+ method on +ActionController::Railtie::UrlHelpers+ which is later used in the +action_controller.url_helpers+ initializer. For more information see the +action_controller.url_helpers+ initializer section.
-h5. ActionController Railtie
+h5. Action Controller Railtie
-After these requires it deprecates a couple of ex-ActionController methods and points whomever references them to their ActionDispatch equivalents. These methods are +session+, +session=+, +session_store+ and +session_store=+.
+After these requires it deprecates a couple of ex-Action Controller methods and points whomever references them to their ActionDispatch equivalents. These methods are +session+, +session=+, +session_store+ and +session_store=+.
After the deprecations, Rails defines the +log_subscriber+ to be a new instance of +ActionController::Railties::LogSubscriber+ and then go about defining the following initializers, keeping in mind that these are added to the list of initializers defined before hand:
@@ -1369,9 +1369,9 @@ After the deprecations, Rails defines the +log_subscriber+ to be a new instance
* action_controller.set_helpers_path
* action_controller.url_helpers
-h4. ActionView Railtie
+h4. Action View Railtie
-The ActionView Railtie provides the backend code for your views and it puts the C into MVC. This implements the +ActionView::Base+ of which all views and partials are objects of.
+The Action View Railtie provides the backend code for your views and it puts the C into MVC. This implements the +ActionView::Base+ of which all views and partials are objects of.
h5. +require 'action_view/railtie'+
@@ -1379,7 +1379,7 @@ The Railtie is defined in a file called _actionpack/lib/action_view/railtie.rb_
h5. +require 'action_view'+
-Here again we have the addition of the path to ActiveSupport to the load path attempted, but because it's already in the load path it will not be added. Similarly, we have two requires:
+Here again we have the addition of the path to Active Support to the load path attempted, but because it's already in the load path it will not be added. Similarly, we have two requires:
<ruby>
require 'active_support/ruby/shim'
@@ -1489,7 +1489,7 @@ h5. +ActionView::Context+
TODO: Not entirely sure what this is all about. Something about the context of view rendering... can't work it out.
-h5. ActionView Railtie
+h5. Action View Railtie
Now that _actionpack/lib/action_view.rb_ has been required, the next step is to +require 'rails'+, but this will be skipped as the file was required by _railties/lib/rails/all.rb_ way back in the beginnings of the initialization process.
@@ -1519,9 +1519,9 @@ The +ActionView::LogSubscriber+ sets up a method called +render_template+ which
The sole initializer defined here, _action_view.cache_asset_timestamps_ is responsible for caching the timestamps on the ends of your assets. If you've ever seen a link generated by +image_tag+ or +stylesheet_link_tag+ you would know that I mean that this timestamp is the number after the _?_ in this example: _/javascripts/prototype.js?1265442620_. This initializer will do nothing if +cache_classes+ is set to false in any of your application's configuration. TODO: Elaborate.
-h4. ActionMailer Railtie
+h4. Action Mailer Railtie
-The ActionMailer Railtie is responsible for including all the emailing functionality into Rails by way of the ActionMailer gem itself. ActionMailer is:
+The Action Mailer Railtie is responsible for including all the emailing functionality into Rails by way of the Action Mailer gem itself. Action Mailer is:
Action Mailer is a framework for designing email-service layers. These layers
are used to consolidate code for sending out forgotten passwords, welcome
@@ -1549,7 +1549,7 @@ The requires in +action_mailer+ are already loaded or are core extensions:
require 'abstract_controller'
require 'action_view'
- # Common ActiveSupport usage in ActionMailer
+ # Common Active Support usage in Action Mailer
require 'active_support/core_ext/class'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/array/uniq_by'
@@ -1559,8 +1559,8 @@ The requires in +action_mailer+ are already loaded or are core extensions:
require 'active_support/lazy_load_hooks'
</ruby>
-_abstract_controller_ is covered in the "ActionController Railtie" section. TODO: Cover AbstractController there and link to it.
-_action_view_ was required by the ActionView Railtie and will not be required again.
+_abstract_controller_ is covered in the "Action Controller Railtie" section. TODO: Cover AbstractController there and link to it.
+_action_view_ was required by the Action View Railtie and will not be required again.
For the core extensions you may reference the "Core Extensions" guide. TODO: Link to guide.
@@ -1615,7 +1615,7 @@ which is used by the +ActionMailer::MailerHelper+ method +block_format+:
end
</ruby>
-h5. ActionMailer Railtie
+h5. Action Mailer Railtie
The Railtie defines the +log_subscriber+ as +ActionMailer::Railties::LogSubscriber.new+, with this class having two logging methods: one for delivery called +deliver+ and one for receipt called +receive+.
@@ -1627,13 +1627,13 @@ The initializers defined in this Railtie are:
These are covered later on the Initialization section. TODO: first write then link to Initialization section.
-h4. ActiveResource Railtie
+h4. Active Resource Railtie
-The ActiveResource Railtie is responsible for creating an interface into remote sites that offer a REST API. The ActiveResource Railtie depends on ActiveSupport and ActiveModel.
+The Active Resource Railtie is responsible for creating an interface into remote sites that offer a REST API. The Active Resource Railtie depends on Active Support and Active Model.
h5. +require 'active_resource/railtie'+
-This file defines the ActiveResource Railtie:
+This file defines the Active Resource Railtie:
<ruby>
require "active_resource"
@@ -1659,7 +1659,7 @@ The +require 'rails'+ has already been done back in TODO: link to section.
h5. +require 'active_resource'+
-This file, _activeresource/lib/active_resource.rb_, defines the +ActiveResource+ module, first off this will add the path to ActiveSupport and ActiveModel to the load path if it's not already there, then require both +active_support+ (_activesupport/lib/active_support.rb_) and +active_model+ (_activemodel/lib/active_model.rb_)
+This file, _activeresource/lib/active_resource.rb_, defines the +ActiveResource+ module, first off this will add the path to Active Support and Active Model to the load path if it's not already there, then require both +active_support+ (_activesupport/lib/active_support.rb_) and +active_model+ (_activemodel/lib/active_model.rb_)
<ruby>
activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
@@ -1685,9 +1685,9 @@ This file, _activeresource/lib/active_resource.rb_, defines the +ActiveResource+
end
</ruby>
-h5. ActiveResource Railtie
+h5. Active Resource Railtie
-The Railtie itself is fairly short as ActiveResource is the smallest component of Rails.
+The Railtie itself is fairly short as Active Resource is the smallest component of Rails.
<ruby>
module ActiveResource
@@ -1713,11 +1713,11 @@ There is only one initializer defined here: +set_configs+. This is covered later
h4. ActionDispatch Railtie
-ActionDispatch handles all dispatch work for Rails. It interfaces with ActionController to determine what action to undertake when a request comes in. TODO: I would quote the README but it is strangely absent. Flyin' blind here!
+ActionDispatch handles all dispatch work for Rails. It interfaces with Action Controller to determine what action to undertake when a request comes in. TODO: I would quote the README but it is strangely absent. Flyin' blind here!
The ActionDispatch Railtie was previously required when we called +require 'rails'+, but we will cover the Railtie here too.
-ActionDispatch depends on ActiveSupport.
+ActionDispatch depends on Active Support.
h5. +require 'action_dispatch/railtie'+
@@ -1755,7 +1755,7 @@ This file was already loaded earlier in the initialization process. TODO: link t
h5. ActionDispatch Railtie
-The ActionDispatch Railtie is almost as short as the ActiveResource Railtie:
+The ActionDispatch Railtie is almost as short as the Active Resource Railtie:
<ruby>
require "action_dispatch"
@@ -2903,7 +2903,7 @@ If you don't want this to happen you can specify the +config.active_support.bare
h4. +preload_frameworks+
-Remember earlier how we had all that stuff +eager_autoload+'d for ActiveSupport?
+Remember earlier how we had all that stuff +eager_autoload+'d for Active Support?
<ruby>
initializer :preload_frameworks do
@@ -2932,11 +2932,11 @@ With +@@autoloads+ being
* initialize_dependency_mechanism
* bootstrap_load_path
-h4. ActiveSupport Initializers
+h4. Active Support Initializers
-ActiveSupport
+Active Support
-**ActiveSupport Initializers**
+**Active Support Initializers**
* active_support.initialize_whiny_nils
* active_support.initialize_time_zone
@@ -2947,18 +2947,18 @@ ActiveSupport
The +I18n::Railtie+ also defines an +after_initialize+ which we will return to later when discussing the initializers in detail.
-**ActionDispatch Initializers**
+**Action Dispatch Initializers**
* action_dispatch.prepare_dispatcher
-**ActionController Initializers**
+**Action Controller Initializers**
* action_controller.logger
* action_controller.set_configs
* action_controller.initialize_framework_caches
* action_controller.set_helpers_path
-**ActiveRecord Initializers**
+**Active Record Initializers**
* active_record.initialize_time_zone
* active_record.logger
@@ -2968,17 +2968,17 @@ The +I18n::Railtie+ also defines an +after_initialize+ which we will return to l
* active_record.load_observers
* active_record.set_dispatch_hooks
-**ActionView Initializers **
+**Action View Initializers **
* action_view.cache_asset_timestamps
-**ActionMailer Initializers **
+**Action Mailer Initializers **
* action_mailer.logger
* action_mailer.set_configs
* action_mailer.url_for
-**ActiveResource Initializers**
+**Active Resource Initializers**
* active_resource.set_configs
@@ -3651,7 +3651,7 @@ This file is _activesupport/lib/active_support/inflector.rb_ and makes a couple
require 'active_support/core_ext/string/inflections'
</ruby>
-The files included here define methods for modifying strings, such as +transliterate+ which will convert a Unicode string to its ASCII version, +parameterize+ for making strings into url-safe versions, +camelize+ for camel-casing a string such as +string_other+ into +StringOther+ and +ordinalize+ converting a string such as +101+ into +101st+. More information about these methods can be found in the ActiveSupport Guide. TODO: Link to AS Guide.
+The files included here define methods for modifying strings, such as +transliterate+ which will convert a Unicode string to its ASCII version, +parameterize+ for making strings into url-safe versions, +camelize+ for camel-casing a string such as +string_other+ into +StringOther+ and +ordinalize+ converting a string such as +101+ into +101st+. More information about these methods can be found in the Active Support Core Extensions Guide. TODO: Link to AS Guide.
h4. +require 'active_support/core_ext/module/delegation'+
@@ -3705,6 +3705,6 @@ The _activesupport/lib/active_support/ruby/shim.rb_ file requires methods that h
* +Time.to_time+
* +Time.to_datetime+
-For more information see the ActiveSupport Extensions guide TODO: link to relevant sections for each method.
+For more information see the Active Support Core Extensions guide TODO: link to relevant sections for each method.
And "the REXML security fix detailed here":[http://weblog.rubyonrails.org/2008/8/23/dos-vulnerabilities-in-rexml]
diff --git a/railties/guides/source/performance_testing.textile b/railties/guides/source/performance_testing.textile
index f74b68b0ef..c02fabc0b2 100644
--- a/railties/guides/source/performance_testing.textile
+++ b/railties/guides/source/performance_testing.textile
@@ -246,16 +246,16 @@ Sample output of +BrowsingTest#test_homepage_wall_time.csv+:
<shell>
measurement,created_at,app,rails,ruby,platform
-0.00738224999999992,2009-01-08T03:40:29Z,,2.3.0.master.0744148,ruby-1.8.6.110,i686-darwin9.0.0
-0.00755874999999984,2009-01-08T03:46:18Z,,2.3.0.master.0744148,ruby-1.8.6.110,i686-darwin9.0.0
-0.00762099999999993,2009-01-08T03:49:25Z,,2.3.0.master.0744148,ruby-1.8.6.110,i686-darwin9.0.0
-0.00603075000000008,2009-01-08T04:03:29Z,,2.3.0.master.0744148,ruby-1.8.6.111,i686-darwin9.1.0
-0.00619899999999995,2009-01-08T04:03:53Z,,2.3.0.master.0744148,ruby-1.8.6.111,i686-darwin9.1.0
-0.00755449999999991,2009-01-08T04:04:55Z,,2.3.0.master.0744148,ruby-1.8.6.110,i686-darwin9.0.0
-0.00595999999999997,2009-01-08T04:05:06Z,,2.3.0.master.0744148,ruby-1.8.6.111,i686-darwin9.1.0
-0.00740450000000004,2009-01-09T03:54:47Z,,2.3.0.master.859e150,ruby-1.8.6.110,i686-darwin9.0.0
-0.00603150000000008,2009-01-09T03:54:57Z,,2.3.0.master.859e150,ruby-1.8.6.111,i686-darwin9.1.0
-0.00771250000000012,2009-01-09T15:46:03Z,,2.3.0.master.859e150,ruby-1.8.6.110,i686-darwin9.0.0
+0.00738224999999992,2009-01-08T03:40:29Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
+0.00755874999999984,2009-01-08T03:46:18Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
+0.00762099999999993,2009-01-08T03:49:25Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
+0.00603075000000008,2009-01-08T04:03:29Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
+0.00619899999999995,2009-01-08T04:03:53Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
+0.00755449999999991,2009-01-08T04:04:55Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
+0.00595999999999997,2009-01-08T04:05:06Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
+0.00740450000000004,2009-01-09T03:54:47Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
+0.00603150000000008,2009-01-09T03:54:57Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
+0.00771250000000012,2009-01-09T15:46:03Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
</shell>
h5(#output-profiling). Profiling
diff --git a/railties/guides/source/testing.textile b/railties/guides/source/testing.textile
index 206ed6e75c..8d7d73b487 100644
--- a/railties/guides/source/testing.textile
+++ b/railties/guides/source/testing.textile
@@ -146,14 +146,14 @@ NOTE: For more information on Rails <i>scaffolding</i>, refer to "Getting Starte
When you use +rails generate scaffold+, for a resource among other things it creates a test stub in the +test/unit+ folder:
-<pre>
+<shell>
$ rails generate scaffold post title:string body:text
...
create app/models/post.rb
create test/unit/post_test.rb
create test/fixtures/posts.yml
...
-</pre>
+</shell>
The default test stub in +test/unit/post_test.rb+ looks like this:
@@ -702,24 +702,24 @@ class UserFlowsTest < ActionController::IntegrationTest
private
- module CustomDsl
- def browses_site
- get "/products/all"
- assert_response :success
- assert assigns(:products)
- end
+ module CustomDsl
+ def browses_site
+ get "/products/all"
+ assert_response :success
+ assert assigns(:products)
end
+ end
- def login(user)
- open_session do |sess|
- sess.extend(CustomDsl)
- u = users(user)
- sess.https!
- sess.post "/login", :username => u.username, :password => u.password
- assert_equal '/welcome', path
- sess.https!(false)
- end
+ def login(user)
+ open_session do |sess|
+ sess.extend(CustomDsl)
+ u = users(user)
+ sess.https!
+ sess.post "/login", :username => u.username, :password => u.password
+ assert_equal '/welcome', path
+ sess.https!(false)
end
+ end
end
</ruby>
@@ -845,9 +845,9 @@ Testing mailer classes requires some specific tools to do a thorough job.
h4. Keeping the Postman in Check
-Your +ActionMailer+ classes -- like every other part of your Rails application -- should be tested to ensure that it is working as expected.
+Your mailer classes -- like every other part of your Rails application -- should be tested to ensure that it is working as expected.
-The goals of testing your +ActionMailer+ classes are to ensure that:
+The goals of testing your mailer classes are to ensure that:
* emails are being processed (created and sent)
* the email content is correct (subject, sender, body, etc)
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index 7cec14c738..d39f9a2ae9 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -85,7 +85,7 @@ module Rails
delegate :metal_loader, :to => :config
def require_environment!
- environment = config.paths.config.environment.to_a.first
+ environment = paths.config.environment.to_a.first
require environment if environment
end
@@ -153,7 +153,7 @@ module Rails
require "rails/tasks"
task :environment do
$rails_rake_task = true
- initialize!
+ require_environment!
end
end
diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb
index 06243f2e5e..022e1a91d8 100644
--- a/railties/lib/rails/application/bootstrap.rb
+++ b/railties/lib/rails/application/bootstrap.rb
@@ -6,7 +6,8 @@ module Rails
include Initializable
initializer :load_environment_config do
- require_environment!
+ environment = config.paths.config.environments.to_a.first
+ require environment if environment
end
initializer :load_all_active_support do
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 874b3a78b6..1ad77fdfec 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -1,3 +1,4 @@
+require 'active_support/deprecation'
require 'rails/engine/configuration'
module Rails
@@ -43,14 +44,15 @@ module Rails
@paths ||= begin
paths = super
paths.app.controllers << builtin_controller if builtin_controller
- paths.config.database "config/database.yml"
- paths.config.environment "config/environments", :glob => "#{Rails.env}.rb"
- paths.lib.templates "lib/templates"
- paths.log "log/#{Rails.env}.log"
- paths.tmp "tmp"
- paths.tmp.cache "tmp/cache"
- paths.vendor "vendor", :load_path => true
- paths.vendor.plugins "vendor/plugins"
+ paths.config.database "config/database.yml"
+ paths.config.environment "config/environment.rb"
+ paths.config.environments "config/environments", :glob => "#{Rails.env}.rb"
+ paths.lib.templates "lib/templates"
+ paths.log "log/#{Rails.env}.log"
+ paths.tmp "tmp"
+ paths.tmp.cache "tmp/cache"
+ paths.vendor "vendor", :load_path => true
+ paths.vendor.plugins "vendor/plugins"
if File.exists?("#{root}/test/mocks/#{Rails.env}")
ActiveSupport::Deprecation.warn "\"Rails.root/test/mocks/#{Rails.env}\" won't be added " <<
@@ -142,7 +144,7 @@ module Rails
middleware.use('::Rack::Runtime')
middleware.use('::Rails::Rack::Logger')
middleware.use('::ActionDispatch::ShowExceptions', lambda { consider_all_requests_local }, :if => lambda { action_dispatch.show_exceptions })
- middleware.use("::ActionDispatch::RemoteIp", lambda { action_dispatch.ip_spoofing_check }, lambda { action_dispatch.trusted_proxies })
+ middleware.use('::ActionDispatch::RemoteIp', lambda { action_dispatch.ip_spoofing_check }, lambda { action_dispatch.trusted_proxies })
middleware.use('::Rack::Sendfile', lambda { action_dispatch.x_sendfile_header })
middleware.use('::ActionDispatch::Callbacks', lambda { !cache_classes })
middleware.use('::ActionDispatch::Cookies')
diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb
index 12748da18b..de93a87615 100644
--- a/railties/lib/rails/commands.rb
+++ b/railties/lib/rails/commands.rb
@@ -1,8 +1,50 @@
-if ARGV.empty?
- ARGV << '--help'
-end
+ARGV << '--help' if ARGV.empty?
-HELP_TEXT = <<-EOT
+aliases = {
+ "g" => "generate",
+ "c" => "console",
+ "s" => "server",
+ "db" => "dbconsole"
+}
+
+command = ARGV.shift
+command = aliases[command] || command
+
+case command
+when 'generate', 'destroy', 'plugin', 'benchmarker', 'profiler'
+ require APP_PATH
+ Rails::Application.require_environment!
+ require "rails/commands/#{command}"
+
+when 'console'
+ require 'rails/commands/console'
+ require APP_PATH
+ Rails::Application.require_environment!
+ Rails::Console.start(Rails::Application)
+
+when 'server'
+ require 'rails/commands/server'
+ Rails::Server.new.tap { |server|
+ require APP_PATH
+ Dir.chdir(Rails::Application.root)
+ server.start
+ }
+
+when 'dbconsole'
+ require 'rails/commands/dbconsole'
+ require APP_PATH
+ Rails::DBConsole.start(Rails::Application)
+
+when 'application', 'runner'
+ require "rails/commands/#{command}"
+
+when '--version', '-v'
+ ARGV.unshift '--version'
+ require 'rails/commands/application'
+
+else
+ puts "Error: Command not recognized" unless %w(-h --help).include?(command)
+ puts <<-EOT
Usage: rails COMMAND [ARGS]
The most common rails commands are:
@@ -21,53 +63,5 @@ In addition to those, there are:
runner Run a piece of code in the application environment
All commands can be run with -h for more information.
-EOT
-
-
-case ARGV.shift
-when 'g', 'generate'
- require ENV_PATH
- require 'rails/commands/generate'
-when 'c', 'console'
- require 'rails/commands/console'
- require ENV_PATH
- Rails::Console.start(Rails::Application)
-when 's', 'server'
- require 'rails/commands/server'
- # Initialize the server first, so environment options are set
- server = Rails::Server.new
- require APP_PATH
-
- Dir.chdir(Rails::Application.root)
- server.start
-when 'db', 'dbconsole'
- require 'rails/commands/dbconsole'
- require APP_PATH
- Rails::DBConsole.start(Rails::Application)
-
-when 'application'
- require 'rails/commands/application'
-when 'destroy'
- require ENV_PATH
- require 'rails/commands/destroy'
-when 'benchmarker'
- require ENV_PATH
- require 'rails/commands/performance/benchmarker'
-when 'profiler'
- require ENV_PATH
- require 'rails/commands/performance/profiler'
-when 'plugin'
- require APP_PATH
- require 'rails/commands/plugin'
-when 'runner'
- require 'rails/commands/runner'
-
-when '--help', '-h'
- puts HELP_TEXT
-when '--version', '-v'
- ARGV.unshift '--version'
- require 'rails/commands/application'
-else
- puts "Error: Command not recognized"
- puts HELP_TEXT
-end
+ EOT
+end \ No newline at end of file
diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb
index 438c976b35..8a8143e00e 100644
--- a/railties/lib/rails/commands/application.rb
+++ b/railties/lib/rails/commands/application.rb
@@ -10,4 +10,4 @@ require 'rubygems' if ARGV.include?("--dev")
require 'rails/generators'
require 'rails/generators/rails/app/app_generator'
-Rails::Generators::AppGenerator.start \ No newline at end of file
+Rails::Generators::AppGenerator.start
diff --git a/railties/lib/rails/commands/performance/benchmarker.rb b/railties/lib/rails/commands/benchmarker.rb
index 0432261802..0432261802 100644
--- a/railties/lib/rails/commands/performance/benchmarker.rb
+++ b/railties/lib/rails/commands/benchmarker.rb
diff --git a/railties/lib/rails/commands/performance/profiler.rb b/railties/lib/rails/commands/profiler.rb
index 6d9717b5cd..6d9717b5cd 100644
--- a/railties/lib/rails/commands/performance/profiler.rb
+++ b/railties/lib/rails/commands/profiler.rb
diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb
index 1dd11e1241..278548558e 100644
--- a/railties/lib/rails/commands/runner.rb
+++ b/railties/lib/rails/commands/runner.rb
@@ -36,7 +36,8 @@ ARGV.delete(code_or_file)
ENV["RAILS_ENV"] = options[:environment]
-require ENV_PATH
+require APP_PATH
+Rails::Application.require_environment!
begin
if code_or_file.nil?
diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb
index dfd849b4bb..bd404f4a14 100644
--- a/railties/lib/rails/configuration.rb
+++ b/railties/lib/rails/configuration.rb
@@ -1,3 +1,4 @@
+require 'active_support/deprecation'
require 'active_support/ordered_options'
require 'rails/paths'
require 'rails/rack'
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 36fcc896ae..ab0ead65a9 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -42,7 +42,7 @@ module Rails
# config.load_paths << File.expand_path("../lib/some/path", __FILE__)
#
# initializer "my_engine.add_middleware" do |app|
- # app.middlewares.use MyEngine::Middleware
+ # app.middleware.use MyEngine::Middleware
# end
# end
#
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index 0da85ea4a4..766644bbc2 100644
--- a/railties/lib/rails/generators/base.rb
+++ b/railties/lib/rails/generators/base.rb
@@ -20,24 +20,19 @@ module Rails
add_runtime_options!
- # Automatically sets the source root based on the class name.
- #
- def self.source_root
- @_rails_source_root ||= begin
- if base_name && generator_name
- File.expand_path(File.join(base_name, generator_name, 'templates'), File.dirname(__FILE__))
- end
- end
+ # Returns the source root for this generator using default_source_root as default.
+ def self.source_root(path=nil)
+ @_source_root = path if path
+ @_source_root ||= default_source_root
end
# Tries to get the description from a USAGE file one folder above the source
# root otherwise uses a default description.
- #
def self.desc(description=nil)
return super if description
- usage = File.expand_path(File.join(source_root, "..", "USAGE"))
+ usage = source_root && File.expand_path("../USAGE", source_root)
- @desc ||= if File.exist?(usage)
+ @desc ||= if usage && File.exist?(usage)
File.read(usage)
else
"Description:\n Create #{base_name.humanize.downcase} files for #{generator_name} generator."
@@ -47,7 +42,6 @@ module Rails
# Convenience method to get the namespace from the class name. It's the
# same as Thor default except that the Generator at the end of the class
# is removed.
- #
def self.namespace(name=nil)
return super if name
@namespace ||= super.sub(/_generator$/, '').sub(/:generators:/, ':')
@@ -200,7 +194,6 @@ module Rails
end
# Make class option aware of Rails::Generators.options and Rails::Generators.aliases.
- #
def self.class_option(name, options={}) #:nodoc:
options[:desc] = "Indicates when to generate #{name.to_s.humanize.downcase}" unless options.key?(:desc)
options[:aliases] = default_aliases_for_option(name, options)
@@ -208,14 +201,27 @@ module Rails
super(name, options)
end
+ # Returns the default source root for a given generator. This is used internally
+ # by rails to set its generators source root. If you want to customize your source
+ # root, you should use source_root.
+ def self.default_source_root
+ return unless base_name && generator_name
+ path = File.expand_path(File.join(base_name, generator_name, 'templates'), base_root)
+ path if File.exists?(path)
+ end
+
+ # Returns the base root for a common set of generators. This is used to dynamically
+ # guess the default source root.
+ def self.base_root
+ File.dirname(__FILE__)
+ end
+
# Cache source root and add lib/generators/base/generator/templates to
# source paths.
- #
def self.inherited(base) #:nodoc:
super
- # Cache source root, we need to do this, since __FILE__ is a relative value
- # and can point to wrong directions when inside an specified directory.
+ # Invoke source_root so the default_source_root is set.
base.source_root
if base.name && base.name !~ /Base$/
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index aa066fe3c4..667a123025 100644
--- a/railties/lib/rails/generators/rails/app/app_generator.rb
+++ b/railties/lib/rails/generators/rails/app/app_generator.rb
@@ -55,6 +55,7 @@ module Rails::Generators
:desc => "Show this help message and quit"
def initialize(*args)
+ raise Error, "Options should be given after the application name. For details run: rails --help" if args[0].blank?
super
if !options[:skip_activerecord] && !DATABASES.include?(options[:database])
raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}."
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
index f600e054cf..4e6391e3d6 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
@@ -1,11 +1,11 @@
# PostgreSQL. Versions 7.4 and 8.x are supported.
#
-# Install the ruby-postgres driver:
-# gem install ruby-postgres
-# On Mac OS X:
-# gem install ruby-postgres -- --include=/usr/local/pgsql
+# Install the pg driver:
+# gem install pg
+# On Mac OS X with macports:
+# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
# On Windows:
-# gem install ruby-postgres
+# gem install pg
# Choose the win32 build.
# Install PostgreSQL and put its /bin directory on your path.
development:
diff --git a/railties/lib/rails/generators/rails/app/templates/script/rails b/railties/lib/rails/generators/rails/app/templates/script/rails
index b01d1ee183..11bc1edde9 100644
--- a/railties/lib/rails/generators/rails/app/templates/script/rails
+++ b/railties/lib/rails/generators/rails/app/templates/script/rails
@@ -1,8 +1,5 @@
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
-ENV_PATH = File.expand_path('../../config/environment', __FILE__)
-BOOT_PATH = File.expand_path('../../config/boot', __FILE__)
-APP_PATH = File.expand_path('../../config/application', __FILE__)
-
-require BOOT_PATH
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require File.expand_path('../../config/boot', __FILE__)
require 'rails/commands'
diff --git a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt
index d8757460e4..d0575772bc 100644
--- a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt
+++ b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt
@@ -1,5 +1,3 @@
class <%= class_name %>Generator < Rails::Generators::NamedBase
- def self.source_root
- @source_root ||= File.expand_path('../templates', __FILE__)
- end
+ source_root File.expand_path('../templates', __FILE__)
end
diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb
index 6ac6be092e..b6b57bc5b5 100644
--- a/railties/lib/rails/railtie.rb
+++ b/railties/lib/rails/railtie.rb
@@ -1,6 +1,7 @@
require 'rails/initializable'
require 'rails/configuration'
require 'active_support/inflector'
+require 'active_support/deprecation'
module Rails
# Railtie is the core of the Rails Framework and provides several hooks to extend
diff --git a/railties/railties.gemspec b/railties/railties.gemspec
index b9278c0399..99537d6d24 100644
--- a/railties/railties.gemspec
+++ b/railties/railties.gemspec
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
s.has_rdoc = false
s.add_dependency('rake', '>= 0.8.3')
- s.add_dependency('thor', '~> 0.13.4')
+ s.add_dependency('thor', '~> 0.13.6')
s.add_dependency('activesupport', version)
s.add_dependency('actionpack', version)
end
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index 27374dcb28..d08f04bddb 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -20,20 +20,21 @@ module ApplicationTests
assert_equal [
"ActionDispatch::Static",
"Rack::Lock",
+ "ActiveSupport::Cache::Strategy::LocalCache",
"Rack::Runtime",
"Rails::Rack::Logger",
"ActionDispatch::ShowExceptions",
"ActionDispatch::RemoteIp",
"Rack::Sendfile",
"ActionDispatch::Callbacks",
+ "ActiveRecord::ConnectionAdapters::ConnectionManagement",
+ "ActiveRecord::QueryCache",
"ActionDispatch::Cookies",
"ActionDispatch::Session::CookieStore",
"ActionDispatch::Flash",
"ActionDispatch::ParamsParser",
"Rack::MethodOverride",
- "ActionDispatch::Head",
- "ActiveRecord::ConnectionAdapters::ConnectionManagement",
- "ActiveRecord::QueryCache"
+ "ActionDispatch::Head"
], middleware
end
diff --git a/railties/test/application/model_initialization_test.rb b/railties/test/application/model_initialization_test.rb
new file mode 100644
index 0000000000..6a22f8d8df
--- /dev/null
+++ b/railties/test/application/model_initialization_test.rb
@@ -0,0 +1,33 @@
+require 'isolation/abstract_unit'
+
+class PostTest < Test::Unit::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ boot_rails
+ end
+
+ def test_reload_should_reload_constants
+ app_file "app/models/post.rb", <<-MODEL
+ class Post < ActiveRecord::Base
+ validates_acceptance_of :title, :accept => "omg"
+ end
+ MODEL
+
+ require "#{rails_root}/config/environment"
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
+ ActiveRecord::Migration.verbose = false
+ ActiveRecord::Schema.define(:version => 1) do
+ create_table :posts do |t|
+ t.string :title
+ end
+ end
+
+ p = Post.create(:title => 'omg')
+ assert_equal 1, Post.count
+ assert_equal 'omg', p.title
+ p = Post.first
+ assert_equal 'omg', p.title
+ end
+end
diff --git a/railties/test/application/paths_test.rb b/railties/test/application/paths_test.rb
index 5ab558c026..978d677efc 100644
--- a/railties/test/application/paths_test.rb
+++ b/railties/test/application/paths_test.rb
@@ -48,7 +48,8 @@ module ApplicationTests
assert_path @paths.tmp.cache, "tmp", "cache"
assert_path @paths.config, "config"
assert_path @paths.config.locales, "config", "locales", "en.yml"
- assert_path @paths.config.environment, "config", "environments", "development.rb"
+ assert_path @paths.config.environment, "config", "environment.rb"
+ assert_path @paths.config.environments, "config", "environments", "development.rb"
assert_equal root("app", "controllers"), @paths.app.controllers.to_a.first
end
@@ -61,7 +62,7 @@ module ApplicationTests
end
test "environments has a glob equal to the current environment" do
- assert_equal "#{Rails.env}.rb", @paths.config.environment.glob
+ assert_equal "#{Rails.env}.rb", @paths.config.environments.glob
end
test "load path includes each of the paths in config.paths as long as the directories exist" do
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index bf2da866f4..6b7a471494 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -19,5 +19,19 @@ module ApplicationTests
::Rails.application.load_tasks
assert $task_loaded
end
+
+ def test_environment_is_required_in_rake_tasks
+ app_file "config/environment.rb", <<-RUBY
+ SuperMiddleware = Struct.new(:app)
+
+ Rails::Application.configure do
+ config.middleware.use SuperMiddleware
+ end
+
+ Rails::Application.initialize!
+ RUBY
+
+ assert_match "SuperMiddleware", Dir.chdir(app_path){ `rake middleware` }
+ end
end
end \ No newline at end of file
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 44e0640552..e6fab93a87 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -209,7 +209,7 @@ class ActionsTest < Rails::Generators::TestCase
def test_readme
run_generator
- Rails::Generators::AppGenerator.expects(:source_root).returns(destination_root)
+ Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root)
assert_match(/Welcome to Rails/, action(:readme, "README"))
end
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 24e6d541c2..2e402c4f3a 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -58,6 +58,11 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_file "public/stylesheets/application.css"
end
+ def test_options_before_application_name_raises_an_error
+ content = capture(:stderr){ run_generator(["--skip-activerecord", destination_root]) }
+ assert_equal "Options should be given after the application name. For details run: rails --help\n", content
+ end
+
def test_name_collision_raises_an_error
content = capture(:stderr){ run_generator [File.join(destination_root, "generate")] }
assert_equal "Invalid application name generate. Please give a name which does not match one of the reserved rails words.\n", content
@@ -152,7 +157,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
template = %{ say "It works!" }
template.instance_eval "def read; self; end" # Make the string respond to read
- generator([destination_root], :template => path).expects(:open).with(path).returns(template)
+ generator([destination_root], :template => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
assert_match /It works!/, silence(:stdout){ generator.invoke }
end
diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb
index 81d6afa221..850b45ff74 100644
--- a/railties/test/generators/mailer_generator_test.rb
+++ b/railties/test/generators/mailer_generator_test.rb
@@ -1,5 +1,6 @@
require 'generators/generators_test_helper'
-require 'rails/generators/rails/mailer/mailer_generator'
+require 'rails/generators/mailer/mailer_generator'
+
class MailerGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper