diff options
67 files changed, 682 insertions, 172 deletions
diff --git a/.gitignore b/.gitignore index 854fdbf450..a3a5304ecd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,7 @@ debug.log .Gemfile /.bundle -/.rbenv-version -/.rvmrc +/.ruby-version /Gemfile.lock /pkg /dist diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 69ed117a03..6940683c8c 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,49 @@ ## Rails 4.0.0 (unreleased) ## +* Change `image_alt` method to replace underscores/hyphens to spaces in filenames. + + Previously, underscored filenames became `alt="A_long_file_name_with_underscores"` + in HTML, which is poor for accessibility. For instance, Apple's VoiceOver Utility + pronounces each underscore. `A_long_file_name` thus would be read as `A underscore + long underscore file underscore name.` Now underscored or hyphenated filenames + (both of which are very popular naming conventions) read more naturally in + screen readers by converting both hyphens and underscores to spaces. + + Before: + image_tag('underscored_file_name.png') + # => <img alt="Underscored_file_name" src="/assets/underscored_file_name.png" /> + + After: + image_tag('underscored_file_name.png') + # => <img alt="Underscored file name" src="/assets/underscored_file_name.png" /> + + *Nick Cox* + +* We don't support the `:controller` option for route definitions + with the ruby constant notation. This will now result in an + `ArgumentError`. + + Example: + # This raises an ArgumentError: + resources :posts, :controller => "Admin::Posts" + + # Use directory notation instead: + resources :posts, :controller => "admin/posts" + + *Yves Senn* + +* `assert_template` can be used to verify the locals of partials, + which live inside a directory. + Fixes #8516. + + # Prefixed partials inside directories worked and still work. + assert_template partial: 'directory/_partial', locals: {name: 'John'} + + # This did not work but does now. + assert_template partial: 'directory/partial', locals: {name: 'John'} + + *Yves Senn* + * Fix `content_tag_for` with array html option. It would embed array as string instead of joining it like `content_tag` does: @@ -248,7 +292,7 @@ * More descriptive error messages when calling `render :partial` with an invalid `:layout` argument. - + Fixes #8376. render partial: 'partial', layout: true diff --git a/actionpack/RUNNING_UNIT_TESTS b/actionpack/RUNNING_UNIT_TESTS.rdoc index 1b29abd2d1..1b29abd2d1 100644 --- a/actionpack/RUNNING_UNIT_TESTS +++ b/actionpack/RUNNING_UNIT_TESTS.rdoc diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 70a1975ee9..03eeb841ee 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -21,8 +21,8 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'builder', '~> 3.1.0' - s.add_dependency 'rack', '~> 1.5.0' - s.add_dependency 'rack-test', '~> 0.6.1' + s.add_dependency 'rack', '~> 1.5.2' + s.add_dependency 'rack-test', '~> 0.6.2' s.add_dependency 'erubis', '~> 2.7.0' s.add_development_dependency 'activemodel', version diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 77b173979e..17379cf7ac 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -126,7 +126,7 @@ module ActionController #:nodoc: host = request.host secure = request.ssl? - new(key_generator, host, secure) + new(key_generator, host, secure, options_for_env({})) end def write(*) diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 5ae5dd331a..bba1f1e201 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -126,7 +126,11 @@ module ActionController if expected_partial = options[:partial] if expected_locals = options[:locals] if defined?(@_rendered_views) - view = expected_partial.to_s.sub(/^_/,'') + view = expected_partial.to_s.sub(/^_/, '').sub(/\/_(?=[^\/]+\z)/, '/') + + partial_was_not_rendered_msg = "expected %s to be rendered but it was not." % view + assert_includes @_rendered_views.rendered_views, view, partial_was_not_rendered_msg + msg = 'expecting %s to be rendered with %s but was with %s' % [expected_partial, expected_locals, @_rendered_views.locals_for(view)] diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index e7e8905d7e..ca1ace4537 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -170,7 +170,7 @@ module ActionDispatch # :nodoc: alias_method :status_message, :message def respond_to?(method) - if method.to_sym == :to_path + if method.to_s == 'to_path' stream.respond_to?(:to_path) else super diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 43f26d696d..e9ef9ee9f2 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -32,8 +32,12 @@ module ActionDispatch params.reject! { |_,v| v.to_param.nil? } result = build_host_url(options) - if options[:trailing_slash] && !path.ends_with?('/') - result << path.sub(/(\?|\z)/) { "/" + $& } + if options[:trailing_slash] + if path.include?('?') + result << path.sub(/\?/, '/\&') + else + result << path.sub(/[^\/]\z|\A\z/, '\&/') + end else result << path end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 0f02d230d4..fff26bd1b2 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/key_generator' require 'active_support/message_verifier' module ActionDispatch @@ -110,13 +111,17 @@ module ActionDispatch # $& => example.local DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ + def self.options_for_env(env) #:nodoc: + { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', + encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', + encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', + token_key: env[TOKEN_KEY] } + end + def self.build(request) env = request.env key_generator = env[GENERATOR_KEY] - options = { signed_cookie_salt: env[SIGNED_COOKIE_SALT], - encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT], - encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT], - token_key: env[TOKEN_KEY] } + options = options_for_env env host = request.host secure = request.ssl? diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 82ef1d0333..0a41ed0fcf 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -246,6 +246,12 @@ module ActionDispatch raise ArgumentError, "missing :action" end + if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/ + message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems." + message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" + raise ArgumentError, message + end + hash = {} hash[:controller] = controller unless controller.blank? hash[:action] = action unless action.blank? diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 5b3a2cae7c..31e37893c6 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -214,10 +214,24 @@ module ActionView end # Returns a string suitable for an html image tag alt attribute. - # +src+ is meant to be an image file path. - # It removes the basename of the file path and the digest, if any. + # The +src+ argument is meant to be an image file path. + # The method removes the basename of the file path and the digest, + # if any. It also removes hyphens and underscores from file names and + # replaces them with spaces, returning a space-separated, titleized + # string. + # + # ==== Examples + # + # image_tag('rails.png') + # # => <img alt="Rails" src="/assets/rails.png" /> + # + # image_tag('hyphenated-file-name.png') + # # => <img alt="Hyphenated file name" src="/assets/hyphenated-file-name.png" /> + # + # image_tag('underscored_file_name.png') + # # => <img alt="Underscored file name" src="/assets/underscored_file_name.png" /> def image_alt(src) - File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').capitalize + File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize end # Returns an html video tag for the +sources+. If +sources+ is a string, diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 4479da5bc4..463f192d0c 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -122,7 +122,7 @@ module ActionView class RenderedViewsCollection def initialize - @rendered_views ||= {} + @rendered_views ||= Hash.new { |hash, key| hash[key] = [] } end def add(view, locals) @@ -134,6 +134,10 @@ module ActionView @rendered_views[view] end + def rendered_views + @rendered_views.keys + end + def view_rendered?(view, expected_locals) locals_for(view).any? do |actual_locals| expected_locals.all? {|key, value| value == actual_locals[key] } diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 7571192f97..c272e785c2 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -66,6 +66,19 @@ class RequestForgeryProtectionControllerUsingException < ActionController::Base protect_from_forgery :only => %w(index meta), :with => :exception end +class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base + protect_from_forgery :with => :null_session + + def signed + cookies.signed[:foo] = 'bar' + render :nothing => true + end + + def encrypted + cookies.encrypted[:foo] = 'bar' + render :nothing => true + end +end class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession self.allow_forgery_protection = false @@ -287,6 +300,28 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController end end +class NullSessionDummyKeyGenerator + def generate_key(secret) + '03312270731a2ed0d11ed091c2338a06' + end +end + +class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase + def setup + @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new + end + + test 'should allow to set signed cookies' do + post :signed + assert_response :ok + end + + test 'should allow to set encrypted cookies' do + post :encrypted + assert_response :ok + end +end + class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase include RequestForgeryProtectionTests def assert_blocked diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 8a01b29340..91810864d5 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -13,6 +13,9 @@ class RequestTest < ActiveSupport::TestCase assert_equal '/books', url_for(:only_path => true, :path => '/books') + assert_equal 'http://www.example.com/books/?q=code', url_for(trailing_slash: true, path: '/books?q=code') + assert_equal 'http://www.example.com/books/?spareslashes=////', url_for(trailing_slash: true, path: '/books?spareslashes=////') + assert_equal 'http://www.example.com', url_for assert_equal 'http://api.example.com', url_for(:subdomain => 'api') assert_equal 'http://example.com', url_for(:subdomain => false) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 9f31ce8127..143733254b 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1031,6 +1031,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'users/home#index', @response.body end + def test_namespace_containing_numbers + draw do + namespace :v2 do + resources :subscriptions + end + end + + get '/v2/subscriptions' + assert_equal 'v2/subscriptions#index', @response.body + assert_equal '/v2/subscriptions', v2_subscriptions_path + end + def test_articles_with_id draw do controller :articles do @@ -2833,21 +2845,52 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest end end - DefaultScopeRoutes = ActionDispatch::Routing::RouteSet.new - DefaultScopeRoutes.draw do - namespace :admin do - resources :storage_files, :controller => "StorageFiles" - end + def draw(&block) + @app = ActionDispatch::Routing::RouteSet.new + @app.draw(&block) end - def app - DefaultScopeRoutes - end + def test_valid_controller_options_inside_namespace + draw do + namespace :admin do + resources :storage_files, :controller => "storage_files" + end + end - def test_controller_options get '/admin/storage_files' assert_equal "admin/storage_files#index", @response.body end + + def test_resources_with_valid_namespaced_controller_option + draw do + resources :storage_files, :controller => 'admin/storage_files' + end + + get 'storage_files' + assert_equal "admin/storage_files#index", @response.body + end + + def test_warn_with_ruby_constant_syntax_controller_option + e = assert_raise(ArgumentError) do + draw do + namespace :admin do + resources :storage_files, :controller => "StorageFiles" + end + end + end + + assert_match "'admin/StorageFiles' is not a supported controller name", e.message + end + + def test_warn_with_ruby_constant_syntax_namespaced_controller_option + e = assert_raise(ArgumentError) do + draw do + resources :storage_files, :controller => 'Admin::StorageFiles' + end + end + + assert_match "'Admin::StorageFiles' is not a supported controller name", e.message + end end class TestDefaultScope < ActionDispatch::IntegrationTest diff --git a/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb b/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb new file mode 100644 index 0000000000..1cc8d41475 --- /dev/null +++ b/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb @@ -0,0 +1 @@ +Hello <%= name %> diff --git a/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb b/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb new file mode 100644 index 0000000000..1461b95186 --- /dev/null +++ b/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb @@ -0,0 +1 @@ +<%= render partial: 'test/_directory/partial_with_locales', locals: {'name' => 'Jane'} %> diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 82c9d383ac..11614a45dc 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -443,7 +443,8 @@ class AssetTagHelperTest < ActionView::TestCase [nil, '/', '/foo/bar/', 'foo/bar/'].each do |prefix| assert_equal 'Rails', image_alt("#{prefix}rails.png") assert_equal 'Rails', image_alt("#{prefix}rails-9c0a079bdd7701d7e729bd956823d153.png") - assert_equal 'Avatar-0000', image_alt("#{prefix}avatar-0000.png") + assert_equal 'Long file name with hyphens', image_alt("#{prefix}long-file-name-with-hyphens.png") + assert_equal 'Long file name with underscores', image_alt("#{prefix}long_file_name_with_underscores.png") end end diff --git a/actionpack/test/template/test_case_test.rb b/actionpack/test/template/test_case_test.rb index c7231d9cd5..acd002ce73 100644 --- a/actionpack/test/template/test_case_test.rb +++ b/actionpack/test/template/test_case_test.rb @@ -329,6 +329,27 @@ module ActionView assert_template partial: '_partial', locals: { 'second' => '2' } end + test 'raises descriptive error message when template was not rendered' do + controller.controller_path = "test" + render(template: "test/hello_world_with_partial") + e = assert_raise ActiveSupport::TestCase::Assertion do + assert_template partial: 'i_was_never_rendered', locals: { 'did_not' => 'happen' } + end + assert_match "i_was_never_rendered to be rendered but it was not.", e.message + assert_match 'Expected ["/test/partial"] to include "i_was_never_rendered"', e.message + end + + test 'specifying locals works when the partial is inside a directory with underline prefix' do + controller.controller_path = "test" + render(template: 'test/render_partial_inside_directory') + assert_template partial: 'test/_directory/_partial_with_locales', locals: { 'name' => 'Jane' } + end + + test 'specifying locals works when the partial is inside a directory without underline prefix' do + controller.controller_path = "test" + render(template: 'test/render_partial_inside_directory') + assert_template partial: 'test/_directory/partial_with_locales', locals: { 'name' => 'Jane' } + end end module AHelperWithInitialize diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 1a40ca8efc..8b2f886cc4 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -5,13 +5,12 @@ require 'models/topic' require 'models/person' class LengthValidationTest < ActiveModel::TestCase - def teardown Topic.reset_callbacks(:validate) end def test_validates_length_of_with_allow_nil - Topic.validates_length_of( :title, :is => 5, :allow_nil => true ) + Topic.validates_length_of( :title, is: 5, allow_nil: true ) assert Topic.new("title" => "ab").invalid? assert Topic.new("title" => "").invalid? @@ -20,7 +19,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_allow_blank - Topic.validates_length_of( :title, :is => 5, :allow_blank => true ) + Topic.validates_length_of( :title, is: 5, allow_blank: true ) assert Topic.new("title" => "ab").invalid? assert Topic.new("title" => "").valid? @@ -29,7 +28,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_minimum - Topic.validates_length_of :title, :minimum => 5 + Topic.validates_length_of :title, minimum: 5 t = Topic.new("title" => "valid", "content" => "whatever") assert t.valid? @@ -51,13 +50,13 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_maximum_should_allow_nil - Topic.validates_length_of :title, :maximum => 10 + Topic.validates_length_of :title, maximum: 10 t = Topic.new assert t.valid? end def test_optionally_validates_length_of_using_minimum - Topic.validates_length_of :title, :minimum => 5, :allow_nil => true + Topic.validates_length_of :title, minimum: 5, allow_nil: true t = Topic.new("title" => "valid", "content" => "whatever") assert t.valid? @@ -67,7 +66,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_maximum - Topic.validates_length_of :title, :maximum => 5 + Topic.validates_length_of :title, maximum: 5 t = Topic.new("title" => "valid", "content" => "whatever") assert t.valid? @@ -82,7 +81,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_optionally_validates_length_of_using_maximum - Topic.validates_length_of :title, :maximum => 5, :allow_nil => true + Topic.validates_length_of :title, maximum: 5, allow_nil: true t = Topic.new("title" => "valid", "content" => "whatever") assert t.valid? @@ -92,7 +91,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_within - Topic.validates_length_of(:title, :content, :within => 3..5) + Topic.validates_length_of(:title, :content, within: 3..5) t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long") assert t.invalid? @@ -111,7 +110,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_within_with_exclusive_range - Topic.validates_length_of(:title, :within => 4...10) + Topic.validates_length_of(:title, within: 4...10) t = Topic.new("title" => "9 chars!!") assert t.valid? @@ -125,7 +124,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_optionally_validates_length_of_using_within - Topic.validates_length_of :title, :content, :within => 3..5, :allow_nil => true + Topic.validates_length_of :title, :content, within: 3..5, allow_nil: true t = Topic.new('title' => 'abc', 'content' => 'abcd') assert t.valid? @@ -135,7 +134,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_is - Topic.validates_length_of :title, :is => 5 + Topic.validates_length_of :title, is: 5 t = Topic.new("title" => "valid", "content" => "whatever") assert t.valid? @@ -153,7 +152,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_optionally_validates_length_of_using_is - Topic.validates_length_of :title, :is => 5, :allow_nil => true + Topic.validates_length_of :title, is: 5, allow_nil: true t = Topic.new("title" => "valid", "content" => "whatever") assert t.valid? @@ -167,25 +166,25 @@ class LengthValidationTest < ActiveModel::TestCase bigmax = 2 ** 32 bigrange = bigmin...bigmax assert_nothing_raised do - Topic.validates_length_of :title, :is => bigmin + 5 - Topic.validates_length_of :title, :within => bigrange - Topic.validates_length_of :title, :in => bigrange - Topic.validates_length_of :title, :minimum => bigmin - Topic.validates_length_of :title, :maximum => bigmax + Topic.validates_length_of :title, is: bigmin + 5 + Topic.validates_length_of :title, within: bigrange + Topic.validates_length_of :title, in: bigrange + Topic.validates_length_of :title, minimum: bigmin + Topic.validates_length_of :title, maximum: bigmax end end def test_validates_length_of_nasty_params - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is => -6) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within => 6) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum => "a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum => "a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within => "a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is => "a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, is: -6) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, within: 6) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, minimum: "a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, maximum: "a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, within: "a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, is: "a") } end def test_validates_length_of_custom_errors_for_minimum_with_message - Topic.validates_length_of( :title, :minimum => 5, :message => "boo %{count}" ) + Topic.validates_length_of( :title, minimum: 5, message: "boo %{count}" ) t = Topic.new("title" => "uhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -193,7 +192,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_minimum_with_too_short - Topic.validates_length_of( :title, :minimum => 5, :too_short => "hoo %{count}" ) + Topic.validates_length_of( :title, minimum: 5, too_short: "hoo %{count}" ) t = Topic.new("title" => "uhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -201,7 +200,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_maximum_with_message - Topic.validates_length_of( :title, :maximum => 5, :message => "boo %{count}" ) + Topic.validates_length_of( :title, maximum: 5, message: "boo %{count}" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -209,7 +208,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_in - Topic.validates_length_of(:title, :in => 10..20, :message => "hoo %{count}") + Topic.validates_length_of(:title, in: 10..20, message: "hoo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -222,7 +221,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_maximum_with_too_long - Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}" ) + Topic.validates_length_of( :title, maximum: 5, too_long: "hoo %{count}" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -230,21 +229,21 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_both_too_short_and_too_long - Topic.validates_length_of :title, :minimum => 3, :maximum => 5, :too_short => 'too short', :too_long => 'too long' + Topic.validates_length_of :title, minimum: 3, maximum: 5, too_short: 'too short', too_long: 'too long' - t = Topic.new(:title => 'a') + t = Topic.new(title: 'a') assert t.invalid? assert t.errors[:title].any? assert_equal ['too short'], t.errors['title'] - t = Topic.new(:title => 'aaaaaa') + t = Topic.new(title: 'aaaaaa') assert t.invalid? assert t.errors[:title].any? assert_equal ['too long'], t.errors['title'] end def test_validates_length_of_custom_errors_for_is_with_message - Topic.validates_length_of( :title, :is => 5, :message => "boo %{count}" ) + Topic.validates_length_of( :title, is: 5, message: "boo %{count}" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -252,7 +251,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_is_with_wrong_length - Topic.validates_length_of( :title, :is => 5, :wrong_length => "hoo %{count}" ) + Topic.validates_length_of( :title, is: 5, wrong_length: "hoo %{count}" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -260,7 +259,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_minimum_utf8 - Topic.validates_length_of :title, :minimum => 5 + Topic.validates_length_of :title, minimum: 5 t = Topic.new("title" => "一二三四五", "content" => "whatever") assert t.valid? @@ -272,7 +271,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_maximum_utf8 - Topic.validates_length_of :title, :maximum => 5 + Topic.validates_length_of :title, maximum: 5 t = Topic.new("title" => "一二三四五", "content" => "whatever") assert t.valid? @@ -284,7 +283,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_within_utf8 - Topic.validates_length_of(:title, :content, :within => 3..5) + Topic.validates_length_of(:title, :content, within: 3..5) t = Topic.new("title" => "一二", "content" => "12三四五六七") assert t.invalid? @@ -296,12 +295,12 @@ class LengthValidationTest < ActiveModel::TestCase end def test_optionally_validates_length_of_using_within_utf8 - Topic.validates_length_of :title, :within => 3..5, :allow_nil => true + Topic.validates_length_of :title, within: 3..5, allow_nil: true - t = Topic.new(:title => "一二三四五") + t = Topic.new(title: "一二三四五") assert t.valid?, t.errors.inspect - t = Topic.new(:title => "一二三") + t = Topic.new(title: "一二三") assert t.valid?, t.errors.inspect t.title = nil @@ -309,7 +308,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_is_utf8 - Topic.validates_length_of :title, :is => 5 + Topic.validates_length_of :title, is: 5 t = Topic.new("title" => "一二345", "content" => "whatever") assert t.valid? @@ -321,9 +320,9 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_block - Topic.validates_length_of :content, :minimum => 5, :too_short => "Your essay must be at least %{count} words.", - :tokenizer => lambda {|str| str.scan(/\w+/) } - t = Topic.new(:content => "this content should be long enough") + Topic.validates_length_of :content, minimum: 5, too_short: "Your essay must be at least %{count} words.", + tokenizer: lambda {|str| str.scan(/\w+/) } + t = Topic.new(content: "this content should be long enough") assert t.valid? t.content = "not long enough" @@ -333,18 +332,18 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_for_fixnum - Topic.validates_length_of(:approved, :is => 4) + Topic.validates_length_of(:approved, is: 4) - t = Topic.new("title" => "uhohuhoh", "content" => "whatever", :approved => 1) + t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1) assert t.invalid? assert t.errors[:approved].any? - t = Topic.new("title" => "uhohuhoh", "content" => "whatever", :approved => 1234) + t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1234) assert t.valid? end def test_validates_length_of_for_ruby_class - Person.validates_length_of :karma, :minimum => 5 + Person.validates_length_of :karma, minimum: 5 p = Person.new p.karma = "Pix" @@ -359,7 +358,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_for_infinite_maxima - Topic.validates_length_of(:title, :within => 5..Float::INFINITY) + Topic.validates_length_of(:title, within: 5..Float::INFINITY) t = Topic.new("title" => "1234") assert t.invalid? @@ -368,7 +367,7 @@ class LengthValidationTest < ActiveModel::TestCase t.title = "12345" assert t.valid? - Topic.validates_length_of(:author_name, :maximum => Float::INFINITY) + Topic.validates_length_of(:author_name, maximum: Float::INFINITY) assert t.valid? @@ -377,13 +376,13 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_maximum_should_not_allow_nil_when_nil_not_allowed - Topic.validates_length_of :title, :maximum => 10, :allow_nil => false + Topic.validates_length_of :title, maximum: 10, allow_nil: false t = Topic.new assert t.invalid? end def test_validates_length_of_using_maximum_should_not_allow_nil_and_empty_string_when_blank_not_allowed - Topic.validates_length_of :title, :maximum => 10, :allow_blank => false + Topic.validates_length_of :title, maximum: 10, allow_blank: false t = Topic.new assert t.invalid? @@ -392,13 +391,13 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_both_minimum_and_maximum_should_not_allow_nil - Topic.validates_length_of :title, :minimum => 5, :maximum => 10 + Topic.validates_length_of :title, minimum: 5, maximum: 10 t = Topic.new assert t.invalid? end def test_validates_length_of_using_minimum_0_should_not_allow_nil - Topic.validates_length_of :title, :minimum => 0 + Topic.validates_length_of :title, minimum: 0 t = Topic.new assert t.invalid? @@ -407,11 +406,19 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_using_is_0_should_not_allow_nil - Topic.validates_length_of :title, :is => 0 + Topic.validates_length_of :title, is: 0 t = Topic.new assert t.invalid? t.title = "" assert t.valid? end + + def test_validates_with_diff_in_option + Topic.validates_length_of(:title, is: 5) + Topic.validates_length_of(:title, is: 5, if: Proc.new { false } ) + + assert Topic.new("title" => "david").valid? + assert Topic.new("title" => "david2").invalid? + end end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index f8931677ed..4489ca1aff 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,59 @@ ## Rails 4.0.0 (unreleased) ## +* Preloading `has_many :through` associations with conditions won't + cache the `:through` association. This will prevent invalid + subsets to be cached. + Fixes #8423. + + Example: + + class User + has_many :posts + has_many :recent_comments, -> { where('created_at > ?', 1.week.ago) }, :through => :posts + end + + a_user = User.includes(:recent_comments).first + + # this is preloaded + a_user.recent_comments + + # fetching the recent_comments through the posts association won't preload it. + a_user.posts + + *Yves Senn* + +* Don't run after_commit callback when creating through an association + if saving the record fails. + + *James Miller * + +* Allow store accessors to be overrided like other attribute methods, e.g.: + + class User < ActiveRecord::Base + store :settings, accessors: [ :color, :homepage ], coder: JSON + + def color + super || 'red' + end + end + + *Sergey Nartimov* + +* Quote numeric values being compared to non-numeric columns. Otherwise, + in some database, the string column values will be coerced to a numeric + allowing 0, 0.0 or false to match any string starting with a non-digit. + + Example: + + App.where(apikey: 0) # => SELECT * FROM users WHERE apikey = '0' + + *Dylan Smith* + +* Schema dumper supports dumping the enabled database extensions to `schema.rb` + (currently only supported by postgresql). + + *Justin George* + * The `DATABASE_URL` environment variable now converts ints, floats, and the strings true and false to Ruby types. For example, SQLite requires that the timeout value is an integer, and PostgreSQL requires that the @@ -591,7 +645,7 @@ After: - #=> SELECT * FROM users WHERE 1 = 2; + #=> SELECT * FROM users WHERE 1=0; *Damien Mathieu* diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS.rdoc index bdd8834dcb..bdd8834dcb 100644 --- a/activerecord/RUNNING_UNIT_TESTS +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 1c1ba11c44..b0b1c13b0d 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -31,7 +31,8 @@ module ActiveRecord through_records = Array.wrap(owner.send(through_reflection.name)) # Dont cache the association - we would only be caching a subset - if reflection.options[:source_type] && through_reflection.collection? + if (through_scope != through_reflection.klass.unscoped) || + (reflection.options[:source_type] && through_reflection.collection?) owner.association(through_reflection.name).reset end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index d18b9c991f..aec4654eee 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -25,13 +25,19 @@ module ActiveRecord when true, false if column && column.type == :integer value ? '1' : '0' + elsif column && [:text, :string, :binary].include?(column.type) + value ? "'1'" : "'0'" else value ? quoted_true : quoted_false end # BigDecimals need to be put in a non-normalized form and quoted. when nil then "NULL" - when BigDecimal then value.to_s('F') - when Numeric, ActiveSupport::Duration then value.to_s + when Numeric, ActiveSupport::Duration + value = BigDecimal === value ? value.to_s('F') : value.to_s + if column && ![:integer, :float, :decimal].include?(column.type) + value = "'#{value}'" + end + value when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" when Class then "'#{value.to_s}'" diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index b1ec33d06c..f758e19a4f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -252,15 +252,12 @@ module ActiveRecord self end - %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| - class_eval <<-EOV, __FILE__, __LINE__ + 1 - def #{column_type}(*args) # def string(*args) - options = args.extract_options! # options = args.extract_options! - column_names = args # column_names = args - type = :'#{column_type}' # type = :string - column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) } - end # end - EOV + [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| + define_method column_type do |*args| + options = args.extract_options! + column_names = args + column_names.each { |name| column(name, column_type, options) } + end end # Adds index options to the indexes hash, keyed by column name @@ -486,15 +483,13 @@ module ActiveRecord # # t.string(:goat) # t.string(:goat, :sheep) - %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| - class_eval <<-EOV, __FILE__, __LINE__ + 1 - def #{column_type}(*args) # def string(*args) - options = args.extract_options! # options = args.extract_options! - args.each do |name| # column_names.each do |name| - @base.add_column(@table_name, name, :#{column_type}, options) # @base.add_column(@table_name, name, :string, options) - end # end - end # end - EOV + [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| + define_method column_type do |*args| + options = args.extract_options! + args.each do |name| + @base.add_column(@table_name, name, column_type, options) + end + end end private diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index b2ad4e600d..eecf4faa5d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -177,10 +177,16 @@ module ActiveRecord false end + # A list of extensions, to be filled in by databases that + # support them (at the moment, postgresql). + def extensions + [] + end + # QUOTING ================================================== # Returns a bind substitution value given a +column+ and list of current - # +binds+ + # +binds+. def substitute_at(column, index) Arel::Nodes::BindParam.new '?' end @@ -320,6 +326,10 @@ module ActiveRecord # override in derived class ActiveRecord::StatementInvalid.new(message) end + + def valid_types?(type) + true + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 52b0b3fe79..de5232f960 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -212,8 +212,6 @@ module ActiveRecord if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) s = column.class.string_to_binary(value).unpack("H*")[0] "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") else super end @@ -750,6 +748,9 @@ module ActiveRecord execute("SET #{encoding} #{variable_assignments}", :skip_logging) end + def valid_type?(type) + !native_database_types[type].nil? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 747331f3a1..a4b3a0c584 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -74,12 +74,13 @@ module ActiveRecord def type_cast_for_write(value) return value unless number? - if value == false + case value + when FalseClass 0 - elsif value == true + when TrueClass 1 - elsif value.is_a?(String) && value.blank? - nil + when String + value.presence else value end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5ce2f1b04c..271a6848ee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -605,6 +605,15 @@ module ActiveRecord end end + def extensions + if supports_extensions? + res = exec_query "SELECT extname from pg_extension", "SCHEMA" + res.rows.map { |r| res.column_types['extname'].type_cast r.first } + else + super + end + end + # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i @@ -878,6 +887,10 @@ module ActiveRecord def table_definition TableDefinition.new(self) end + + def valid_type?(type) + !native_database_types[type].nil? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7bead4bde9..b644e7bd60 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -603,6 +603,10 @@ module ActiveRecord end end + def valid_type?(type) + true + end + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 537ebbef28..5cd015eba7 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -8,7 +8,7 @@ module ActiveRecord if value.is_a?(Hash) if value.empty? - queries << '1 = 2' + queries << '1=0' else table = Arel::Table.new(column, default_table.engine) association = klass.reflect_on_association(column.to_sym) @@ -98,6 +98,11 @@ module ActiveRecord when Class # FIXME: I think we need to deprecate this behavior attribute.eq(value.name) + when Integer, ActiveSupport::Duration + # Arel treats integers as literals, but they should be quoted when compared with strings + table = attribute.relation + column = table.engine.connection.schema_cache.columns_hash(table.name)[attribute.name.to_s] + attribute.eq(Arel::Nodes::SqlLiteral.new(table.engine.connection.quote(value, column))) else attribute.eq(value) end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 36bde44e7c..df090b972d 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -24,6 +24,7 @@ module ActiveRecord def dump(stream) header(stream) + extensions(stream) tables(stream) trailer(stream) stream @@ -66,6 +67,18 @@ HEADER stream.puts "end" end + def extensions(stream) + return unless @connection.supports_extensions? + extensions = @connection.extensions + if extensions.any? + stream.puts " # These are extensions that must be enabled in order to support this database" + extensions.each do |extension| + stream.puts " enable_extension #{extension.inspect}" + end + stream.puts + end + end + def tables(stream) @connection.tables.sort.each do |tbl| next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index cf4cf9e602..a610f479f2 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -42,21 +42,19 @@ module ActiveRecord # # All stored values are automatically available through accessors on the Active Record # object, but sometimes you want to specialize this behavior. This can be done by overwriting - # the default accessors (using the same name as the attribute) and calling - # <tt>read_store_attribute(store_attribute_name, attr_name)</tt> and - # <tt>write_store_attribute(store_attribute_name, attr_name, value)</tt> to actually - # change things. + # the default accessors (using the same name as the attribute) and calling <tt>super</tt> + # to actually change things. # # class Song < ActiveRecord::Base # # Uses a stored integer to hold the volume adjustment of the song # store :settings, accessors: [:volume_adjustment] # # def volume_adjustment=(decibels) - # write_store_attribute(:settings, :volume_adjustment, decibels.to_i) + # super(decibels.to_i) # end # # def volume_adjustment - # read_store_attribute(:settings, :volume_adjustment).to_i + # super.to_i # end # end module Store @@ -75,19 +73,30 @@ module ActiveRecord def store_accessor(store_attribute, *keys) keys = keys.flatten - keys.each do |key| - define_method("#{key}=") do |value| - write_store_attribute(store_attribute, key, value) - end - define_method(key) do - read_store_attribute(store_attribute, key) + _store_accessors_module.module_eval do + keys.each do |key| + define_method("#{key}=") do |value| + write_store_attribute(store_attribute, key, value) + end + + define_method(key) do + read_store_attribute(store_attribute, key) + end end end self.stored_attributes[store_attribute] ||= [] self.stored_attributes[store_attribute] |= keys end + + def _store_accessors_module + @_store_accessors_module ||= begin + mod = Module.new + include mod + mod + end + end end protected diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 4a608e4f7b..4b7a388dc7 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -286,8 +286,11 @@ module ActiveRecord end # Call the after_commit callbacks + # + # Ensure that it is not called if the object was never persisted (failed create), + # but call it after the commit of a destroyed object def committed! #:nodoc: - run_callbacks :commit + run_callbacks :commit if destroyed? || persisted? ensure clear_transaction_record_state end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 7351ed9013..33c796191e 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -545,13 +545,13 @@ _SQL def test_update_bit_string new_bit_string = '11111111' - new_bit_string_varying = 'FF' + new_bit_string_varying = '11111110' assert @first_bit_string.bit_string = new_bit_string assert @first_bit_string.bit_string_varying = new_bit_string_varying assert @first_bit_string.save assert @first_bit_string.reload - assert_equal @first_bit_string.bit_string, new_bit_string - assert_equal @first_bit_string.bit_string, @first_bit_string.bit_string_varying + assert_equal new_bit_string, @first_bit_string.bit_string + assert_equal new_bit_string_varying, @first_bit_string.bit_string_varying end def test_update_oid diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 9498c829dc..6640f9b497 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -11,11 +11,18 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection + + unless @connection.supports_extensions? + return skip "do not test on PG without hstore" + end + unless @connection.extension_enabled?('hstore') @connection.enable_extension 'hstore' - return skip "do not test on PG without hstore" + @connection.commit_db_transaction end + @connection.reconnect! + @connection.transaction do @connection.create_table('hstores') do |t| t.hstore 'tags', :default => '' @@ -28,6 +35,11 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase @connection.execute 'drop table if exists hstores' end + def test_hstore_included_in_extensions + assert @connection.respond_to?(:extensions), "connection should have a list of extensions" + assert @connection.extensions.include?('hstore'), "extension list should include hstore" + end + def test_hstore_enabled assert @connection.extension_enabled?('hstore') end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index be2d30641e..3bf9125013 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1162,4 +1162,10 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.where('1 = 0').scoping { Comment.preload(:post).find(1).post } ) end + + test "preloading does not cache has many association subset when preloaded with a through association" do + author = Author.includes(:comments_with_order_and_conditions, :posts).first + assert_no_queries { assert_equal 2, author.comments_with_order_and_conditions.size } + assert_no_queries { assert_equal 5, author.posts.size, "should not cache a subset of the association" } + end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index b936cca875..08dbf19e7b 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -399,6 +399,12 @@ class PersistencesTest < ActiveRecord::TestCase assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } end + def test_string_ids + mv = Minivan.where(:minivan_id => 1234).first_or_initialize + assert mv.new_record? + assert_equal '1234', mv.minivan_id + end + def test_update_attribute_with_one_updated t = Topic.first t.update_attribute(:title, 'super_title') diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 3dd11ae89d..0ad05223d4 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -122,35 +122,35 @@ module ActiveRecord def test_quote_float float = 1.2 assert_equal float.to_s, @quoter.quote(float, nil) - assert_equal float.to_s, @quoter.quote(float, Object.new) + assert_equal float.to_s, @quoter.quote(float, FakeColumn.new(:float)) end def test_quote_fixnum fixnum = 1 assert_equal fixnum.to_s, @quoter.quote(fixnum, nil) - assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new) + assert_equal fixnum.to_s, @quoter.quote(fixnum, FakeColumn.new(:integer)) end def test_quote_bignum bignum = 1 << 100 assert_equal bignum.to_s, @quoter.quote(bignum, nil) - assert_equal bignum.to_s, @quoter.quote(bignum, Object.new) + assert_equal bignum.to_s, @quoter.quote(bignum, FakeColumn.new(:integer)) end def test_quote_bigdecimal bigdec = BigDecimal.new((1 << 100).to_s) assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil) - assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new) + assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, FakeColumn.new(:decimal)) end def test_dates_and_times @quoter.extend(Module.new { def quoted_date(value) 'lol' end }) assert_equal "'lol'", @quoter.quote(Date.today, nil) - assert_equal "'lol'", @quoter.quote(Date.today, Object.new) + assert_equal "'lol'", @quoter.quote(Date.today, FakeColumn.new(:date)) assert_equal "'lol'", @quoter.quote(Time.now, nil) - assert_equal "'lol'", @quoter.quote(Time.now, Object.new) + assert_equal "'lol'", @quoter.quote(Time.now, FakeColumn.new(:time)) assert_equal "'lol'", @quoter.quote(DateTime.now, nil) - assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new) + assert_equal "'lol'", @quoter.quote(DateTime.now, FakeColumn.new(:datetime)) end def test_crazy_object diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index c43c7601a2..53cdf89b1f 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -108,5 +108,30 @@ module ActiveRecord assert_equal 4, Edge.where(blank).order("sink_id").to_a.size end end + + def test_where_with_integer_for_string_column + count = Post.where(:title => 0).count + assert_equal 0, count + end + + def test_where_with_float_for_string_column + count = Post.where(:title => 0.0).count + assert_equal 0, count + end + + def test_where_with_boolean_for_string_column + count = Post.where(:title => false).count + assert_equal 0, count + end + + def test_where_with_decimal_for_string_column + count = Post.where(:title => BigDecimal.new(0)).count + assert_equal 0, count + end + + def test_where_with_duration_for_string_column + count = Post.where(:title => 0.seconds).count + assert_equal 0, count + end end end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 7388324a0d..8e6c38706f 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -391,19 +391,19 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_default_scope_with_inheritance wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash assert_equal "Jamis", wheres[:name] - assert_equal 50000, wheres[:salary] + assert_equal Arel.sql("50000"), wheres[:salary] end def test_default_scope_with_module_includes wheres = ModuleIncludedPoorDeveloperCalledJamis.all.where_values_hash assert_equal "Jamis", wheres[:name] - assert_equal 50000, wheres[:salary] + assert_equal Arel.sql("50000"), wheres[:salary] end def test_default_scope_with_multiple_calls wheres = MultiplePoorDeveloperCalledJamis.all.where_values_hash assert_equal "Jamis", wheres[:name] - assert_equal 50000, wheres[:salary] + assert_equal Arel.sql("50000"), wheres[:salary] end def test_scope_overwrites_default diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index cae12e0e3a..bfecc0d1e9 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -1,6 +1,5 @@ require "cases/helper" - class SchemaDumperTest < ActiveRecord::TestCase def setup super @@ -231,6 +230,21 @@ class SchemaDumperTest < ActiveRecord::TestCase end if current_adapter?(:PostgreSQLAdapter) + def test_schema_dump_includes_extensions + connection = ActiveRecord::Base.connection + skip unless connection.supports_extensions? + + connection.stubs(:extensions).returns(['hstore']) + output = standard_dump + assert_match "# These are extensions that must be enabled", output + assert_match %r{enable_extension "hstore"}, output + + connection.stubs(:extensions).returns([]) + output = standard_dump + assert_no_match "# These are extensions that must be enabled", output + assert_no_match %r{enable_extension}, output + end + def test_schema_dump_includes_xml_shorthand_definition output = standard_dump if %r{create_table "postgresql_xml_data_type"} =~ output diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 43bf285ba9..3e32d866ee 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -35,6 +35,12 @@ class StoreTest < ActiveRecord::TestCase assert_equal '(123) 456-7890', @john.phone_number end + test "overriding a read accessor using super" do + @john.settings[:color] = nil + + assert_equal 'red', @john.color + end + test "updating the store will mark it as changed" do @john.color = 'red' assert @john.settings_changed? @@ -66,6 +72,12 @@ class StoreTest < ActiveRecord::TestCase assert_equal '1234567890', @john.settings[:phone_number] end + test "overriding a write accessor using super" do + @john.color = 'yellow' + + assert_equal 'blue', @john.color + end + test "preserve store attributes data in HashWithIndifferentAccess format without any conversion" do @john.json_data = ActiveSupport::HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy') @john.height = 'low' diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 869892e33f..fd5651b4e0 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -5,9 +5,29 @@ class TransactionCallbacksTest < ActiveRecord::TestCase self.use_transactional_fixtures = false fixtures :topics + class ReplyWithCallbacks < ActiveRecord::Base + self.table_name = :topics + + belongs_to :topic, foreign_key: "parent_id" + + validates_presence_of :content + + after_commit :do_after_commit, on: :create + + def history + @history ||= [] + end + + def do_after_commit + history << :commit_on_create + end + end + class TopicWithCallbacks < ActiveRecord::Base self.table_name = :topics + has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id" + 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)} @@ -93,6 +113,13 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [:commit_on_create], @new_record.history end + def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record_if_create_succeeds_creating_through_association + topic = TopicWithCallbacks.create!(:title => "New topic", :written_on => Date.today) + reply = topic.replies.create + + assert_equal [], reply.history + end + def test_call_after_rollback_after_transaction_rollsback @first.after_commit_block{|r| r.history << :after_commit} @first.after_rollback_block{|r| r.history << :after_rollback} diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index 024fede266..4c3b71e8f9 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -27,4 +27,13 @@ class Admin::User < ActiveRecord::Base def phone_number=(value) write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/,'')) end + + def color + super || 'red' + end + + def color=(value) + value = 'blue' unless %w(black red green blue).include?(value) + super + end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index cd9835259a..d789b6cb7a 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -540,6 +540,8 @@ ActiveRecord::Schema.define do create_table :price_estimates, :force => true do |t| t.string :estimate_of_type t.integer :estimate_of_id + t.string :thing_type + t.integer :thing_id t.integer :price end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index a2e6c62c3e..5f7559b5a6 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,17 @@ ## Rails 4.0.0 (unreleased) ## +* ActiveSupport::Gzip.compress allows two optional arguments for compression + level and strategy. + + *Beyond* + +* Modify `TimeWithZone#as_json` to include 3 decimal places of sub-second accuracy + by default, which is optional as per the ISO8601 spec, but extremely useful. Also + the default behaviour of Date#toJSON() in recent versions of Chrome, Safari and + Firefox. + + *James Harton* + * Improve `String#squish` to handle Unicode whitespace. *Antoine Lyset* * Standardise on `to_time` returning an instance of `Time` in the local system timezone @@ -14,8 +26,8 @@ *Yves Senn* -* Hash.from_xml raises when it encounters type="symbol" or type="yaml". - Use Hash.from_trusted_xml to parse this XML. +* `Hash.from_xml` raises when it encounters `type="symbol"` or `type="yaml"`. + Use `Hash.from_trusted_xml` to parse this XML. CVE-2013-0156 diff --git a/activesupport/lib/active_support/gzip.rb b/activesupport/lib/active_support/gzip.rb index 6ef33ab683..b837c879bb 100644 --- a/activesupport/lib/active_support/gzip.rb +++ b/activesupport/lib/active_support/gzip.rb @@ -25,9 +25,9 @@ module ActiveSupport end # Compresses a string using gzip. - def self.compress(source) + def self.compress(source, level=Zlib::DEFAULT_COMPRESSION, strategy=Zlib::DEFAULT_STRATEGY) output = Stream.new - gz = Zlib::GzipWriter.new(output) + gz = Zlib::GzipWriter.new(output, level, strategy) gz.write(source) gz.close output.string diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index 188653bd9b..22521a8e93 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -1,4 +1,7 @@ begin + require 'active_support/core_ext/hash/deep_merge' + require 'active_support/core_ext/hash/except' + require 'active_support/core_ext/hash/slice' require 'i18n' require 'active_support/lazy_load_hooks' rescue LoadError => e diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index f49ca47f14..cbc1608349 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -11,7 +11,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = '6.1.0' + UNICODE_VERSION = '6.2.0' # The default normalization used for operations that require # normalization. It can be set to any of the normalizations diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index ff13efa990..0e6d12a186 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -154,7 +154,7 @@ module ActiveSupport # # => "2005/02/01 15:15:10 +0000" def as_json(options = nil) if ActiveSupport::JSON::Encoding.use_standard_json_time_format - xmlschema + xmlschema(3) else %(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) end diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex df17a8cccf..2571faa019 100644 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ b/activesupport/lib/active_support/values/unicode_tables.dat diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 18eca4cd8b..c2b3676aac 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -75,7 +75,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase def test_to_json_with_use_standard_json_time_format_config_set_to_true old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, true - assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(@twz) + assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(@twz) ensure ActiveSupport.use_standard_json_time_format = old end diff --git a/activesupport/test/gzip_test.rb b/activesupport/test/gzip_test.rb index 75a0505899..0e3cf3b429 100644 --- a/activesupport/test/gzip_test.rb +++ b/activesupport/test/gzip_test.rb @@ -4,6 +4,12 @@ require 'active_support/core_ext/object/blank' class GzipTest < ActiveSupport::TestCase def test_compress_should_decompress_to_the_same_value assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World")) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", Zlib::NO_COMPRESSION)) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", Zlib::BEST_SPEED)) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", Zlib::BEST_COMPRESSION)) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", nil, Zlib::FILTERED)) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", nil, Zlib::HUFFMAN_ONLY)) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", nil, nil)) end def test_compress_should_return_a_binary_string @@ -12,4 +18,16 @@ class GzipTest < ActiveSupport::TestCase assert_equal Encoding.find('binary'), compressed.encoding assert !compressed.blank?, "a compressed blank string should not be blank" end + + def test_compress_should_return_gzipped_string_by_compression_level + source_string = "Hello World"*100 + + gzipped_by_speed = ActiveSupport::Gzip.compress(source_string, Zlib::BEST_SPEED) + assert_equal 1, Zlib::GzipReader.new(StringIO.new(gzipped_by_speed)).level + + gzipped_by_best_compression = ActiveSupport::Gzip.compress(source_string, Zlib::BEST_COMPRESSION) + assert_equal 9, Zlib::GzipReader.new(StringIO.new(gzipped_by_best_compression)).level + + assert_equal true, (gzipped_by_best_compression.bytesize < gzipped_by_speed.bytesize) + end end diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index a678dd9d90..9c157ec0b3 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -3,7 +3,7 @@ Ruby on Rails 4.0 Release Notes Highlights in Rails 4.0: -* Ruby 1.9.3 only +* Ruby 2.0 preferred; 1.9.3+ required * Strong Parameters * Turbolinks * Russian Doll Caching diff --git a/guides/source/engines.md b/guides/source/engines.md index 459aa8d57e..00939c4ff2 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -28,7 +28,7 @@ Engines can also be isolated from their host applications. This means that an ap It's important to keep in mind at all times that the application should **always** take precedence over its engines. An application is the object that has final say in what goes on in the universe (with the universe being the application's environment) where the engine should only be enhancing it, rather than changing it drastically. -To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or [Forem](https://github.com/radar/forem), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/resolve/refinerycms), a CMS engine. +To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or [Forem](https://github.com/radar/forem), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. Finally, engines would not have been possible without the work of James Adam, Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever meet them, don't forget to say thanks! @@ -57,7 +57,7 @@ The `--full` option tells the generator that you want to create an engine, inclu end ``` * A file at `lib/blorgh/engine.rb` which is identical in function to a standard Rails application's `config/application.rb` file: - + ```ruby module Blorgh class Engine < ::Rails::Engine @@ -72,12 +72,12 @@ The `--mountable` option tells the generator that you want to create a "mountabl * A namespaced `ApplicationHelper` stub * A layout view template for the engine * Namespace isolation to `config/routes.rb`: - + ```ruby Blorgh::Engine.routes.draw do end ``` - + * Namespace isolation to `lib/blorgh/engine.rb`: ```ruby @@ -395,7 +395,7 @@ def create @post = Post.find(params[:post_id]) @comment = @post.comments.create(params[:comment]) flash[:notice] = "Comment has been created!" - redirect_to post_path + redirect_to posts_path end ``` @@ -650,6 +650,14 @@ self.author = Blorgh.user_class.find_or_create_by(name: author_name) Resulting in something a little shorter, and more implicit in its behavior. The `user_class` method should always return a `Class` object. +Since we changed the `user_class` method to no longer return a +`String` but a `Class` we must also modify our `belongs_to` definition +in the `Blorgh::Post` model: + +```ruby +belongs_to :author, class_name: Blorgh.user_class.to_s +``` + To set this configuration setting within the application, an initializer should be used. By using an initializer, the configuration will be set up before the application starts and calls the engine's models which may depend on this configuration setting existing. Create a new initializer at `config/initializers/blorgh.rb` inside the application where the `blorgh` engine is installed and put this content in it: @@ -789,7 +797,7 @@ module Blorgh::Concerns::Models::Post extend ActiveSupport::Concern # 'included do' causes the included code to be evaluated in the - # context where it is included (post.rb), rather than be + # context where it is included (post.rb), rather than be # executed in the module's context (blorgh/concerns/models/post). included do attr_accessor :author_name diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index c394f30c38..87f5e43157 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -21,7 +21,7 @@ application from scratch. It does not assume that you have any prior experience with Rails. However, to get the most out of it, you need to have some prerequisites installed: -* The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or higher +* The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer * The [RubyGems](http://rubygems.org/) packaging system * To learn more about RubyGems, please read the [RubyGems User Guide](http://docs.rubygems.org/read/book/1) * A working installation of the [SQLite3 Database](http://www.sqlite.org) @@ -84,7 +84,7 @@ current version of Ruby installed: ```bash $ ruby -v -ruby 1.9.3p327 +ruby 1.9.3p385 ``` To install Rails, use the `gem install` command provided by RubyGems: diff --git a/guides/source/routing.md b/guides/source/routing.md index 14f23d4020..4614169653 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -832,6 +832,19 @@ will recognize incoming paths beginning with `/photos` but route to the `Images` NOTE: Use `photos_path`, `new_photo_path`, etc. to generate paths for this resource. +For namespaced controllers you can use the directory notation. For example: + +```ruby +resources :user_permissions, controller: 'admin/user_permissions' +``` + +This will route to the `Admin::UserPermissions` controller. + +NOTE: Only the directory notation is supported. specifying the +controller with ruby constant notation (eg. `:controller => +'Admin::UserPermissions'`) can lead to routing problems and results in +a warning. + ### Specifying Constraints You can use the `:constraints` option to specify a required format on the implicit `id`. For example: diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index ea04036663..568767d9de 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -16,11 +16,11 @@ The best way to be sure that your application still works after upgrading is to Rails generally stays close to the latest released Ruby version when it's released: -* Rails 3 and above requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. -* Rails 3.2.x will be the last branch to support Ruby 1.8.7. -* Rails 4 will support Ruby 1.9.3 or higher. +* Rails 3 and above require Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially. You should upgrade as early as possible. +* Rails 3.2.x is the last branch to support Ruby 1.8.7. +* Rails 4 prefers Ruby 2.0 and requires 1.9.3 or newer. -TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump on to 1.9.2 or 1.9.3 for smooth sailing. +TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing. Upgrading from Rails 3.2 to Rails 4.0 ------------------------------------- @@ -82,6 +82,8 @@ becomes get 'こんにちは', controller: 'welcome', action: 'index' ``` +* Rails 4.0 has removed ActionDispatch::BestStandardsSupport middleware, !DOCTYPE html already triggers standards mode per http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx and ChromeFrame header has been moved to `config.action_dispatch.default_headers` + ### Active Support Rails 4.0 removes the `j` alias for `ERB::Util#json_escape` since `j` is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript`. diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index f86baee4c3..997858b3c5 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,11 +1,22 @@ ## Rails 4.0.0 (unreleased) ## -* Add --rc option to support the load of a custom rc file during the generation of a new app. +* Add notice message for destroy action in scaffold generator. + + *Rahul P. Chaudhari* + +* Add two new test rake tasks to speed up full test runs. + + * `test:all`: run tests quickly by merging all types and not resetting db. + * `test:all:db`: run tests quickly, but also reset db. + + *Ryan Davis* + +* Add `--rc` option to support the load of a custom rc file during the generation of a new app. *Amparo Luna* -* Add --no-rc option to skip the loading of railsrc file during the generation of a new app. - +* Add `--no-rc` option to skip the loading of railsrc file during the generation of a new app. + *Amparo Luna* * Fixes database.yml when creating a new rails application with '.' diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb index 7d84d3ae0e..2ff29418c6 100644 --- a/railties/lib/rails/commands/application.rb +++ b/railties/lib/rails/commands/application.rb @@ -10,8 +10,12 @@ if ARGV.first != "new" else ARGV.shift unless ARGV.delete("--no-rc") - customrc = ARGV.index('--rc') - railsrc = customrc ? ARGV.slice!(customrc, 2).last : File.join(File.expand_path("~"), '.railsrc') + customrc = ARGV.index{ |x| x.include?("--rc=") } + railsrc = if customrc + File.expand_path(ARGV.delete_at(customrc).gsub(/--rc=/, "")) + else + File.join(File.expand_path("~"), '.railsrc') + end if File.exist?(railsrc) extra_args_string = File.read(railsrc) extra_args = extra_args_string.split(/\n+/).map {|l| l.split}.flatten diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index ca3652c703..99d80b3245 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -20,10 +20,10 @@ module Rails def self.add_shared_options_for(name) class_option :builder, type: :string, aliases: '-b', - desc: "Path to a #{name} builder (can be a filesystem path or URL)" + desc: "Path to some #{name} builder (can be a filesystem path or URL)" class_option :template, type: :string, aliases: '-m', - desc: "Path to an #{name} template (can be a filesystem path or URL)" + desc: "Path to some #{name} template (can be a filesystem path or URL)" class_option :skip_gemfile, type: :boolean, default: false, desc: "Don't create a Gemfile" @@ -61,6 +61,12 @@ module Rails class_option :skip_test_unit, type: :boolean, aliases: '-T', default: false, desc: 'Skip Test::Unit files' + class_option :rc, type: :string, default: false, + desc: "Path to file containing extra configuration options for rails command" + + class_option :no_rc, type: :boolean, default: false, + desc: 'Skip loading of extra configuration options from .railsrc file' + class_option :help, type: :boolean, aliases: '-h', group: :rails, desc: 'Show this help message and quit' end diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb index 5bf98bb6e0..cd69a017dd 100644 --- a/railties/lib/rails/generators/migration.rb +++ b/railties/lib/rails/generators/migration.rb @@ -52,7 +52,7 @@ module Rails if destination && options.force? remove_file(destination) elsif destination - raise Error, "Another migration is already named #{@migration_file_name}: #{destination}" + raise Error, "Another migration is already named #{@migration_file_name}: #{destination}. Use --force to remove the old migration file and replace it." end destination = File.join(migration_dir, "#{@migration_number}_#{@migration_file_name}.rb") end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb index e813437d75..72281a2fef 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -47,7 +47,7 @@ class <%= controller_class_name %>Controller < ApplicationController # DELETE <%= route_url %>/1 def destroy @<%= orm_instance.destroy %> - redirect_to <%= index_helper %>_url + redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully destroyed.'" %> end private diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb index 4536fedaa3..3b7f358a5b 100644 --- a/railties/lib/rails/ruby_version_check.rb +++ b/railties/lib/rails/ruby_version_check.rb @@ -2,12 +2,12 @@ if RUBY_VERSION < '1.9.3' desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" abort <<-end_message - Rails 4 requires Ruby 1.9.3+. + Rails 4 prefers to run on Ruby 2.0. You're running #{desc} - Please upgrade to continue. + Please upgrade to Ruby 1.9.3 or newer to continue. end_message end diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index f0d46fd959..44485d9b14 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -71,6 +71,18 @@ namespace :test do end end + # Inspired by: http://ngauthier.com/2012/02/quick-tests-with-bash.html + desc "Run tests quickly by merging all types and not resetting db" + Rake::TestTask.new(:all) do |t| + t.libs << "test" + t.pattern = "test/**/*_test.rb" + end + + namespace :all do + desc "Run tests quickly, but also reset db" + task :db => %w[db:test:prepare test:all] + end + Rake::TestTask.new(recent: "test:prepare") do |t| since = TEST_CHANGES_SINCE touched = FileList['test/**/*_test.rb'].select { |path| File.mtime(path) > since } + diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index c34ce285e3..013cb78252 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -39,6 +39,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_instance_method :destroy, content do |m| assert_match(/@user\.destroy/, m) + assert_match(/User was successfully destroyed/, m) end assert_instance_method :set_user, content do |m| |