diff options
110 files changed, 983 insertions, 716 deletions
@@ -20,9 +20,9 @@ else end if ENV['AR_DEPRECATED_FINDERS'] - gem 'active_record_deprecated_finders', path: ENV['AR_DEPRECATED_FINDERS'] + gem 'activerecord-deprecated_finders', path: ENV['AR_DEPRECATED_FINDERS'] else - gem 'active_record_deprecated_finders', github: 'rails/active_record_deprecated_finders' + gem 'activerecord-deprecated_finders', github: 'rails/activerecord-deprecated_finders' end # This needs to be with require false to avoid @@ -40,7 +40,7 @@ group :doc do end # AS -gem 'memcache-client', '>= 1.8.5' +gem 'dalli' # Add your own local bundler stuff local_gemfile = File.dirname(__FILE__) + "/.Gemfile" @@ -96,3 +96,5 @@ end # A gem necessary for ActiveRecord tests with IBM DB gem 'ibm_db' if ENV['IBM_DB'] + +gem 'benchmark-ips' diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 32e6183ff6..cfbe2f1cbd 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -35,7 +35,10 @@ require 'active_support/lazy_load_hooks' module ActionMailer extend ::ActiveSupport::Autoload - autoload :Collector + eager_autoload do + autoload :Collector + end + autoload :Base autoload :DeliveryMethods autoload :MailHelper diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 5c03a29f0f..8679096735 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -5,6 +5,7 @@ require "abstract_controller/railties/routes_helpers" module ActionMailer class Railtie < Rails::Railtie config.action_mailer = ActiveSupport::OrderedOptions.new + config.eager_load_namespaces << ActionMailer initializer "action_mailer.logger" do ActiveSupport.on_load(:action_mailer) { self.logger ||= Rails.logger } diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 516fcbe62f..1445cf43f6 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,17 +1,22 @@ ## Rails 4.0.0 (unreleased) ## +* Fix select_tag when option_tags is nil. + Fixes #7404. + + *Sandeep Ravichandran* + * Add Request#formats=(extensions) that lets you set multiple formats directly in a prioritized order *DHH* Example of using this for custom iphone views with an HTML fallback: - class ApplicationController < ActionController::Base - before_filter :adjust_format_for_iphone_with_html_fallback - - private - def adjust_format_for_iphone_with_html_fallback - request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/] - end - end + class ApplicationController < ActionController::Base + before_filter :adjust_format_for_iphone_with_html_fallback + + private + def adjust_format_for_iphone_with_html_fallback + request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/] + end + end * Add Routing Concerns to declare common routes that can be reused inside @@ -51,8 +56,9 @@ *Richard Schneeman* -* Add 'X-Frame-Options' => 'SAMEORIGIN' and - 'X-XSS-Protection' => '1; mode=block' +* Add 'X-Frame-Options' => 'SAMEORIGIN' + 'X-XSS-Protection' => '1; mode=block' and + 'X-Content-Type-Options' => 'nosniff' as default headers. *Egor Homakov* @@ -1000,11 +1006,11 @@ Before: - translate('foo_html', :something => '<script>') # => "...<script>..." + translate('foo_html', :something => '<script>') # => "...<script>..." After: - translate('foo_html', :something => '<script>') # => "...<script>..." + translate('foo_html', :something => '<script>') # => "...<script>..." *Sergey Nartimov* diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index ceb90f8cee..31df9d605c 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -48,6 +48,12 @@ module ActionController eager_autoload do autoload :RecordIdentifier end + + def self.eager_load! + super + ActionController::Caching.eager_load! + HTML.eager_load! + end end # All of these simply register additional autoloads diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 851a2c4aee..3ecc105e22 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -9,6 +9,8 @@ module ActionController class Railtie < Rails::Railtie #:nodoc: config.action_controller = ActiveSupport::OrderedOptions.new + config.eager_load_namespaces << ActionController + initializer "action_controller.assets_config", :group => :all do |app| app.config.action_controller.assets_dir ||= app.config.paths["public"].first end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index cc81970ddc..57b4678add 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -26,7 +26,6 @@ require 'active_support/rails' require 'active_support/core_ext/module/attribute_accessors' require 'action_pack' -require 'active_model' require 'rack' module Rack @@ -39,9 +38,11 @@ module ActionDispatch class IllegalStateError < StandardError end - autoload_under 'http' do - autoload :Request - autoload :Response + eager_autoload do + autoload_under 'http' do + autoload :Request + autoload :Response + end end autoload_under 'middleware' do diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 0dcf1fc4fe..ccc0435a39 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -21,9 +21,12 @@ module ActionDispatch config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', - 'X-XSS-Protection' => '1; mode=block' + 'X-XSS-Protection' => '1; mode=block', + 'X-Content-Type-Options' => 'nosniff' } + config.eager_load_namespaces << ActionDispatch + initializer "action_dispatch.configure" do |app| ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index ea5028a7c0..f64cff8394 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -444,9 +444,10 @@ module ActionDispatch raise "A rack application must be specified" unless path - options[:as] ||= app_name(app) + options[:as] ||= app_name(app) + options[:via] ||= :all - match(path, options.merge(:to => app, :anchor => false, :format => false, :via => :all)) + match(path, options.merge(:to => app, :anchor => false, :format => false)) define_generate_prefix(app, options[:as]) self diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 4bd72c5520..9d11c284f5 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -38,7 +38,6 @@ module ActionView autoload :PathSet autoload :Template - autoload_under "renderer" do autoload :Renderer autoload :AbstractRenderer @@ -77,6 +76,11 @@ module ActionView autoload :TestCase ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*' + + def self.eager_load! + super + ActionView::Template.eager_load! + end end require 'active_support/core_ext/string/output_safety' diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 68b0195700..ceb56824fa 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -209,18 +209,18 @@ module ActionView # * <tt>:title</tt> - Specify the title of the link, defaults to the +type+ # # ==== Examples - # auto_discovery_link_tag # => - # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" /> - # auto_discovery_link_tag(:atom) # => - # <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" /> - # auto_discovery_link_tag(:rss, {:action => "feed"}) # => - # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" /> - # auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"}) # => - # <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.currenthost.com/controller/feed" /> - # auto_discovery_link_tag(:rss, {:controller => "news", :action => "feed"}) # => - # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" /> - # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {:title => "Example RSS"}) # => - # <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" /> + # auto_discovery_link_tag + # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" /> + # auto_discovery_link_tag(:atom) + # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" /> + # auto_discovery_link_tag(:rss, {:action => "feed"}) + # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" /> + # auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"}) + # # => <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.currenthost.com/controller/feed" /> + # auto_discovery_link_tag(:rss, {:controller => "news", :action => "feed"}) + # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" /> + # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {:title => "Example RSS"}) + # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" /> def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) tag( "link", @@ -360,18 +360,18 @@ module ActionView # width="30" and height="45". <tt>:size</tt> will be ignored if the # value is not in the correct format. # - # image_tag("icon") # => - # <img src="/assets/icon" alt="Icon" /> - # image_tag("icon.png") # => - # <img src="/assets/icon.png" alt="Icon" /> - # image_tag("icon.png", :size => "16x10", :alt => "Edit Entry") # => - # <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" /> - # image_tag("/icons/icon.gif", :size => "16x16") # => - # <img src="/icons/icon.gif" width="16" height="16" alt="Icon" /> - # image_tag("/icons/icon.gif", :height => '32', :width => '32') # => - # <img alt="Icon" height="32" src="/icons/icon.gif" width="32" /> - # image_tag("/icons/icon.gif", :class => "menu_icon") # => - # <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> + # image_tag("icon") + # # => <img src="/assets/icon" alt="Icon" /> + # image_tag("icon.png") + # # => <img src="/assets/icon.png" alt="Icon" /> + # image_tag("icon.png", :size => "16x10", :alt => "Edit Entry") + # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" /> + # image_tag("/icons/icon.gif", :size => "16x16") + # # => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" /> + # image_tag("/icons/icon.gif", :height => '32', :width => '32') + # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" /> + # image_tag("/icons/icon.gif", :class => "menu_icon") + # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> def image_tag(source, options={}) options = options.symbolize_keys @@ -408,24 +408,24 @@ module ActionView # width="30" and height="45". <tt>:size</tt> will be ignored if the # value is not in the correct format. # - # video_tag("trailer") # => - # <video src="/videos/trailer" /> - # video_tag("trailer.ogg") # => - # <video src="/videos/trailer.ogg" /> - # video_tag("trailer.ogg", :controls => true, :autobuffer => true) # => - # <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" /> - # video_tag("trailer.m4v", :size => "16x10", :poster => "screenshot.png") # => - # <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" /> - # video_tag("/trailers/hd.avi", :size => "16x16") # => - # <video src="/trailers/hd.avi" width="16" height="16" /> - # video_tag("/trailers/hd.avi", :height => '32', :width => '32') # => - # <video height="32" src="/trailers/hd.avi" width="32" /> - # video_tag("trailer.ogg", "trailer.flv") # => - # <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> - # video_tag(["trailer.ogg", "trailer.flv"]) # => - # <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> - # video_tag(["trailer.ogg", "trailer.flv"], :size => "160x120") # => - # <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> + # video_tag("trailer") + # # => <video src="/videos/trailer" /> + # video_tag("trailer.ogg") + # # => <video src="/videos/trailer.ogg" /> + # video_tag("trailer.ogg", :controls => true, :autobuffer => true) + # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" /> + # video_tag("trailer.m4v", :size => "16x10", :poster => "screenshot.png") + # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" /> + # video_tag("/trailers/hd.avi", :size => "16x16") + # # => <video src="/trailers/hd.avi" width="16" height="16" /> + # video_tag("/trailers/hd.avi", :height => '32', :width => '32') + # # => <video height="32" src="/trailers/hd.avi" width="32" /> + # video_tag("trailer.ogg", "trailer.flv") + # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> + # video_tag(["trailer.ogg", "trailer.flv"]) + # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> + # video_tag(["trailer.ogg", "trailer.flv"], :size => "160x120") + # # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> def video_tag(*sources) multiple_sources_tag('video', sources) do |options| options[:poster] = path_to_image(options[:poster]) if options[:poster] diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 5dc5bb8a98..b79577bcd3 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -720,15 +720,15 @@ module ActionView # label(:post, :title) # # => <label for="post_title">Title</label> # - # You can localize your labels based on model and attribute names. - # For example you can define the following in your locale (e.g. en.yml) + # You can localize your labels based on model and attribute names. + # For example you can define the following in your locale (e.g. en.yml) # # helpers: # label: # post: # body: "Write your entire text here" # - # Which then will result in + # Which then will result in # # label(:post, :body) # # => <label for="post_body">Write your entire text here</label> diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index a9b91d1db3..ace457df2e 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -118,6 +118,7 @@ module ActionView # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option> # # <option>Paris</option><option>Rome</option></select> def select_tag(name, option_tags = nil, options = {}) + option_tags ||= "" html_name = (options[:multiple] == true && !name.to_s.ends_with?("[]")) ? "#{name}[]" : name if options.delete(:include_blank) diff --git a/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb index 9f5e3be454..2d36deaa78 100644 --- a/actionpack/lib/action_view/railtie.rb +++ b/actionpack/lib/action_view/railtie.rb @@ -9,6 +9,8 @@ module ActionView config.action_view.javascript_expansions = { :defaults => %w(jquery jquery_ujs) } config.action_view.embed_authenticity_token_in_remote_forms = false + config.eager_load_namespaces << ActionView + initializer "action_view.embed_authenticity_token_in_remote_forms" do |app| ActiveSupport.on_load(:action_view) do ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 6cc1370105..57ab325683 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -197,7 +197,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end def test_regexp_precidence - @rs.draw do + rs.draw do get '/whois/:domain', :constraints => { :domain => /\w+\.[\w\.]+/ }, :to => lambda { |env| [200, {}, %w{regexp}] } @@ -216,7 +216,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end } - @rs.draw do + rs.draw do get '/', :constraints => subdomain.new, :to => lambda { |env| [200, {}, %w{default}] } get '/', :constraints => { :subdomain => 'clients' }, @@ -228,7 +228,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end def test_lambda_constraints - @rs.draw do + rs.draw do get '/', :constraints => lambda { |req| req.subdomain.present? and req.subdomain != "clients" }, :to => lambda { |env| [200, {}, %w{default}] } @@ -266,22 +266,22 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_draw_with_block_arity_one_raises assert_raise(RuntimeError) do - @rs.draw { |map| map.match '/:controller(/:action(/:id))' } + rs.draw { |map| map.match '/:controller(/:action(/:id))' } end end def test_specific_controller_action_failure - @rs.draw do + rs.draw do mount lambda {} => "/foo" end assert_raises(ActionController::RoutingError) do - url_for(@rs, :controller => "omg", :action => "lol") + url_for(rs, :controller => "omg", :action => "lol") end end def test_default_setup - @rs.draw { get '/:controller(/:action(/:id))' } + rs.draw { get '/:controller(/:action(/:id))' } assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/content")) assert_equal({:controller => "content", :action => 'list'}, rs.recognize_path("/content/list")) assert_equal({:controller => "content", :action => 'show', :id => '10'}, rs.recognize_path("/content/show/10")) @@ -298,8 +298,8 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end def test_ignores_leading_slash - @rs.clear! - @rs.draw { get '/:controller(/:action(/:id))'} + rs.clear! + rs.draw { get '/:controller(/:action(/:id))'} test_default_setup end @@ -470,7 +470,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end def test_changing_controller - @rs.draw { get ':controller/:action/:id' } + rs.draw { get ':controller/:action/:id' } assert_equal '/admin/stuff/show/10', url_for(rs, {:controller => 'stuff', :action => 'show', :id => 10}, @@ -583,7 +583,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end def test_action_expiry - @rs.draw { get ':controller(/:action(/:id))' } + rs.draw { get ':controller(/:action(/:id))' } assert_equal '/content', url_for(rs, { :controller => 'content' }, { :controller => 'content', :action => 'show' }) end diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index 536e35ab2e..3b008fdff0 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -22,6 +22,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest mount SprocketsApp => "/shorthand" mount FakeEngine, :at => "/fakeengine" + mount FakeEngine, :at => "/getfake", :via => :get scope "/its_a" do mount SprocketsApp, :at => "/sprocket" @@ -52,6 +53,14 @@ class TestRoutingMount < ActionDispatch::IntegrationTest assert_equal "/shorthand -- /omg", response.body end + def test_mounting_works_with_via + get "/getfake" + assert_equal "OK", response.body + + post "/getfake" + assert_response :not_found + end + def test_with_fake_engine_does_not_call_invalid_method get "/fakeengine" assert_equal "OK", response.body diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 71609d7340..4d699bd739 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -177,9 +177,10 @@ class ResponseTest < ActiveSupport::TestCase end end - test "read x_frame_options and x_xss_protection" do + test "read x_frame_options, x_content_type_options and x_xss_protection" do ActionDispatch::Response.default_headers = { 'X-Frame-Options' => 'DENY', + 'X-Content-Type-Options' => 'nosniff', 'X-XSS-Protection' => '1;' } resp = ActionDispatch::Response.new.tap { |response| @@ -188,6 +189,7 @@ class ResponseTest < ActiveSupport::TestCase resp.to_a assert_equal('DENY', resp.headers['X-Frame-Options']) + assert_equal('nosniff', resp.headers['X-Content-Type-Options']) assert_equal('1;', resp.headers['X-XSS-Protection']) end diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 81ba92f2e6..3c66a29754 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -225,6 +225,18 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_select_tag_with_nil_option_tags_and_include_blank + actual = select_tag "places", nil, :include_blank => true + expected = %(<select id="places" name="places"><option value=""></option></select>) + assert_dom_equal expected, actual + end + + def test_select_tag_with_nil_option_tags_and_prompt + actual = select_tag "places", nil, :prompt => "string" + expected = %(<select id="places" name="places"><option value="">string</option></select>) + assert_dom_equal expected, actual + end + def test_text_area_tag_size_string actual = text_area_tag "body", "hello world", "size" => "20x40" expected = %(<textarea cols="20" id="body" name="body" rows="40">\nhello world</textarea>) diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 12e0956d3c..a8f470397b 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,6 +1,15 @@ ## Rails 4.0.0 (unreleased) ## -* Changed `ActiveModel::Serializers::Xml::Serializer#add_associations` to by default propagate `:skip_types, :dasherize, :camelize` keys to included associations. It can be overriden on each association by explicitly specifying the option on one or more associations *Anthony Alberto* +* `AM::Validation#validates` ability to pass custom exception to `:strict` option. + + *Bogdan Gusiev* + +* Changed `ActiveModel::Serializers::Xml::Serializer#add_associations` to by default + propagate `:skip_types, :dasherize, :camelize` keys to included associations. + It can be overriden on each association by explicitly specifying the option on one + or more associations + + *Anthony Alberto* * Changed `AM::Serializers::JSON.include_root_in_json' default value to false. Now, AM Serializers and AR objects have the same default behaviour. Fixes #6578. diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index ec2d734647..d1cc19ec6b 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -34,7 +34,6 @@ module ActiveModel autoload :Conversion autoload :Dirty autoload :EachValidator, 'active_model/validator' - autoload :Errors autoload :Lint autoload :MassAssignmentSecurity autoload :Model @@ -49,11 +48,22 @@ module ActiveModel autoload :Validations autoload :Validator + eager_autoload do + autoload :Errors + end + module Serializers extend ActiveSupport::Autoload - autoload :JSON - autoload :Xml + eager_autoload do + autoload :JSON + autoload :Xml + end + end + + def eager_load! + super + ActiveModel::Serializer.eager_load! end end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 1026b0f4d3..b3b9ba8e56 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -283,15 +283,19 @@ module ActiveModel # # If the <tt>:strict</tt> option is set to true will raise # ActiveModel::StrictValidationFailed instead of adding the error. + # <tt>:strict</tt> option can also be set to any other exception. # # person.errors.add(:name, nil, strict: true) # # => ActiveModel::StrictValidationFailed: name is invalid + # person.errors.add(:name, nil, strict: NameIsInvalid) + # # => NameIsInvalid: name is invalid # # person.errors.messages # => {} def add(attribute, message = nil, options = {}) message = normalize_message(attribute, message, options) - if options[:strict] - raise ActiveModel::StrictValidationFailed, full_message(attribute, message) + if exception = options[:strict] + exception = ActiveModel::StrictValidationFailed if exception == true + raise exception, full_message(attribute, message) end self[attribute] << message diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb index 63ffe5db63..f239758b35 100644 --- a/activemodel/lib/active_model/railtie.rb +++ b/activemodel/lib/active_model/railtie.rb @@ -1,2 +1,8 @@ require "active_model" -require "rails"
\ No newline at end of file +require "rails" + +module ActiveModel + class Railtie < Rails::Railtie + config.eager_load_namespaces << ActiveModel + end +end
\ No newline at end of file diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 5892ad29d1..eb6e604851 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -84,12 +84,15 @@ module ActiveModel # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a +true+ or # +false+ value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # * <tt>:strict</tt> - if the <tt>:strict</tt> option is set to true + # will raise ActiveModel::StrictValidationFailed instead of adding the error. + # <tt>:strict</tt> option can also be set to any other exception. # # Example: # # validates :password, presence: true, confirmation: true, if: :password_required? + # validates :token, uniqueness: true, strict: TokenGenerationException + # # # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+ # and +:strict+ can be given to one specific validator, as a hash: diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 8ea9745fbf..a9d32808da 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -11,6 +11,8 @@ require 'active_support/xml_mini' class ValidationsTest < ActiveModel::TestCase + class CustomStrictValidationException < StandardError; end + def setup Topic._validators.clear end @@ -323,6 +325,13 @@ class ValidationsTest < ActiveModel::TestCase end end + def test_strict_validation_custom_exception + Topic.validates_presence_of :title, :strict => CustomStrictValidationException + assert_raises CustomStrictValidationException do + Topic.new.valid? + end + end + def test_validates_with_bang Topic.validates! :title, :presence => true assert_raises ActiveModel::StrictValidationFailed do diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index faf1bbf232..c5ef39b9d2 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,30 @@ ## Rails 4.0.0 (unreleased) ## +* Fix `reset_counters` when there are multiple `belongs_to` association with the + same foreign key and one of them have a counter cache. + Fixes #5200. + + *Dave Desrochers* + +* `serialized_attributes` and `_attr_readonly` become class method only. Instance reader methods are deprecated. + + *kennyj* + +* Round usec when comparing timestamp attributes in the dirty tracking. + Fixes #6975. + + *kennyj* + +* Use inversed parent for first and last child of has_many association. + + *Ravil Bayramgalin* + +* Fix Column.microseconds and Column.fast_string_to_date to avoid converting + timestamp seconds to a float, since it occasionally results in inaccuracies + with microsecond-precision times. Fixes #7352. + + *Ari Pollak* + * Raise `ArgumentError` if list of attributes to change is empty in `update_all`. *Roman Shatsov* @@ -294,7 +319,7 @@ `where(...).first_or_create!` The implementation of the deprecated dynamic finders has been moved - to the `active_record_deprecated_finders` gem. See below for details. + to the `activerecord-deprecated_finders` gem. See below for details. *Jon Leighton* @@ -325,7 +350,7 @@ * `:extend` becomes `:extending` The code to implement the deprecated features has been moved out to - the `active_record_deprecated_finders` gem. This gem is a dependency + the `activerecord-deprecated_finders` gem. This gem is a dependency of Active Record in Rails 4.0. It will no longer be a dependency from Rails 4.1, but if your app relies on the deprecated features then you can add it to your own Gemfile. It will be maintained by diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index dca7f13fd2..53791d96ef 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency('activemodel', version) s.add_dependency('arel', '~> 3.0.2') - s.add_dependency('active_record_deprecated_finders', '0.0.1') + s.add_dependency('activerecord-deprecated_finders', '0.0.1') end diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index 31f3e02bb8..cd9825b50c 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -1,7 +1,9 @@ -TIMES = (ENV['N'] || 10000).to_i - require File.expand_path('../../../load_paths', __FILE__) require "active_record" +require 'benchmark/ips' + +TIME = (ENV['BENCHMARK_TIME'] || 20).to_i +RECORDS = (ENV['BENCHMARK_RECORDS'] || TIME*1000).to_i conn = { :adapter => 'sqlite3', :database => ':memory:' } @@ -72,8 +74,8 @@ end notes = ActiveRecord::Faker::LOREM.join ' ' today = Date.today -puts 'Inserting 10,000 users and exhibits...' -10_000.times do +puts "Inserting #{RECORDS} users and exhibits..." +RECORDS.times do user = User.create( :created_at => today, :name => ActiveRecord::Faker.name, @@ -88,9 +90,7 @@ puts 'Inserting 10,000 users and exhibits...' ) end -require 'benchmark' - -Benchmark.bm(46) do |x| +Benchmark.ips(TIME) do |x| ar_obj = Exhibit.find(1) attrs = { :name => 'sam' } attrs_first = { :name => 'sam' } @@ -101,77 +101,72 @@ Benchmark.bm(46) do |x| :created_at => Date.today } - x.report("Model#id (x#{(TIMES * 100).ceil})") do - (TIMES * 100).ceil.times { ar_obj.id } + x.report("Model#id") do + ar_obj.id end x.report 'Model.new (instantiation)' do - TIMES.times { Exhibit.new } + Exhibit.new end x.report 'Model.new (setting attributes)' do - TIMES.times { Exhibit.new(attrs) } + Exhibit.new(attrs) end x.report 'Model.first' do - TIMES.times { Exhibit.first.look } + Exhibit.first.look end - x.report 'Model.named_scope' do - TIMES.times { Exhibit.limit(10).with_name.with_notes } + x.report("Model.all limit(100)") do + Exhibit.look Exhibit.limit(100) end - x.report("Model.all limit(100) (x#{(TIMES / 10).ceil})") do - (TIMES / 10).ceil.times { Exhibit.look Exhibit.limit(100) } + x.report "Model.all limit(100) with relationship" do + Exhibit.feel Exhibit.limit(100).includes(:user) end - x.report "Model.all limit(100) with relationship (x#{(TIMES / 10).ceil})" do - (TIMES / 10).ceil.times { Exhibit.feel Exhibit.limit(100).includes(:user) } + x.report "Model.all limit(10,000)" do + Exhibit.look Exhibit.limit(10000) end - x.report "Model.all limit(10,000) x(#{(TIMES / 1000).ceil})" do - (TIMES / 1000).ceil.times { Exhibit.look Exhibit.limit(10000) } + x.report 'Model.named_scope' do + Exhibit.limit(10).with_name.with_notes end x.report 'Model.create' do - TIMES.times { Exhibit.create(exhibit) } + Exhibit.create(exhibit) end x.report 'Resource#attributes=' do - TIMES.times { - exhibit = Exhibit.new(attrs_first) - exhibit.attributes = attrs_second - } + e = Exhibit.new(attrs_first) + e.attributes = attrs_second end x.report 'Resource#update' do - TIMES.times { Exhibit.first.update_attributes(:name => 'bob') } + Exhibit.first.update_attributes(:name => 'bob') end x.report 'Resource#destroy' do - TIMES.times { Exhibit.first.destroy } + Exhibit.first.destroy end x.report 'Model.transaction' do - TIMES.times { Exhibit.transaction { Exhibit.new } } + Exhibit.transaction { Exhibit.new } end x.report 'Model.find(id)' do - id = Exhibit.first.id - TIMES.times { Exhibit.find(id) } + User.find(1) end x.report 'Model.find_by_sql' do - TIMES.times { - Exhibit.find_by_sql("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}").first - } + Exhibit.find_by_sql("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}").first end - x.report "Model.log x(#{TIMES * 10})" do - (TIMES * 10).times { Exhibit.connection.send(:log, "hello", "world") {} } + x.report "Model.log" do + Exhibit.connection.send(:log, "hello", "world") {} end - x.report "AR.execute(query) (#{TIMES / 2})" do - (TIMES / 2).times { ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}") } + x.report "AR.execute(query)" do + ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}") end end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 5a51aaaced..fa94f6a941 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -25,7 +25,7 @@ require 'active_support' require 'active_support/rails' require 'active_model' require 'arel' -require 'active_record_deprecated_finders' +require 'active_record/deprecated_finders' require 'active_record/version' @@ -160,6 +160,15 @@ module ActiveRecord autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' + + def self.eager_load! + super + ActiveRecord::Locking.eager_load! + ActiveRecord::Scoping.eager_load! + ActiveRecord::Associations.eager_load! + ActiveRecord::AttributeMethods.eager_load! + ActiveRecord::ConnectionAdapters.eager_load! + end end ActiveSupport.on_load(:active_record) do diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index a84eda1d3b..b15df4f308 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -574,7 +574,7 @@ module ActiveRecord args.shift if args.first.is_a?(Hash) && args.first.empty? collection = fetch_first_or_last_using_find?(args) ? scope : load_target - collection.send(type, *args) + collection.send(type, *args).tap {|it| set_inverse_instance it } end end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 6992840040..d9989274c8 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -84,11 +84,11 @@ module ActiveRecord def assign_attributes(new_attributes, options = {}) return if new_attributes.blank? - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] + attributes = new_attributes.stringify_keys + multi_parameter_attributes = [] nested_parameter_attributes = [] - previous_options = @mass_assignment_options - @mass_assignment_options = options + previous_options = @mass_assignment_options + @mass_assignment_options = options unless options[:without_protection] attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) @@ -97,23 +97,15 @@ module ActiveRecord attributes.each do |k, v| if k.include?("(") multi_parameter_attributes << [ k, v ] - elsif respond_to?("#{k}=") - if v.is_a?(Hash) - nested_parameter_attributes << [ k, v ] - else - send("#{k}=", v) - end + elsif v.is_a?(Hash) + nested_parameter_attributes << [ k, v ] else - raise(UnknownAttributeError, "unknown attribute: #{k}") + _assign_attribute(k, v) end end - # assign any deferred nested attributes after the base attributes have been set - nested_parameter_attributes.each do |k,v| - send("#{k}=", v) - end - - assign_multiparameter_attributes(multi_parameter_attributes) + assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? + assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? ensure @mass_assignment_options = previous_options end @@ -130,6 +122,21 @@ module ActiveRecord private + def _assign_attribute(k, v) + public_send("#{k}=", v) + rescue NoMethodError + if respond_to?("#{k}=") + raise + else + raise UnknownAttributeError, "unknown attribute: #{k}" + end + end + + # Assign any deferred nested attributes after the base attributes have been set. + def assign_nested_parameter_attributes(pairs) + pairs.each { |k, v| _assign_attribute(k, v) } + end + # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate @@ -143,19 +150,11 @@ module ActiveRecord ) end - def instantiate_time_object(name, values) - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) - Time.zone.local(*values) - else - Time.time_with_datetime_fallback(self.class.default_timezone, *values) - end - end - def execute_callstack_for_multiparameter_attributes(callstack) errors = [] callstack.each do |name, values_with_empty_parameters| begin - send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) + send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value) rescue => ex errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end @@ -166,74 +165,12 @@ module ActiveRecord end end - def read_value_from_parameter(name, values_hash_from_param) - klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass - if values_hash_from_param.values.all?{|v|v.nil?} - nil - elsif klass == Time - read_time_parameter_value(name, values_hash_from_param) - elsif klass == Date - read_date_parameter_value(name, values_hash_from_param) - else - read_other_parameter_value(klass, name, values_hash_from_param) - end - end - - def read_time_parameter_value(name, values_hash_from_param) - # If column is a :time (and not :date or :timestamp) there is no need to validate if - # there are year/month/day fields - if column_for_attribute(name).type == :time - # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil - {1 => 1970, 2 => 1, 3 => 1}.each do |key,value| - values_hash_from_param[key] ||= value - end - else - # else column is a timestamp, so if Date bits were not provided, error - if missing_parameter = [1,2,3].detect{ |position| !values_hash_from_param.has_key?(position) } - raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter}i)") - end - - # If Date bits were provided but blank, then return nil - return nil if (1..3).any? { |position| values_hash_from_param[position].blank? } - end - - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) - set_values = (1..max_position).collect{ |position| values_hash_from_param[position] } - # If Time bits are not there, then default to 0 - (3..5).each { |i| set_values[i] = set_values[i].blank? ? 0 : set_values[i] } - instantiate_time_object(name, set_values) - end - - def read_date_parameter_value(name, values_hash_from_param) - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - end - - def read_other_parameter_value(klass, name, values_hash_from_param) - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) - values = (1..max_position).collect do |position| - raise "Missing Parameter" if !values_hash_from_param.has_key?(position) - values_hash_from_param[position] - end - klass.new(*values) - end - - def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) - [values_hash_from_param.keys.max,upper_cap].min - end - def extract_callstack_for_multiparameter_attributes(pairs) attributes = { } - pairs.each do |pair| - multiparameter_name, value = pair + pairs.each do |(multiparameter_name, value)| attribute_name = multiparameter_name.split("(").first - attributes[attribute_name] = {} unless attributes.include?(attribute_name) + attributes[attribute_name] ||= {} parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value @@ -250,5 +187,100 @@ module ActiveRecord multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end + class MultiparameterAttribute #:nodoc: + attr_reader :object, :name, :values, :column + + def initialize(object, name, values) + @object = object + @name = name + @values = values + end + + def read_value + return if values.values.compact.empty? + + @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name) + klass = column.klass + + if klass == Time + read_time + elsif klass == Date + read_date + else + read_other(klass) + end + end + + private + + def instantiate_time_object(set_values) + if object.class.send(:create_time_zone_conversion_attribute?, name, column) + Time.zone.local(*set_values) + else + Time.time_with_datetime_fallback(object.class.default_timezone, *set_values) + end + end + + def read_time + # If column is a :time (and not :date or :timestamp) there is no need to validate if + # there are year/month/day fields + if column.type == :time + # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil + { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| + values[key] ||= value + end + else + # else column is a timestamp, so if Date bits were not provided, error + validate_missing_parameters!([1,2,3]) + + # If Date bits were provided but blank, then return nil + return if blank_date_parameter? + end + + max_position = extract_max_param(6) + set_values = values.values_at(*(1..max_position)) + # If Time bits are not there, then default to 0 + (3..5).each { |i| set_values[i] = set_values[i].presence || 0 } + instantiate_time_object(set_values) + end + + def read_date + return if blank_date_parameter? + set_values = values.values_at(1,2,3) + begin + Date.new(*set_values) + rescue ArgumentError # if Date.new raises an exception on an invalid date + instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates + end + end + + def read_other(klass) + max_position = extract_max_param + positions = (1..max_position) + validate_missing_parameters!(positions) + + set_values = values.values_at(*positions) + klass.new(*set_values) + end + + # Checks whether some blank date parameter exists. Note that this is different + # than the validate_missing_parameters! method, since it just checks for blank + # positions instead of missing ones, and does not raise in case one blank position + # exists. The caller is responsible to handle the case of this returning true. + def blank_date_parameter? + (1..3).any? { |position| values[position].blank? } + end + + # If some position is not provided, it errors out a missing parameter exception. + def validate_missing_parameters!(positions) + if missing_parameter = positions.detect { |position| !values.key?(position) } + raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})") + end + end + + def extract_max_param(upper_cap = 100) + [values.keys.max, upper_cap].min + end + end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 49ab3ab808..bdda5bc009 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -6,10 +6,46 @@ module ActiveRecord included do # Returns a hash of all the attributes that have been specified for serialization as # keys and their class restriction as values. - class_attribute :serialized_attributes, instance_writer: false + class_attribute :serialized_attributes, instance_accessor: false self.serialized_attributes = {} end + module ClassMethods + # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, + # then specify the name of that attribute using this method and it will be handled automatically. + # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that + # class on retrieval or SerializationTypeMismatch will be raised. + # + # ==== Parameters + # + # * +attr_name+ - The field name that should be serialized. + # * +class_name+ - Optional, class name that the object type should be equal to. + # + # ==== Example + # # Serialize a preferences attribute + # class User < ActiveRecord::Base + # serialize :preferences + # end + def serialize(attr_name, class_name = Object) + include Behavior + + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end + + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) + end + end + + def serialized_attributes + ActiveSupport::Deprecation.warn("Instance level serialized_attributes method is deprecated, please use class level method.") + defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes + end + class Type # :nodoc: def initialize(column) @column = column @@ -44,71 +80,50 @@ module ActiveRecord end end - module ClassMethods - # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, - # then specify the name of that attribute using this method and it will be handled automatically. - # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that - # class on retrieval or SerializationTypeMismatch will be raised. - # - # ==== Parameters - # - # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. - # - # ==== Example - # # Serialize a preferences attribute - # class User < ActiveRecord::Base - # serialize :preferences - # end - def serialize(attr_name, class_name = Object) - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name - else - Coders::YAMLColumn.new(class_name) - end + # This is only added to the model when serialize is called, which + # ensures we do not make things slower when serialization is not used. + module Behavior #:nodoc: + extend ActiveSupport::Concern - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) - end - - def initialize_attributes(attributes, options = {}) #:nodoc: - serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized - super(attributes, options) + module ClassMethods + def initialize_attributes(attributes, options = {}) + serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized + super(attributes, options) - serialized_attributes.each do |key, coder| - if attributes.key?(key) - attributes[key] = Attribute.new(coder, attributes[key], serialized) + serialized_attributes.each do |key, coder| + if attributes.key?(key) + attributes[key] = Attribute.new(coder, attributes[key], serialized) + end end + + attributes end - attributes - end + private - private + def attribute_cast_code(attr_name) + if serialized_attributes.include?(attr_name) + "v.unserialized_value" + else + super + end + end + end - def attribute_cast_code(attr_name) - if serialized_attributes.include?(attr_name) - "v.unserialized_value" + def type_cast_attribute_for_write(column, value) + if column && coder = self.class.serialized_attributes[column.name] + Attribute.new(coder, value, :unserialized) else super end end - end - - def type_cast_attribute_for_write(column, value) - if column && coder = self.class.serialized_attributes[column.name] - Attribute.new(coder, value, :unserialized) - else - super - end - end - def read_attribute_before_type_cast(attr_name) - if serialized_attributes.include?(attr_name) - super.unserialized_value - else - super + def read_attribute_before_type_cast(attr_name) + if self.class.serialized_attributes.include?(attr_name) + super.unserialized_value + else + super + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index fa5b2ef336..d1e9d2de0e 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -59,11 +59,14 @@ module ActiveRecord unless time.acts_like?(:time) time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end - time = time.in_time_zone rescue nil if time - changed = read_attribute(:#{attr_name}) != time - write_attribute(:#{attr_name}, original_time) - #{attr_name}_will_change! if changed - @attributes_cache["#{attr_name}"] = time + zoned_time = time && time.in_time_zone rescue nil + rounded_time = round_usec(zoned_time) + rounded_value = round_usec(read_attribute("#{attr_name}")) + if (rounded_value != rounded_time) || (!rounded_value && original_time) + write_attribute("#{attr_name}", original_time) + #{attr_name}_will_change! + @attributes_cache["#{attr_name}"] = zoned_time + end end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) @@ -79,6 +82,12 @@ module ActiveRecord [:datetime, :timestamp].include?(column.type) end end + + private + def round_usec(value) + return unless value + value.change(:usec => 0) + end end end end 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 b0b51f540c..02459763f7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,6 +1,12 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements + def initialize + super + @_current_transaction_records = [] + @transaction_joinable = nil + end + # Converts an arel AST to SQL def to_sql(arel, binds = []) if arel.respond_to?(:ast) @@ -167,7 +173,7 @@ module ActiveRecord def transaction(options = {}) options.assert_valid_keys :requires_new, :joinable - last_transaction_joinable = defined?(@transaction_joinable) ? @transaction_joinable : nil + last_transaction_joinable = @transaction_joinable if options.has_key?(:joinable) @transaction_joinable = options[:joinable] else @@ -176,22 +182,19 @@ 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 - if open_transactions == 0 - begin_db_transaction - elsif requires_new - create_savepoint - end - increment_open_transactions - transaction_open = true - @_current_transaction_records.push([]) + if requires_new || open_transactions == 0 + if open_transactions == 0 + begin_db_transaction + elsif requires_new + create_savepoint end - yield + increment_open_transactions + transaction_open = true + @_current_transaction_records.push([]) end + yield rescue Exception => database_transaction_rollback if transaction_open && !outside_transaction? transaction_open = false @@ -225,7 +228,7 @@ module ActiveRecord @_current_transaction_records.last.concat(save_point_records) end end - rescue Exception => database_transaction_rollback + rescue Exception if open_transactions == 0 rollback_db_transaction rollback_transaction_records(true) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index b9045cf1e7..1445bb3b2f 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -208,7 +208,7 @@ module ActiveRecord # '0.123456' -> 123456 # '1.123456' -> 123456 def microseconds(time) - ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 end def new_date(year, mon, mday) @@ -233,7 +233,7 @@ module ActiveRecord # Doesn't handle time zones. def fast_string_to_time(string) if string =~ Format::ISO_DATETIME - microsec = ($7.to_f * 1_000_000).to_i + microsec = ($7.to_r * 1_000_000).to_i new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 3b0353358a..6bf7af081f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -298,13 +298,6 @@ module ActiveRecord @connection.insert_id end - class Result < ActiveRecord::Result - def initialize(columns, rows, column_types) - super(columns, rows) - @column_types = column_types - end - end - module Fields class Type def type; end @@ -437,7 +430,7 @@ module ActiveRecord } end } - result_set = Result.new(types.keys, result.to_a, types) + result_set = ActiveRecord::Result.new(types.keys, result.to_a, types) result.free else result_set = ActiveRecord::Result.new([], []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 8e9ce80697..40cd65cce9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -803,13 +803,6 @@ module ActiveRecord Arel::Nodes::BindParam.new "$#{index + 1}" end - class Result < ActiveRecord::Result - def initialize(columns, rows, column_types) - super(columns, rows) - @column_types = column_types - end - end - def exec_query(sql, name = 'SQL', binds = []) log(sql, name, binds) do result = binds.empty? ? exec_no_cache(sql, binds) : @@ -825,7 +818,7 @@ module ActiveRecord } end - ret = Result.new(result.fields, result.values, types) + ret = ActiveRecord::Result.new(result.fields, result.values, types) result.clear return ret end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 57aa47ab61..4fe0013f0f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -380,9 +380,9 @@ module ActiveRecord case field["dflt_value"] when /^null$/i field["dflt_value"] = nil - when /^'(.*)'$/ + when /^'(.*)'$/m field["dflt_value"] = $1.gsub("''", "'") - when /^"(.*)"$/ + when /^"(.*)"$/m field["dflt_value"] = $1.gsub('""', '"') end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 0fddfdf0cb..aad21b8e37 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/object/duplicable' require 'thread' module ActiveRecord @@ -173,7 +173,10 @@ module ActiveRecord # # Instantiates a single new object bypassing mass-assignment security # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) def initialize(attributes = nil, options = {}) - @attributes = self.class.initialize_attributes(self.class.column_defaults.deep_dup) + defaults = self.class.column_defaults.dup + defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } + + @attributes = self.class.initialize_attributes(defaults) @columns_hash = self.class.column_types.dup init_internals @@ -185,7 +188,7 @@ module ActiveRecord assign_attributes(attributes, options) if attributes yield self if block_given? - run_callbacks :initialize if _initialize_callbacks.any? + run_callbacks :initialize unless _initialize_callbacks.empty? end # Initialize an empty model object from +coder+. +coder+ must contain @@ -390,6 +393,7 @@ module ActiveRecord @marked_for_destruction = false @new_record = true @mass_assignment_options = nil + @_start_transaction_state = {} end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index b27a19f89a..c877079b25 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -25,7 +25,7 @@ module ActiveRecord foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass belongs_to = child_class.reflect_on_all_associations(:belongs_to) - reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key } + reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 843587c32e..3bac31c6aa 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,8 +1,8 @@ module ActiveRecord module DynamicMatchers #:nodoc: # This code in this file seems to have a lot of indirection, but the indirection - # is there to provide extension points for the active_record_deprecated_finders - # gem. When we stop supporting active_record_deprecated_finders (from Rails 5), + # is there to provide extension points for the activerecord-deprecated_finders + # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5), # then we can remove the indirection. def respond_to?(name, include_private = false) @@ -74,17 +74,17 @@ module ActiveRecord end module Finder - # Extended in active_record_deprecated_finders + # Extended in activerecord-deprecated_finders def body result end - # Extended in active_record_deprecated_finders + # Extended in activerecord-deprecated_finders def result "#{finder}(#{attributes_hash})" end - # Extended in active_record_deprecated_finders + # Extended in activerecord-deprecated_finders def signature attribute_names.join(', ') end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index e0344f3c56..e96ed00f9c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -168,16 +168,16 @@ module ActiveRecord super end - # If the locking column has no default value set, - # start the lock version at zero. Note we can't use - # <tt>locking_enabled?</tt> at this point as - # <tt>@attributes</tt> may not have been initialized yet. - def initialize_attributes(attributes, options = {}) #:nodoc: - if attributes.key?(locking_column) && lock_optimistically - attributes[locking_column] ||= 0 - end + def column_defaults + @column_defaults ||= begin + defaults = super + + if defaults.key?(locking_column) && lock_optimistically + defaults[locking_column] ||= 0 + end - attributes + defaults + end end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index def48c03bf..99de16cd33 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -225,7 +225,7 @@ module ActiveRecord def decorate_columns(columns_hash) # :nodoc: return if columns_hash.empty? - serialized_attributes.keys.each do |key| + serialized_attributes.each_key do |key| columns_hash[key] = AttributeMethods::Serialization::Type.new(columns_hash[key]) end @@ -259,13 +259,12 @@ module ActiveRecord # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute # is available. def column_methods_hash #:nodoc: - @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| + @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods| attr_name = attr.to_s methods[attr.to_sym] = attr_name methods["#{attr}=".to_sym] = attr_name methods["#{attr}?".to_sym] = attr_name methods["#{attr}_before_type_cast".to_sym] = attr_name - methods end end @@ -312,13 +311,19 @@ module ActiveRecord @relation = nil end + # This is a hook for use by modules that need to do extra stuff to + # attributes when they are initialized. (e.g. attribute + # serialization) + def initialize_attributes(attributes, options = {}) #:nodoc: + attributes + end + private # Guesses the table name, but does not decorate it with prefix and suffix information. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore - table_name = table_name.pluralize if pluralize_table_names - table_name + pluralize_table_names ? table_name.pluralize : table_name end # Computes and returns a table name according to default conventions. diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 593fed5a85..6b4b9bd103 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -218,7 +218,7 @@ module ActiveRecord raise ActiveRecordError, "can not update on a new record object" unless persisted? attributes.each_key do |key| - raise ActiveRecordError, "#{key.to_s} is marked as readonly" if self.class.readonly_attributes.include?(key.to_s) + raise ActiveRecordError, "#{key} is marked as readonly" if self.class.readonly_attributes.include?(key.to_s) end attributes.each do |k,v| @@ -391,9 +391,5 @@ module ActiveRecord @new_record = false id end - - def verify_readonly_attribute(name) - raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) - end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 672d9a4246..ecf8547e67 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -29,8 +29,11 @@ module ActiveRecord 'ActiveRecord::RecordNotSaved' => :unprocessable_entity ) + config.active_record.use_schema_cache_dump = true + config.eager_load_namespaces << ActiveRecord + rake_tasks do require "active_record/base" load "active_record/railties/databases.rake" diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 1d8c566e40..b3c20c4aff 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -4,7 +4,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_attr_readonly, instance_writer: false + class_attribute :_attr_readonly, instance_accessor: false self._attr_readonly = [] end @@ -20,5 +20,10 @@ module ActiveRecord self._attr_readonly end end + + def _attr_readonly + ActiveSupport::Deprecation.warn("Instance level _attr_readonly method is deprecated, please use class level method.") + defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly + end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 1abbc58314..2d0457636e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -18,6 +18,7 @@ module ActiveRecord attr_reader :table, :klass, :loaded attr_accessor :default_scoped + alias :model :klass alias :loaded? :loaded alias :default_scoped? :default_scoped diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 71aaedee1e..e5b50673da 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -97,15 +97,13 @@ module ActiveRecord merged_wheres = relation.where_values + values[:where] unless relation.where_values.empty? - # Remove duplicates, last one wins. - seen = Hash.new { |h,table| h[table] = {} } + # Remove equalities with duplicated left-hand. Last one wins. + seen = {} merged_wheres = merged_wheres.reverse.reject { |w| nuke = false if w.respond_to?(:operator) && w.operator == :== - name = w.left.name - table = w.left.relation.name - nuke = seen[table][name] - seen[table][name] = true + nuke = seen[w.left] + seen[w.left] = true end nuke }.reverse diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index fd276ccf5d..2414a4bbd7 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -10,11 +10,11 @@ module ActiveRecord attr_reader :columns, :rows, :column_types - def initialize(columns, rows) + def initialize(columns, rows, column_types = {}) @columns = columns @rows = rows @hash_rows = nil - @column_types = {} + @column_types = column_types end def each diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 5151f349b7..b4013ecc1e 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -41,7 +41,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :stored_attributes, instance_writer: false + class_attribute :stored_attributes, instance_accessor: false self.stored_attributes = {} end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 5c3399e2aa..9cec791faf 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -293,12 +293,12 @@ module ActiveRecord begin status = yield rescue ActiveRecord::Rollback - if defined?(@_start_transaction_state) + if defined?(@_start_transaction_state) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 end status = nil end - + raise ActiveRecord::Rollback unless status end status @@ -308,7 +308,6 @@ module ActiveRecord # 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 ||= {} @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) @_start_transaction_state[:new_record] = @new_record @_start_transaction_state[:destroyed] = @destroyed @@ -317,18 +316,16 @@ module ActiveRecord # 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 + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + @_start_transaction_state.clear if @_start_transaction_state[:level] < 1 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) + unless @_start_transaction_state.empty? @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 || force - restore_state = remove_instance_variable(:@_start_transaction_state) + restore_state = @_start_transaction_state was_frozen = @attributes.frozen? @attributes = @attributes.dup if was_frozen @new_record = restore_state[:new_record] @@ -340,13 +337,14 @@ module ActiveRecord @attributes_cache.delete(self.class.primary_key) end @attributes.freeze if was_frozen + @_start_transaction_state.clear 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) + @_start_transaction_state[state] end # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index c3f82bc63d..4bccd2cc59 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -128,11 +128,12 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled + def test_mysql_strict_mode_disabled_dont_override_global_sql_mode run_without_connection do |orig_connection| ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false})) - result = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [['']], result.rows + global_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@GLOBAL.sql_mode" + session_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal global_sql_mode.rows, session_sql_mode.rows end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 276c499276..c63e4fe5b6 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -44,11 +44,12 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled + def test_mysql_strict_mode_disabled_dont_override_global_sql_mode run_without_connection do |orig_connection| ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false})) - result = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [['']], result.rows + global_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@GLOBAL.sql_mode" + session_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal global_sql_mode.rows, session_sql_mode.rows end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 8cb8a5a861..aad48e7ce9 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -259,6 +259,12 @@ class InverseHasManyTests < ActiveRecord::TestCase assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance" end + def test_parent_instance_should_be_shared_with_first_and_last_child + man = Man.first + assert man.interests.first.man.equal? man + assert man.interests.last.man.equal? man + end + def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests } end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 807971d678..4bc68acd13 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -34,7 +34,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert t.attribute_present?("written_on") assert !t.attribute_present?("content") assert !t.attribute_present?("author_name") - end def test_attribute_present_with_booleans diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 062f196a12..63981a68a9 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -231,6 +231,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 11, Topic.find(1).written_on.sec assert_equal 223300, Topic.find(1).written_on.usec assert_equal 9900, Topic.find(2).written_on.usec + assert_equal 129346, Topic.find(3).written_on.usec end end @@ -603,6 +604,12 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "changed", post.body end + def test_attr_readonly_is_class_level_setting + post = ReadonlyTitlePost.new + assert_raise(NoMethodError) { post._attr_readonly = [:title] } + assert_deprecated { post._attr_readonly } + end + def test_non_valid_identifier_column_name weird = Weird.create('a$b' => 'value') weird.reload diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index cd3d19e783..ee443741ca 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -8,9 +8,11 @@ require 'models/category' require 'models/categorization' require 'models/dog' require 'models/dog_lover' +require 'models/person' +require 'models/friendship' class CounterCacheTest < ActiveRecord::TestCase - fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers + fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers, :people, :friendships class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' @@ -109,4 +111,11 @@ class CounterCacheTest < ActiveRecord::TestCase Topic.update_counters([t1.id, t2.id], :replies_count => 2) end end + + test "reset the right counter if two have the same foreign key" do + michael = people(:michael) + assert_nothing_raised(ActiveRecord::StatementInvalid) do + Person.reset_counters(michael.id, :followers) + end + end end diff --git a/activerecord/test/cases/deprecated_dynamic_methods_test.rb b/activerecord/test/cases/deprecated_dynamic_methods_test.rb index fe307bc49b..392f5f4cd5 100644 --- a/activerecord/test/cases/deprecated_dynamic_methods_test.rb +++ b/activerecord/test/cases/deprecated_dynamic_methods_test.rb @@ -1,4 +1,4 @@ -# This file should be deleted when active_record_deprecated_finders is removed as +# This file should be deleted when activerecord-deprecated_finders is removed as # a dependency. # # It is kept for now as there is some fairly nuanced behaviour in the dynamic diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 248f4efe3e..92677b9926 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -525,6 +525,21 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_setting_time_attributes_with_time_zone_field_to_same_time_should_not_be_marked_as_a_change + in_time_zone 'Paris' do + target = Class.new(ActiveRecord::Base) + target.table_name = 'pirates' + + created_on = Time.now + + pirate = target.create(:created_on => created_on) + pirate.reload # Here mysql truncate the usec value to 0 + + pirate.created_on = created_on + assert !pirate.created_on_changed? + end + end + private def with_partial_updates(klass, on = true) old = klass.partial_updates? diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index afb0bd6fd9..2392516395 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -3,6 +3,7 @@ require "cases/helper" require 'models/person' require 'models/job' require 'models/reader' +require 'models/ship' require 'models/legacy_thing' require 'models/reference' require 'models/string_key_object' @@ -18,8 +19,8 @@ class LockWithCustomColumnWithoutDefault < ActiveRecord::Base self.locking_column = :custom_lock_version end -class ReadonlyFirstNamePerson < Person - attr_readonly :first_name +class ReadonlyNameShip < Ship + attr_readonly :name end class OptimisticLockingTest < ActiveRecord::TestCase @@ -200,15 +201,15 @@ class OptimisticLockingTest < ActiveRecord::TestCase end def test_readonly_attributes - assert_equal Set.new([ 'first_name' ]), ReadonlyFirstNamePerson.readonly_attributes + assert_equal Set.new([ 'name' ]), ReadonlyNameShip.readonly_attributes - p = ReadonlyFirstNamePerson.create(:first_name => "unchangeable name") - p.reload - assert_equal "unchangeable name", p.first_name + s = ReadonlyNameShip.create(:name => "unchangeable name") + s.reload + assert_equal "unchangeable name", s.name - p.update_attributes(:first_name => "changed name") - p.reload - assert_equal "unchangeable name", p.first_name + s.update_attributes(:name => "changed name") + s.reload + assert_equal "unchangeable name", s.name end def test_quote_table_name diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb index 73a01906b9..a36b2c2506 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -313,7 +313,7 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase end -# This class should be deleted when we removed active_record_deprecated_finders as a +# This class should be deleted when we remove activerecord-deprecated_finders as a # dependency. class MassAssignmentSecurityDeprecatedFindersTest < ActiveRecord::TestCase include MassAssignmentTestHelpers diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 9584d5dd06..b88db384a0 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -7,6 +7,14 @@ module ActiveRecord self.use_transactional_fixtures = false + def test_add_column_newline_default + string = "foo\nbar" + add_column 'test_models', 'command', :string, :default => string + TestModel.reset_column_information + + assert_equal string, TestModel.new.command + end + def test_add_remove_single_field_using_string_arguments refute TestModel.column_methods_hash.key?(:last_name) diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 5fb54b1ca1..6399111be6 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -19,6 +19,11 @@ module ActiveRecord assert !relation.loaded, 'relation is not loaded' end + def test_responds_to_model_and_returns_klass + relation = Relation.new :a, :b + assert_equal :a, relation.model + end + def test_initialize_single_values relation = Relation.new :a, :b (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 84027ea5ae..684538940a 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -668,6 +668,25 @@ class RelationTest < ActiveRecord::TestCase assert_equal [developers(:poor_jamis)], dev_with_count.to_a end + def test_relation_merging_with_arel_equalities_keeps_last_equality + devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge( + Developer.where(Developer.arel_table[:salary].eq(9000)) + ) + assert_equal [developers(:poor_jamis)], devs.to_a + end + + def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand + salary_attr = Developer.arel_table[:salary] + devs = Developer.where( + Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000) + ).merge( + Developer.where( + Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000) + ) + ) + assert_equal [developers(:poor_jamis)], devs.to_a + end + def test_relation_merging_with_eager_load relations = [] relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all) diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index ce167509c1..10d8ccc711 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -53,9 +53,8 @@ class SerializationTest < ActiveRecord::TestCase end def test_serialized_attributes_are_class_level_settings - assert_raise NoMethodError do - topic = Topic.new - topic.serialized_attributes = [] - end + topic = Topic.new + assert_raise(NoMethodError) { topic.serialized_attributes = [] } + assert_deprecated { topic.serialized_attributes } end end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 3e60b62fd5..fb0d116c08 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -122,9 +122,8 @@ class StoreTest < ActiveRecord::TestCase end test "stores_attributes are class level settings" do - assert_raise NoMethodError do - @john.stored_attributes = {} - end + assert_raise(NoMethodError) { @john.stored_attributes = Hash.new } + assert_raise(NoMethodError) { @john.stored_attributes } end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index d5597a68ad..0d0de455b3 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -91,18 +91,14 @@ class TransactionTest < ActiveRecord::TestCase end def test_raising_exception_in_callback_rollbacks_in_save - add_exception_raising_after_save_callback_to_topic - - begin - @first.approved = true - @first.save - flunk - rescue => e - assert_equal "Make the transaction rollback", e.message - assert !Topic.find(1).approved? - ensure - remove_exception_raising_after_save_callback_to_topic + def @first.after_save_for_transaction + raise 'Make the transaction rollback' end + + @first.approved = true + e = assert_raises(RuntimeError) { @first.save } + assert_equal "Make the transaction rollback", e.message + assert !Topic.find(1).approved? end def test_update_attributes_should_rollback_on_failure @@ -125,100 +121,83 @@ class TransactionTest < ActiveRecord::TestCase end def test_cancellation_from_before_destroy_rollbacks_in_destroy - add_cancelling_before_destroy_with_db_side_effect_to_topic - begin - nbooks_before_destroy = Book.count - status = @first.destroy - assert !status - assert_nothing_raised(ActiveRecord::RecordNotFound) { @first.reload } - assert_equal nbooks_before_destroy, Book.count - ensure - remove_cancelling_before_destroy_with_db_side_effect_to_topic - end + add_cancelling_before_destroy_with_db_side_effect_to_topic @first + nbooks_before_destroy = Book.count + status = @first.destroy + assert !status + @first.reload + assert_equal nbooks_before_destroy, Book.count end - def test_cancellation_from_before_filters_rollbacks_in_save - %w(validation save).each do |filter| - send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") - begin - nbooks_before_save = Book.count - original_author_name = @first.author_name - @first.author_name += '_this_should_not_end_up_in_the_db' - status = @first.save - assert !status - assert_equal original_author_name, @first.reload.author_name - assert_equal nbooks_before_save, Book.count - ensure - send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") - end + %w(validation save).each do |filter| + define_method("test_cancellation_from_before_filters_rollbacks_in_#{filter}") do + send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic", @first) + nbooks_before_save = Book.count + original_author_name = @first.author_name + @first.author_name += '_this_should_not_end_up_in_the_db' + status = @first.save + assert !status + assert_equal original_author_name, @first.reload.author_name + assert_equal nbooks_before_save, Book.count end - end - def test_cancellation_from_before_filters_rollbacks_in_save! - %w(validation save).each do |filter| - send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") + define_method("test_cancellation_from_before_filters_rollbacks_in_#{filter}!") do + send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic", @first) + nbooks_before_save = Book.count + original_author_name = @first.author_name + @first.author_name += '_this_should_not_end_up_in_the_db' + begin - nbooks_before_save = Book.count - original_author_name = @first.author_name - @first.author_name += '_this_should_not_end_up_in_the_db' @first.save! - flunk - rescue - assert_equal original_author_name, @first.reload.author_name - assert_equal nbooks_before_save, Book.count - ensure - send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved end + + assert_equal original_author_name, @first.reload.author_name + assert_equal nbooks_before_save, Book.count end end def test_callback_rollback_in_create - new_topic = Topic.new( - :title => "A new topic", - :author_name => "Ben", - :author_email_address => "ben@example.com", - :written_on => "2003-07-16t15:28:11.2233+01:00", - :last_read => "2004-04-15", - :bonus_time => "2005-01-30t15:28:00.00+01:00", - :content => "Have a nice day", - :approved => false) + topic = Class.new(Topic) { + def after_create_for_transaction + raise 'Make the transaction rollback' + end + } + + new_topic = topic.new(:title => "A new topic", + :author_name => "Ben", + :author_email_address => "ben@example.com", + :written_on => "2003-07-16t15:28:11.2233+01:00", + :last_read => "2004-04-15", + :bonus_time => "2005-01-30t15:28:00.00+01:00", + :content => "Have a nice day", + :approved => false) + new_record_snapshot = !new_topic.persisted? id_present = new_topic.has_attribute?(Topic.primary_key) id_snapshot = new_topic.id # Make sure the second save gets the after_create callback called. 2.times do - begin - add_exception_raising_after_create_callback_to_topic - new_topic.approved = true - new_topic.save - flunk - rescue => e - assert_equal "Make the transaction rollback", e.message - assert_equal new_record_snapshot, !new_topic.persisted?, "The topic should have its old persisted value" - assert_equal id_snapshot, new_topic.id, "The topic should have its old id" - assert_equal id_present, new_topic.has_attribute?(Topic.primary_key) - ensure - remove_exception_raising_after_create_callback_to_topic - end + new_topic.approved = true + e = assert_raises(RuntimeError) { new_topic.save } + assert_equal "Make the transaction rollback", e.message + assert_equal new_record_snapshot, !new_topic.persisted?, "The topic should have its old persisted value" + assert_equal id_snapshot, new_topic.id, "The topic should have its old id" + assert_equal id_present, new_topic.has_attribute?(Topic.primary_key) end end def test_callback_rollback_in_create_with_record_invalid_exception - begin - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method(:after_create_for_transaction) - def after_create_for_transaction - raise ActiveRecord::RecordInvalid.new(Author.new) - end - eoruby + topic = Class.new(Topic) { + def after_create_for_transaction + raise ActiveRecord::RecordInvalid.new(Author.new) + end + } - new_topic = Topic.create(:title => "A new topic") - assert !new_topic.persisted?, "The topic should not be persisted" - assert_nil new_topic.id, "The topic should not have an ID" - ensure - remove_exception_raising_after_create_callback_to_topic - end + new_topic = topic.create(:title => "A new topic") + assert !new_topic.persisted?, "The topic should not be persisted" + assert_nil new_topic.id, "The topic should not have an ID" end def test_nested_explicit_transactions @@ -478,62 +457,16 @@ 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) - def after_save_for_transaction - raise 'Make the transaction rollback' - end - eoruby - end - - def remove_exception_raising_after_save_callback_to_topic - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method :after_save_for_transaction - def after_save_for_transaction; end - eoruby - end - - def add_exception_raising_after_create_callback_to_topic - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method(:after_create_for_transaction) - def after_create_for_transaction - raise 'Make the transaction rollback' - end - eoruby - end - - def remove_exception_raising_after_create_callback_to_topic - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method :after_create_for_transaction - def after_create_for_transaction; end - eoruby - end - - %w(validation save destroy).each do |filter| - define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method :before_#{filter}_for_transaction - def before_#{filter}_for_transaction - Book.create - false - end - eoruby - end - - define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method :before_#{filter}_for_transaction - def before_#{filter}_for_transaction; end - eoruby + %w(validation save destroy).each do |filter| + define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do |topic| + meta = class << topic; self; end + meta.send("define_method", "before_#{filter}_for_transaction") do + Book.create + false end end + end end class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase diff --git a/activerecord/test/fixtures/friendships.yml b/activerecord/test/fixtures/friendships.yml new file mode 100644 index 0000000000..1ee09175bf --- /dev/null +++ b/activerecord/test/fixtures/friendships.yml @@ -0,0 +1,4 @@ +Connection 1: + id: 1 + person_id: 1 + friend_id: 2
\ No newline at end of file diff --git a/activerecord/test/fixtures/people.yml b/activerecord/test/fixtures/people.yml index 123673a2af..e640a38f1f 100644 --- a/activerecord/test/fixtures/people.yml +++ b/activerecord/test/fixtures/people.yml @@ -4,15 +4,18 @@ michael: primary_contact_id: 2 number1_fan_id: 3 gender: M + followers_count: 1 david: id: 2 first_name: David primary_contact_id: 3 number1_fan_id: 1 gender: M + followers_count: 1 susan: id: 3 first_name: Susan primary_contact_id: 2 number1_fan_id: 1 gender: F + followers_count: 1 diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml index 93f48aedc4..2b042bd135 100644 --- a/activerecord/test/fixtures/topics.yml +++ b/activerecord/test/fixtures/topics.yml @@ -25,7 +25,7 @@ third: id: 3 title: The Third Topic of the day author_name: Carl - written_on: 2005-07-15t15:28:00.0099+01:00 + written_on: 2012-08-12t20:24:22.129346+00:00 content: I'm a troll approved: true replies_count: 1 diff --git a/activerecord/test/models/friendship.rb b/activerecord/test/models/friendship.rb new file mode 100644 index 0000000000..6b4f7acc38 --- /dev/null +++ b/activerecord/test/models/friendship.rb @@ -0,0 +1,4 @@ +class Friendship < ActiveRecord::Base + belongs_to :friend, class_name: 'Person' + belongs_to :follower, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :followers_count +end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index e204508986..6e6ff29f77 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -8,6 +8,8 @@ class Person < ActiveRecord::Base has_many :posts_with_no_comments, -> { includes(:comments).where('comments.id is null').references(:comments) }, :through => :readers, :source => :post + has_many :followers, foreign_key: 'friend_id', class_name: 'Friendship' + has_many :references has_many :bad_references has_many :fixed_bad_references, -> { where :favourite => true }, :class_name => 'BadReference' diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 6c919a2b02..7c45ca27c0 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -270,6 +270,11 @@ ActiveRecord::Schema.define do t.string :name end + create_table :friendships, :force => true do |t| + t.integer :friend_id + t.integer :person_id + end + create_table :goofy_string_id, :force => true, :id => false do |t| t.string :id, :null => false t.string :info @@ -476,6 +481,7 @@ ActiveRecord::Schema.define do t.references :number1_fan t.integer :lock_version, :null => false, :default => 0 t.string :comments + t.integer :followers_count, :default => 0 t.references :best_friend t.references :best_friend_of t.timestamps diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index b0b21a1563..4f57efb35d 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,15 @@ ## Rails 4.0.0 (unreleased) ## +* Remove `j` alias for `ERB::Util#json_escape`. + The `j` alias is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript` + and both modules are included in the view context that would confuse the developers. + + *Akira Matsuda* + +* Replace deprecated `memcache-client` gem with `dalli` in ActiveSupport::Cache::MemCacheStore + + *Guillermo Iguaran* + * Add default values to all `ActiveSupport::NumberHelper` methods, to avoid errors with empty locales or missing values. diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 56d6676961..41d77ab6c1 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -37,7 +37,6 @@ module ActiveSupport autoload :LogSubscriber autoload :Notifications - # TODO: Narrow this list down eager_autoload do autoload :BacktraceCleaner autoload :BasicObject @@ -55,12 +54,12 @@ module ActiveSupport autoload :OptionMerger autoload :OrderedHash autoload :OrderedOptions - autoload :Rescuable autoload :StringInquirer autoload :TaggedLogging autoload :XmlMini end + autoload :Rescuable autoload :SafeBuffer, "active_support/core_ext/string/output_safety" autoload :TestCase end diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 2e1ccb72d8..5aa78cc9f3 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -1,7 +1,7 @@ begin - require 'memcache' + require 'dalli' rescue LoadError => e - $stderr.puts "You don't have memcache-client installed in your application. Please add it to your Gemfile and run bundle install" + $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install" raise e end @@ -22,21 +22,13 @@ module ActiveSupport # 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" - NOT_STORED = "NOT_STORED\r\n" - EXISTS = "EXISTS\r\n" - NOT_FOUND = "NOT_FOUND\r\n" - DELETED = "DELETED\r\n" - end - ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n def self.build_mem_cache(*addresses) addresses = addresses.flatten options = addresses.extract_options! addresses = ["localhost:11211"] if addresses.empty? - MemCache.new(addresses, options) + Dalli::Client.new(addresses, options) end # Creates a new MemCacheStore object, with the given memcached server @@ -90,11 +82,11 @@ module ActiveSupport # to zero. def increment(name, amount = 1, options = nil) # :nodoc: options = merged_options(options) - response = instrument(:increment, name, :amount => amount) do + instrument(:increment, name, :amount => amount) do @data.incr(escape_key(namespaced_key(name, options)), amount) end - response == Response::NOT_FOUND ? nil : response.to_i - rescue MemCache::MemCacheError + rescue Dalli::DalliError + logger.error("DalliError (#{e}): #{e.message}") if logger nil end @@ -104,11 +96,11 @@ module ActiveSupport # to zero. def decrement(name, amount = 1, options = nil) # :nodoc: options = merged_options(options) - response = instrument(:decrement, name, :amount => amount) do + instrument(:decrement, name, :amount => amount) do @data.decr(escape_key(namespaced_key(name, options)), amount) end - response == Response::NOT_FOUND ? nil : response.to_i - rescue MemCache::MemCacheError + rescue Dalli::DalliError + logger.error("DalliError (#{e}): #{e.message}") if logger nil end @@ -116,6 +108,9 @@ module ActiveSupport # be used with care when shared cache is being used. def clear(options = nil) @data.flush_all + rescue Dalli::DalliError => e + logger.error("DalliError (#{e}): #{e.message}") if logger + nil end # Get the statistics from the memcached servers. @@ -126,9 +121,9 @@ module ActiveSupport 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 + deserialize_entry(@data.get(escape_key(key), options)) + rescue Dalli::DalliError => e + logger.error("DalliError (#{e}): #{e.message}") if logger nil end @@ -137,23 +132,17 @@ module ActiveSupport 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 + @data.send(method, escape_key(key), value, expires_in, options) + rescue Dalli::DalliError => e + logger.error("DalliError (#{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 + @data.delete(escape_key(key)) + rescue Dalli::DalliError => e + logger.error("DalliError (#{e}): #{e.message}") if logger false end diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index c17d695967..dad4b29d46 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -60,18 +60,11 @@ class ERB # json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}') # # => {name:john,created_at:2010-04-28T01:39:31Z,id:1} # - # This method is also aliased as +j+, and available as a helper - # in Rails templates: - # - # <%=j @person.to_json %> - # def json_escape(s) result = s.to_s.gsub(JSON_ESCAPE_REGEXP) { |special| JSON_ESCAPE[special] } s.html_safe? ? result.html_safe : result end - alias j json_escape - module_function :j module_function :json_escape end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index c9071a73d8..77cab6f08d 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -168,16 +168,30 @@ module ActiveSupport #:nodoc: end end - def const_missing(const_name, nesting = nil) + def const_missing(const_name) klass_name = name.presence || "Object" - unless nesting - # We'll assume that the nesting of Foo::Bar is ["Foo::Bar", "Foo"] - # even though it might not be, such as in the case of - # class Foo::Bar; Baz; end - nesting = [] - klass_name.to_s.scan(/::|$/) { nesting.unshift $` } - end + # Since Ruby does not pass the nesting at the point the unknown + # constant triggered the callback we cannot fully emulate constant + # name lookup and need to make a trade-off: we are going to assume + # that the nesting in the body of Foo::Bar is [Foo::Bar, Foo] even + # though it might not be. Counterexamples are + # + # class Foo::Bar + # Module.nesting # => [Foo::Bar] + # end + # + # or + # + # module M::N + # module S::T + # Module.nesting # => [S::T, M::N] + # end + # end + # + # for example. + nesting = [] + klass_name.to_s.scan(/::|$/) { nesting.unshift $` } # If there are multiple levels of nesting to search under, the top # level is the one we want to report as the lookup fail. @@ -287,13 +301,11 @@ module ActiveSupport #:nodoc: Object.class_eval { include Loadable } Module.class_eval { include ModuleConstMissing } Exception.class_eval { include Blamable } - true end def unhook! ModuleConstMissing.exclude_from(Module) Loadable.exclude_from(Object) - true end def load? diff --git a/activesupport/lib/active_support/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb index 4045db3232..df490ae298 100644 --- a/activesupport/lib/active_support/dependencies/autoload.rb +++ b/activesupport/lib/active_support/dependencies/autoload.rb @@ -1,52 +1,78 @@ require "active_support/inflector/methods" module ActiveSupport + # Autoload and eager load conveniences for your library. + # + # This module allows you to define autoloads based on + # Rails conventions (i.e. no need to define the path + # it is automatically guessed based on the filename) + # and also define a set of constants that needs to be + # eager loaded: + # + # module MyLib + # extend ActiveSupport::Autoload + # + # autoload :Model + # + # eager_autoload do + # autoload :Cache + # end + # end + # + # Then your library can be eager loaded by simply calling: + # + # MyLib.eager_load! + # module Autoload - @@autoloads = {} - @@under_path = nil - @@at_path = nil - @@eager_autoload = false + def self.extended(base) + base.class_eval do + @_autoloads = {} + @_under_path = nil + @_at_path = nil + @_eager_autoload = false + end + end - def autoload(const_name, path = @@at_path) + def autoload(const_name, path = @_at_path) unless path - full = [name, @@under_path, const_name.to_s, path].compact.join("::") + full = [name, @_under_path, const_name.to_s, path].compact.join("::") path = Inflector.underscore(full) end - if @@eager_autoload - @@autoloads[const_name] = path + if @_eager_autoload + @_autoloads[const_name] = path end super const_name, path end def autoload_under(path) - @@under_path, old_path = path, @@under_path + @_under_path, old_path = path, @_under_path yield ensure - @@under_path = old_path + @_under_path = old_path end def autoload_at(path) - @@at_path, old_path = path, @@at_path + @_at_path, old_path = path, @_at_path yield ensure - @@at_path = old_path + @_at_path = old_path end def eager_autoload - old_eager, @@eager_autoload = @@eager_autoload, true + old_eager, @_eager_autoload = @_eager_autoload, true yield ensure - @@eager_autoload = old_eager + @_eager_autoload = old_eager end - def self.eager_autoload! - @@autoloads.values.each { |file| require file } + def eager_load! + @_autoloads.values.each { |file| require file } end def autoloads - @@autoloads + @_autoloads end end end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 30ac881090..aa8d408da9 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -5,6 +5,8 @@ module ActiveSupport class Railtie < Rails::Railtie config.active_support = ActiveSupport::OrderedOptions.new + config.eager_load_namespaces << ActiveSupport + initializer "active_support.deprecation_behavior" do |app| if deprecation = app.config.active_support.deprecation ActiveSupport::Deprecation.behavior = deprecation diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 25ed962c23..34c9e920bc 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -22,11 +22,11 @@ ENV['NO_RELOAD'] = '1' require 'active_support' def uses_memcached(test_name) - require 'memcache' + require 'dalli' begin - MemCache.new('localhost:11211').stats + Dalli::Client.new('localhost:11211').stats yield - rescue MemCache::MemCacheError + rescue Dalli::DalliError $stderr.puts "Skipping #{test_name} tests. Start memcached and try again." end end diff --git a/activesupport/test/autoload.rb b/activesupport/test/autoload_test.rb index 5d8026a9ca..7d02d835a8 100644 --- a/activesupport/test/autoload.rb +++ b/activesupport/test/autoload_test.rb @@ -28,15 +28,6 @@ class TestAutoloadModule < ActiveSupport::TestCase assert_nothing_raised { ::Fixtures::Autoload::SomeClass } end - test ":eager constants can be triggered via ActiveSupport::Autoload.eager_autoload!" do - module ::Fixtures::Autoload - autoload :SomeClass, "fixtures/autoload/some_class" - end - ActiveSupport::Autoload.eager_autoload! - assert $LOADED_FEATURES.include?("fixtures/autoload/some_class.rb") - assert_nothing_raised { ::Fixtures::Autoload::SomeClass } - end - test "the location of autoloaded constants defaults to :name.underscore" do module ::Fixtures::Autoload autoload :SomeClass @@ -51,8 +42,7 @@ class TestAutoloadModule < ActiveSupport::TestCase autoload :SomeClass end - ActiveSupport::Autoload.eager_autoload! - assert $LOADED_FEATURES.include?("fixtures/autoload/some_class.rb") + ::Fixtures::Autoload.eager_load! assert_nothing_raised { ::Fixtures::Autoload::SomeClass } end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index a75db47be8..5056d8deb8 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -83,20 +83,20 @@ class CacheStoreSettingTest < ActiveSupport::TestCase end def test_mem_cache_fragment_cache_store - MemCache.expects(:new).with(%w[localhost], {}) + Dalli::Client.expects(:new).with(%w[localhost], {}) store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) end def test_mem_cache_fragment_cache_store_with_given_mem_cache - mem_cache = MemCache.new - MemCache.expects(:new).never + mem_cache = Dalli::Client.new + Dalli::Client.expects(:new).never store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) end def test_mem_cache_fragment_cache_store_with_given_mem_cache_like_object - MemCache.expects(:new).never + Dalli::Client.expects(:new).never memcache = Object.new def memcache.get() true end store = ActiveSupport::Cache.lookup_store :mem_cache_store, memcache @@ -104,13 +104,13 @@ class CacheStoreSettingTest < ActiveSupport::TestCase end def test_mem_cache_fragment_cache_store_with_multiple_servers - MemCache.expects(:new).with(%w[localhost 192.168.1.1], {}) + Dalli::Client.expects(:new).with(%w[localhost 192.168.1.1], {}) store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1' assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) end def test_mem_cache_fragment_cache_store_with_options - MemCache.expects(:new).with(%w[localhost 192.168.1.1], { :timeout => 10 }) + Dalli::Client.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] @@ -447,6 +447,7 @@ module CacheIncrementDecrementBehavior assert_equal 2, @cache.read('foo').to_i assert_equal 3, @cache.increment('foo') assert_equal 3, @cache.read('foo').to_i + assert_nil @cache.increment('bar') end def test_decrement @@ -456,6 +457,7 @@ module CacheIncrementDecrementBehavior assert_equal 2, @cache.read('foo').to_i assert_equal 1, @cache.decrement('foo') assert_equal 1, @cache.read('foo').to_i + assert_nil @cache.decrement('bar') end end diff --git a/guides/source/4_0_release_notes.textile b/guides/source/4_0_release_notes.textile index 0de8a0a4b7..2f21f8cc71 100644 --- a/guides/source/4_0_release_notes.textile +++ b/guides/source/4_0_release_notes.textile @@ -82,8 +82,6 @@ will generate the model with <tt>belongs_to :supplier, polymorphic: true</tt> as * Load all environments available in <tt>config.paths["config/environments"]</tt>. -* The application generator generates <tt>public/humans.txt</tt> with some basic data. - * Add <tt>config.queue_consumer</tt> to allow the default consumer to be configurable. * Add <tt>Rails.queue</tt> as an interface with a default implementation that consumes jobs in a separate thread. diff --git a/guides/source/active_record_validations_callbacks.textile b/guides/source/active_record_validations_callbacks.textile index cf7293bd9e..b866337e3f 100644 --- a/guides/source/active_record_validations_callbacks.textile +++ b/guides/source/active_record_validations_callbacks.textile @@ -532,6 +532,16 @@ end Person.new.valid? => ActiveModel::StrictValidationFailed: Name can't be blank </ruby> +There is also an ability to pass custom exception to +:strict+ option + +<ruby> +class Person < ActiveRecord::Base + validates :token, :presence => true, :uniqueness => true, :strict => TokenGenerationException +end + +Person.new.valid? => TokenGenerationException: Token can't be blank +</ruby> + h3. Conditional Validation Sometimes it will make sense to validate an object just when a given predicate is satisfied. You can do that by using the +:if+ and +:unless+ options, which can take a symbol, a string, a +Proc+ or an +Array+. You may use the +:if+ option when you want to specify when the validation *should* happen. If you want to specify when the validation *should not* happen, then you may use the +:unless+ option. diff --git a/guides/source/configuring.textile b/guides/source/configuring.textile index 5ed3ad4a6b..27eaf1cbc5 100644 --- a/guides/source/configuring.textile +++ b/guides/source/configuring.textile @@ -50,8 +50,6 @@ config.after_initialize do end </ruby> -* +config.allow_concurrency+ should be true to allow concurrent (threadsafe) action processing. False by default. You probably don't want to call this one directly, though, because a series of other adjustments need to be made for threadsafe mode to work properly. Can also be enabled with +threadsafe!+. - * +config.asset_host+ sets the host for the assets. Useful when CDNs are used for hosting assets, or when you want to work around the concurrency constraints builtin in browsers using different domain aliases. Shorter version of +config.action_controller.asset_host+. * +config.asset_path+ lets you decorate asset paths. This can be a callable, a string, or be +nil+ which is the default. For example, the normal path for +blog.js+ would be +/javascripts/blog.js+, let that absolute path be +path+. If +config.asset_path+ is a callable, Rails calls it when generating asset paths passing +path+ as argument. If +config.asset_path+ is a string, it is expected to be a +sprintf+ format string with a +%s+ where +path+ will get inserted. In either case, Rails outputs the decorated path. Shorter version of +config.action_controller.asset_path+. @@ -89,6 +87,10 @@ end * +config.dependency_loading+ is a flag that allows you to disable constant autoloading setting it to false. It only has effect if +config.cache_classes+ is true, which it is by default in production mode. This flag is set to false by +config.threadsafe!+. +* +config.eager_load+ when true, eager loads all registered `config.eager_load_namespaces`. This includes your application, engines, Rails frameworks and any other registered namespace. + +* +config.eager_load_namespaces+ registers namespaces that are eager loaded when +config.eager_load+ is true. All namespaces in the list must respond to the +eager_load!+ method. + * +config.eager_load_paths+ accepts an array of paths from which Rails will eager load on boot if cache classes is enabled. Defaults to every folder in the +app+ directory of the application. * +config.encoding+ sets up the application-wide encoding. Defaults to UTF-8. @@ -109,8 +111,6 @@ end * +config.middleware+ allows you to configure the application's middleware. This is covered in depth in the "Configuring Middleware":#configuring-middleware section below. -* +config.preload_frameworks+ enables or disables preloading all frameworks at startup. Enabled by +config.threadsafe!+. Defaults to +nil+, so is disabled. - * +config.queue+ configures a different queue implementation for the application. Defaults to +Rails::Queueing::Queue+. Note that, if the default queue is changed, the default +queue_consumer+ is not going to be initialized, it is up to the new queue implementation to handle starting and shutting down its own consumer(s). * +config.queue_consumer+ configures a different consumer implementation for the default queue. Defaults to +Rails::Queueing::ThreadedConsumer+. @@ -129,10 +129,6 @@ config.session_store :my_custom_store This custom store must be defined as +ActionDispatch::Session::MyCustomStore+. In addition to symbols, they can also be objects implementing a certain API, like +ActiveRecord::SessionStore+, in which case no special namespace is required. -* +config.threadsafe!+ enables +allow_concurrency+, +cache_classes+, +dependency_loading+ and +preload_frameworks+ to make the application threadsafe. - -WARNING: Threadsafe operation is incompatible with the normal workings of development mode Rails. In particular, automatic dependency loading and class reloading are automatically disabled when you call +config.threadsafe!+. - * +config.time_zone+ sets the default time zone for the application and enables time zone awareness for Active Record. * +config.whiny_nils+ enables or disables warnings when a certain set of methods are invoked on +nil+ and it does not respond to them. Defaults to true in development and test environments. @@ -203,7 +199,7 @@ Every Rails application comes with a standard set of middleware which it uses in * +ActionDispatch::SSL+ forces every request to be under HTTPS protocol. Will be available if +config.force_ssl+ is set to +true+. Options passed to this can be configured by using +config.ssl_options+. * +ActionDispatch::Static+ is used to serve static assets. Disabled if +config.serve_static_assets+ is +true+. -* +Rack::Lock+ wraps the app in mutex so it can only be called by a single thread at a time. Only enabled if +config.action_controller.allow_concurrency+ is set to +false+, which it is by default. +* +Rack::Lock+ wraps the app in mutex so it can only be called by a single thread at a time. Only enabled when +config.cache_classes_+ is +false+. * +ActiveSupport::Cache::Strategy::LocalCache+ serves as a basic memory backed cache. This cache is not thread safe and is intended only for serving as a temporary memory cache for a single thread. * +Rack::Runtime+ sets an +X-Runtime+ header, containing the time (in seconds) taken to execute the request. * +Rails::Rack::Logger+ notifies the logs that the request has began. After request is complete, flushes all the logs. @@ -341,7 +337,7 @@ h4. Configuring Action Dispatch * +config.action_dispatch.default_headers+ is a hash with HTTP headers that are set by default in each response. By default, this is defined as: <ruby> -config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-XSS-Protection' => '1; mode=block' } +config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-XSS-Protection' => '1; mode=block', 'X-Content-Type-Options' => 'nosniff' } </ruby> * +config.action_dispatch.tld_length+ sets the TLD (top-level domain) length for the application. Defaults to +1+. @@ -656,8 +652,6 @@ Serves as a placeholder so that +:load_environment_config+ can be defined to run *+load_active_support+* Requires +active_support/dependencies+ which sets up the basis for Active Support. Optionally requires +active_support/all+ if +config.active_support.bare+ is un-truthful, which is the default. -*+preload_frameworks+* Loads all autoload dependencies of Rails automatically if +config.preload_frameworks+ is +true+ or "truthful". By default this configuration option is disabled. In Rails, when internal classes are referenced for the first time they are autoloaded. +:preload_frameworks+ loads all of this at once on initialization. - *+initialize_logger+* Initializes the logger (an +ActiveSupport::BufferedLogger+ object) for the application and makes it accessible at +Rails.logger+, provided that no initializer inserted before this point has defined +Rails.logger+. *+initialize_cache+* If +Rails.cache+ isn't set yet, initializes the cache by referencing the value in +config.cache_store+ and stores the outcome as +Rails.cache+. If this object responds to the +middleware+ method, its middleware is inserted before +Rack::Runtime+ in the middleware stack. @@ -752,13 +746,13 @@ The error occurred while evaluating nil.each *+build_middleware_stack+* Builds the middleware stack for the application, returning an object which has a +call+ method which takes a Rack environment object for the request. -*+eager_load!+* If +config.cache_classes+ is true, runs the +config.before_eager_load+ hooks and then calls +eager_load!+ which will load all the Ruby files from +config.eager_load_paths+. +*+eager_load!+* If +config.eager_load+ is true, runs the +config.before_eager_load+ hooks and then calls +eager_load!+ which will load all +config.eager_load_namespaces+. *+finisher_hook+* Provides a hook for after the initialization of process of the application is complete, as well as running all the +config.after_initialize+ blocks for the application, railties and engines. *+set_routes_reloader+* Configures Action Dispatch to reload the routes file using +ActionDispatch::Callbacks.to_prepare+. -*+disable_dependency_loading+* Disables the automatic dependency loading if the +config.cache_classes+ is set to true and +config.dependency_loading+ is set to false. +*+disable_dependency_loading+* Disables the automatic dependency loading if the +config.eager_load+ is set to true. h3. Database pooling diff --git a/guides/source/upgrading_ruby_on_rails.textile b/guides/source/upgrading_ruby_on_rails.textile index 5024bc4c37..cdf7306264 100644 --- a/guides/source/upgrading_ruby_on_rails.textile +++ b/guides/source/upgrading_ruby_on_rails.textile @@ -44,6 +44,8 @@ The <tt>delete</tt> method in collection associations can now receive <tt>Fixnum Rails 4.0 has changed how orders get stacked in +ActiveRecord::Relation+. In previous versions of rails new order was applied after previous defined order. But this is no long true. Check "ActiveRecord Query guide":active_record_querying.html#ordering for more information. +Rails 4.0 has changed <tt>serialized_attributes</tt> and <tt>_attr_readonly</tt> to class methods only. Now you shouldn't use instance methods, it's deprecated. You must change them, e.g. <tt>self.serialized_attributes</tt> to <tt>self.class.serialized_attributes</tt>. + h4(#active_model4_0). Active Model Rails 4.0 has changed how errors attach with the <tt>ActiveModel::Validations::ConfirmationValidator</tt>. Now when confirmation validations fail the error will be attached to <tt>:#{attribute}_confirmation</tt> instead of <tt>attribute</tt>. @@ -54,6 +56,10 @@ Rails 4.0 changed how <tt>assert_generates</tt>, <tt>assert_recognizes</tt>, and Rails 4.0 also changed the way unicode character routes are drawn. Now you can draw unicode character routes directly. If you already draw such routes, you must change them, e.g. <tt>get Rack::Utils.escape('こんにちは'), :controller => 'welcome', :action => 'index'</tt> to <tt>get 'こんにちは', :controller => 'welcome', :action => 'index'</tt>. +h4(#active_support4_0). Active Support + +Rails 4.0 Removed the `j` alias for `ERB::Util#json_escape` since `j` is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript`. + h4(#helpers_order). Helpers Loading Order The loading order of helpers from more than one directory has changed in Rails 4.0. Previously, helpers from all directories were gathered and then sorted alphabetically. After upgrade to Rails 4.0 helpers will preserve the order of loaded directories and will be sorted alphabetically only within each directory. Unless you explicitly use <tt>helpers_path</tt> parameter, this change will only impact the way of loading helpers from engines. If you rely on the fact that particular helper from engine loads before or after another helper from application or another engine, you should check if correct methods are available after upgrade. If you would like to change order in which engines are loaded, you can use <tt>config.railties_order=</tt> method. diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 851f41249a..fea18b5f47 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -5,6 +5,8 @@ default_url_options[:script_name] to set proper application's mount point by yourself. *Piotr Sarnacki* +* `config.threadsafe!` is deprecated in favor of `config.eager_load` which provides a more fine grained control on what is eager loaded *José Valim* + * The migration generator will now produce AddXXXToYYY/RemoveXXXFromYYY migrations with references statements, for instance rails g migration AddReferencesToProducts user:references supplier:references{polymorphic} diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 3531728421..ae872f2bb0 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -16,7 +16,7 @@ module Rails # # Besides providing the same configuration as Rails::Engine and Rails::Railtie, # the application object has several specific configurations, for example - # "allow_concurrency", "cache_classes", "consider_all_requests_local", "filter_parameters", + # "cache_classes", "consider_all_requests_local", "filter_parameters", # "logger" and so forth. # # Check Rails::Application::Configuration to see them all. @@ -216,8 +216,9 @@ module Rails railties.each { |r| r.run_tasks_blocks(app) } super require "rails/tasks" + config = self.config task :environment do - $rails_rake_task = true + config.eager_load = false require_environment! end end @@ -296,7 +297,7 @@ module Rails middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control end - middleware.use ::Rack::Lock unless config.allow_concurrency + middleware.use ::Rack::Lock unless config.cache_classes middleware.use ::Rack::Runtime middleware.use ::Rack::MethodOverride middleware.use ::ActionDispatch::RequestId diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index e567df7162..a1bc95550b 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -13,11 +13,18 @@ module Rails require "active_support/all" unless config.active_support.bare end - # Preload all frameworks specified by the Configuration#frameworks. - # Used by Passenger to ensure everything's loaded before forking and - # to avoid autoload race conditions in JRuby. - initializer :preload_frameworks, :group => :all do - ActiveSupport::Autoload.eager_autoload! if config.preload_frameworks + initializer :set_eager_load, :group => :all do + if config.eager_load.nil? + warn <<-INFO +config.eager_load is set to nil. Please update your config/environments/*.rb files accordingly: + + * development - set it to false + * test - set it to false (unless you use a tool that preloads your test environment) + * production - set it to true + +INFO + config.eager_load = config.cache_classes + end end # Initialize the logger early in the stack in case we need to log some deprecation. @@ -60,7 +67,6 @@ module Rails end # Sets the dependency loading mechanism. - # TODO: Remove files from the $" and always use require. initializer :initialize_dependency_mechanism, :group => :all do ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 2b36cc9d0d..7f05b2e7e1 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -5,11 +5,11 @@ require 'rails/engine/configuration' module Rails class Application class Configuration < ::Rails::Engine::Configuration - attr_accessor :allow_concurrency, :asset_host, :asset_path, :assets, :autoflush_log, + attr_accessor :asset_host, :asset_path, :assets, :autoflush_log, :cache_classes, :cache_store, :consider_all_requests_local, :console, - :dependency_loading, :exceptions_app, :file_watcher, :filter_parameters, + :eager_load, :exceptions_app, :file_watcher, :filter_parameters, :force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags, - :preload_frameworks, :railties_order, :relative_url_root, :secret_token, + :railties_order, :relative_url_root, :secret_token, :serve_static_assets, :ssl_options, :static_cache_control, :session_options, :time_zone, :reload_classes_only_on_change, :queue, :queue_consumer @@ -20,11 +20,9 @@ module Rails def initialize(*) super self.encoding = "utf-8" - @allow_concurrency = false @consider_all_requests_local = false @filter_parameters = [] @helpers_paths = [] - @dependency_loading = true @serve_static_assets = true @static_cache_control = nil @force_ssl = false @@ -45,6 +43,7 @@ module Rails @log_formatter = ActiveSupport::Logger::SimpleFormatter.new @queue = Rails::Queueing::Queue @queue_consumer = Rails::Queueing::ThreadedConsumer + @eager_load = nil @assets = ActiveSupport::OrderedOptions.new @assets.enabled = false @@ -91,15 +90,12 @@ module Rails end end - # Enable threaded mode. Allows concurrent requests to controller actions and - # multiple database connections. Also disables automatic dependency loading - # after boot, and disables reloading code on every request, as these are - # fundamentally incompatible with thread safety. def threadsafe! - @preload_frameworks = true + ActiveSupport::Deprecation.warn "config.threadsafe! is deprecated. Rails applications " \ + "behave by default as thread safe in production as long as config.cache_classes and " \ + "config.eager_load are set to true" @cache_classes = true - @dependency_loading = false - @allow_concurrency = true + @eager_load = true self end diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 60aa40b92f..1952a0fc3a 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -50,9 +50,9 @@ module Rails end initializer :eager_load! do - if config.cache_classes && !(defined?($rails_rake_task) && $rails_rake_task) + if config.eager_load ActiveSupport.run_load_hooks(:before_eager_load, self) - eager_load! + config.eager_load_namespaces.each(&:eager_load!) end end @@ -91,7 +91,7 @@ module Rails # Disable dependency loading during request cycle initializer :disable_dependency_loading do - if config.cache_classes && !config.dependency_loading + if config.eager_load ActiveSupport::Dependencies.unhook! end end diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 9ef64da3ef..a684129353 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -87,6 +87,15 @@ module Rails middlewares = [] middlewares << [Rails::Rack::Debugger] if options[:debugger] middlewares << [::Rack::ContentLength] + + # FIXME: add Rack::Lock in the case people are using webrick. + # This is to remain backwards compatible for those who are + # running webrick in production. We should consider removing this + # in development. + if server.name == 'Rack::Handler::WEBrick' + middlewares << [::Rack::Lock] + end + Hash.new(middlewares) end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 40f35ae5a6..3a5caf9f62 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -339,11 +339,16 @@ module Rails class << self attr_accessor :called_from, :isolated + alias :isolated? :isolated alias :engine_name :railtie_name + delegate :eager_load!, to: :instance + def inherited(base) unless base.abstract_railtie? + Rails::Railtie::Configuration.eager_load_namespaces << base + base.called_from = begin # Remove the line number from backtraces making sure we don't leave anything behind call_stack = caller.map { |p| p.sub(/:\d+.*/, '') } diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 5838c9fc38..383bea9e54 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -140,14 +140,14 @@ module Rails gem 'rails', path: '#{Rails::Generators::RAILS_DEV_PATH}' gem 'journey', github: 'rails/journey' gem 'arel', github: 'rails/arel' - gem 'active_record_deprecated_finders', github: 'rails/active_record_deprecated_finders' + gem 'activerecord-deprecated_finders', github: 'rails/activerecord-deprecated_finders' GEMFILE elsif options.edge? <<-GEMFILE.strip_heredoc gem 'rails', github: 'rails/rails' gem 'journey', github: 'rails/journey' gem 'arel', github: 'rails/arel' - gem 'active_record_deprecated_finders', github: 'rails/active_record_deprecated_finders' + gem 'activerecord-deprecated_finders', github: 'rails/activerecord-deprecated_finders' GEMFILE else <<-GEMFILE.strip_heredoc diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb index b2b760ee7b..1ac0248bcf 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -60,7 +60,7 @@ module <%= app_const_base %> config.assets.version = '1.0' <% end -%> - # Enable app-wide asynchronous ActionMailer + # Enable app-wide asynchronous ActionMailer. # config.action_mailer.async = true end end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environment.rb b/railties/lib/rails/generators/rails/app/templates/config/environment.rb index 1684986a59..e080ebd74e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environment.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/environment.rb @@ -1,5 +1,5 @@ -# Load the rails application +# Load the rails application. require File.expand_path('../application', __FILE__) -# Initialize the rails application +# Initialize the rails application. <%= app_const %>.initialize! diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index fee9eb9456..122e7e2b34 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -6,6 +6,9 @@ # since you don't have to restart the web server when you make code changes. config.cache_classes = false + # Do not eager load code on boot. + config.eager_load = false + # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false @@ -39,6 +42,6 @@ config.assets.debug = true <%- end -%> - # In development, use an in-memory queue for queueing + # In development, use an in-memory queue for queueing. config.queue = Rails::Queueing::Queue end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 2ffae66a57..a627636089 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -4,6 +4,12 @@ # Code is not reloaded between requests. config.cache_classes = true + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both thread web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true @@ -71,10 +77,10 @@ # Disable automatic flushing of the log to improve performance. # config.autoflush_log = false - # Use default logging formatter so that PID and timestamp are not suppressed + # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Default the production mode queue to an in-memory queue. You will probably - # want to replace this with an out-of-process queueing solution + # want to replace this with an out-of-process queueing solution. config.queue = Rails::Queueing::Queue end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt index b27b88a3c6..8ab27eb6e1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt @@ -7,6 +7,11 @@ # and recreated between test runs. Don't rely on the data there! config.cache_classes = true + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + # Configure static asset server for tests with Cache-Control for performance. config.serve_static_assets = true config.static_cache_control = "public, max-age=3600" @@ -34,6 +39,6 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Use the testing queue + # Use the testing queue. config.queue = Rails::Queueing::TestQueue end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt index e02397aaf9..21a5de4cba 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt @@ -2,8 +2,11 @@ # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! + # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. +# You can generate a safe secret key with the command `rake secret`. + # Make sure your secret_token is kept private # if you're sharing your code publicly. <%= app_const %>.config.secret_token = '<%= app_secret %>' diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt index ade0c4f78c..ff676280cb 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt @@ -4,5 +4,5 @@ # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information -# (create the session table with "rails generate session_migration") +# (create the session table with "rails generate session_migration"). # <%= app_const %>.config.session_store :active_record_store diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb index 85acac6725..f6b1ef1feb 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb @@ -8,11 +8,11 @@ # Sample of regular route: # get 'products/:id' => 'catalog#view' - # Keep in mind you can assign values other than :controller and :action + # Keep in mind you can assign values other than :controller and :action. # Sample of named route: # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase - # This route can be invoked with purchase_url(id: product.id) + # This route can be invoked with purchase_url(id: product.id). # Sample resource route (maps HTTP verbs to controller actions automatically): # resources :products @@ -35,7 +35,7 @@ # resource :seller # end - # Sample resource route with more complex sub-resources + # Sample resource route with more complex sub-resources: # resources :products do # resources :comments # resources :sales do @@ -51,5 +51,5 @@ # end - # See how all your routes lay out with "rake routes" + # See how all your routes lay out with "rake routes". end diff --git a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb index ab0e440bc4..4f937ad65a 100644 --- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb +++ b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb @@ -155,7 +155,7 @@ task :default => :test :desc => "Create dummy application at given path" class_option :full, :type => :boolean, :default => false, - :desc => "Generate rails engine with integration tests" + :desc => "Generate a rails engine with bundled Rails application for testing" class_option :mountable, :type => :boolean, :default => false, :desc => "Generate mountable isolated application" diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 1cb99463cc..fba685c769 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -178,9 +178,6 @@ module Rails @config ||= Railtie::Configuration.new end - def eager_load! - end - def railtie_namespace @railtie_namespace ||= self.class.parents.detect { |n| n.respond_to?(:railtie_namespace) } end diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb index 1c6b3769a5..9dc8843887 100644 --- a/railties/lib/rails/railtie/configuration.rb +++ b/railties/lib/rails/railtie/configuration.rb @@ -7,6 +7,16 @@ module Rails @@options ||= {} end + # Expose the eager_load_namespaces at "module" level for convenience. + def self.eager_load_namespaces #:nodoc: + @@eager_load_namespaces ||= [] + end + + # All namespaces that are eager loaded + def eager_load_namespaces + @@eager_load_namespaces ||= [] + end + # Add files that should be watched for change. def watchable_files @@watchable_files ||= [] diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index c813defe93..ebdbbaee8b 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -139,22 +139,19 @@ module ApplicationTests assert_instance_of Pathname, Rails.root end - test "marking the application as threadsafe sets the correct config variables" do + test "initialize an eager loaded, cache classes app" do add_to_config <<-RUBY - config.threadsafe! + config.eager_load = true + config.cache_classes = true RUBY require "#{app_path}/config/application" - assert AppTemplate::Application.config.allow_concurrency + assert AppTemplate::Application.initialize! end - test "initialize a threadsafe app" do - add_to_config <<-RUBY - config.threadsafe! - RUBY - + test "application is always added to eager_load namespaces" do require "#{app_path}/config/application" - assert AppTemplate::Application.initialize! + assert AppTemplate::Application, AppTemplate::Application.config.eager_load_namespaces end test "asset_path defaults to nil for application" do @@ -162,10 +159,11 @@ module ApplicationTests assert_equal nil, AppTemplate::Application.config.asset_path end - test "the application can be marked as threadsafe when there are no frameworks" do + test "the application can be eager loaded even when there are no frameworks" do FileUtils.rm_rf("#{app_path}/config/environments") add_to_config <<-RUBY - config.threadsafe! + config.eager_load = true + config.cache_classes = true RUBY use_frameworks [] @@ -175,22 +173,6 @@ module ApplicationTests end end - test "frameworks are not preloaded by default" do - require "#{app_path}/config/environment" - - assert ActionController.autoload?(:Caching) - end - - test "frameworks are preloaded with config.preload_frameworks is set" do - add_to_config <<-RUBY - config.preload_frameworks = true - RUBY - - require "#{app_path}/config/environment" - - assert !ActionView.autoload?(:AssetPaths) - end - test "filter_parameters should be able to set via config.filter_parameters" do add_to_config <<-RUBY config.filter_parameters += [ :foo, 'bar', lambda { |key, value| diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb index 066f0c1c84..9d97bae9ae 100644 --- a/railties/test/application/middleware/remote_ip_test.rb +++ b/railties/test/application/middleware/remote_ip_test.rb @@ -4,20 +4,6 @@ module ApplicationTests class RemoteIpTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation - def setup - build_app - boot_rails - FileUtils.rm_rf "#{app_path}/config/environments" - end - - def teardown - teardown_app - end - - def app - @app ||= Rails.application - end - def remote_ip(env = {}) remote_ip = nil env = Rack::MockRequest.env_for("/").merge(env).merge!( diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 9c9ed0cd6b..34b460d0a7 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -87,8 +87,8 @@ module ApplicationTests assert !middleware.include?("ActiveRecord::QueryCache") end - test "removes lock if allow concurrency is set" do - add_to_config "config.allow_concurrency = true" + test "removes lock if cache classes is set" do + add_to_config "config.cache_classes = true" boot! assert !middleware.include?("Rack::Lock") end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index a450f90dbf..b05fe3aed5 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -55,6 +55,29 @@ module ApplicationTests assert_match "Doing something...", output end + def test_does_not_explode_when_accessing_a_model_with_eager_load + add_to_config <<-RUBY + config.eager_load = true + + rake_tasks do + task :do_nothing => :environment do + Hello.new.world + end + end + RUBY + + app_file "app/models/hello.rb", <<-RUBY + class Hello + def world + puts "Hello world" + end + end + RUBY + + output = Dir.chdir(app_path){ `rake do_nothing` } + assert_match "Hello world", output + end + def test_code_statistics_sanity assert_match "Code LOC: 5 Test LOC: 0 Code to Test Ratio: 1:0.0", Dir.chdir(app_path){ `rake stats` } diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 8f04692aef..37839a1c54 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -12,7 +12,6 @@ require 'bundler/setup' require 'minitest/autorun' require 'active_support/test_case' -# TODO: Remove setting this magic constant RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..") # These files do not require any others and are needed @@ -118,6 +117,7 @@ module TestHelpers end add_to_config <<-RUBY + config.eager_load = false config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" config.session_store :cookie_store, :key => "_myapp_session" config.active_support.deprecation = :log @@ -136,6 +136,7 @@ module TestHelpers require "action_controller/railtie" app = Class.new(Rails::Application) + app.config.eager_load = false app.config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" app.config.session_store :cookie_store, :key => "_myapp_session" app.config.active_support.deprecation = :log |