diff options
97 files changed, 1443 insertions, 893 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index b9bff4958f..5be7f34331 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,17 +1,29 @@ ## Rails 4.0.0 (unreleased) ## -* Remove support for parsing XML parameters from request. If you still want to parse XML - parameters, please install `actionpack-xml_parser' gem. +* Fix `respond_to` not using formats that have no block if all is present. *Michael Grosser* - *Prem Sichanugrist* +* New applications use an encrypted session store by default. + + *Santiago Pastorino* + +* Determine the controller#action from only the matched path when using the + shorthand syntax. Previously the complete path was used, which led + to problems with nesting (scopes and namespaces). + Fixes #7554. -* Fix `time_zone_options_for_select` to call `dup` on the returned TimeZone array. + Example: + + # This will route to questions#new. + scope ':locale' do + get 'questions/new' + end + + *Yves Senn* - Previously if you supplied :priority_zones options to `time_zone_options_for_select` - the memoized ActiveSupport::TimeZone.all array would be mutated. Calling - `dup` prevents mutation of the main TimeZones array. +* Remove support for parsing XML parameters from request. If you still want to parse XML + parameters, please install `actionpack-xml_parser' gem. - *Brian McManus* + *Prem Sichanugrist* * Remove support for parsing YAML parameters from request. @@ -19,7 +31,7 @@ * Add a message when you have no routes defined to both `rake routes` and GET "/rails/info/routes" that lets you know you have none defined and links - to the Rails Guide on the topic. + to the Rails guide on the topic. *Steve Klabnik* @@ -33,31 +45,30 @@ screen readers by converting both hyphens and underscores to spaces. Before: + image_tag('underscored_file_name.png') # => <img alt="Underscored_file_name" src="/assets/underscored_file_name.png" /> After: + image_tag('underscored_file_name.png') # => <img alt="Underscored file name" src="/assets/underscored_file_name.png" /> *Nick Cox* -* We don't support the `:controller` option for route definitions - with the ruby constant notation. This will now result in an - `ArgumentError`. +* We don't support Ruby constant notation in the `:controller` option for route + definitions. So, this raises an `ArgumentError` now: - Example: - # This raises an ArgumentError: - resources :posts, :controller => "Admin::Posts" + resources :posts, controller: "Admin::Posts" # WRONG + + Use path notation instead: - # Use directory notation instead: - resources :posts, :controller => "admin/posts" + resources :posts, controller: "admin/posts" # RIGHT *Yves Senn* * `assert_template` can be used to verify the locals of partials, which live inside a directory. - Fixes #8516. # Prefixed partials inside directories worked and still work. assert_template partial: 'directory/_partial', locals: {name: 'John'} @@ -65,23 +76,25 @@ # This did not work but does now. assert_template partial: 'directory/partial', locals: {name: 'John'} + Fixes #8516. + *Yves Senn* -* Fix `content_tag_for` with array html option. +* Fix `content_tag_for` with array HTML option. It would embed array as string instead of joining it like `content_tag` does: content_tag(:td, class: ["foo", "bar"]){} - #=> '<td class="foo bar"></td>' + # => <td class="foo bar"></td> Before: content_tag_for(:td, item, class: ["foo", "bar"]) - #=> '<td class="item ["foo", "bar"]" id="item_1"></td>' + # => <td class="item ["foo", "bar"]" id="item_1"></td> After: content_tag_for(:td, item, class: ["foo", "bar"]) - #=> '<td class="item foo bar" id="item_1"></td>' + # => <td class="item foo bar" id="item_1"></td> *Semyon Perepelitsa* @@ -101,35 +114,18 @@ *Piotr Sarnacki* -* Add javascript based routing path matcher to `/rails/info/routes`. +* Add JavaScript based routing path matcher to `/rails/info/routes`. Routes can now be filtered by whether or not they match a path. *Richard Schneeman* -* Given - - params.permit(:name) - - `:name` passes if it is a key of `params` whose value is a permitted scalar. - - Similarly, given - - params.permit(tags: []) - - `:tags` passes if it is a key of `params` whose value is an array of - permitted scalars. - - Permitted scalars filtering happens at any level of nesting. - - *Xavier Noria* - * Change the behavior of route defaults so that explicit defaults are no longer required where the key is not part of the path. For example: resources :posts, bucket_type: 'posts' will be required whenever constructing the url from a hash such as a functional - test or using url_for directly. However using the explicit form alters the + test or using `url_for` directly. However using the explicit form alters the behavior so it's not required: resources :projects, defaults: { bucket_type: 'projects' } @@ -163,7 +159,7 @@ *Colin Burn-Murdoch* -* Fixed json params parsing regression for non-object JSON content. +* Fixed JSON params parsing regression for non-object JSON content. *Dylan Smith* @@ -201,12 +197,13 @@ * Do not append second slash to `root_url` when using `trailing_slash: true` Fix #8700 - Example: - # before - root_url # => http://test.host// + Before: - # after - root_url # => http://test.host/ + root_url(trailing_slash: true) # => http://test.host// + + After: + + root_url(trailing_slash: true) # => http://test.host/ *Yves Senn* @@ -230,8 +227,8 @@ *Yves Senn* -* Added `Mime::NullType` class. This allows to use html?, xml?, json?..etc when - the `format` of `request` is unknown, without raise an exception. +* Added `Mime::NullType` class. This allows to use `html?`, `xml?`, `json?`, etc. + when the format of the request is unknown, without raising an exception. *Angelo Capilleri* @@ -256,7 +253,7 @@ *Matt Venables* -* Prevent raising EOFError on multipart GET request (IE issue). *Adam Stankiewicz* +* Prevent raising `EOFError` on multipart GET request (IE issue). *Adam Stankiewicz* * Rename all action callbacks from *_filter to *_action to avoid the misconception that these callbacks are only suited for transforming or halting the response. With the new style, @@ -302,7 +299,7 @@ *Stephen Ausman + Fabrizio Regini + Angelo Capilleri* -* Add filter capability to ActionController logs for redirect locations: +* Add logging filter capability for redirect URLs: config.filter_redirect << 'http://please.hide.it/' @@ -401,23 +398,17 @@ Before: check_box("post", "comment_ids", { multiple: true, index: "foo" }, 1) - #=> <input name=\"post[foo][comment_ids]\" type=\"hidden\" value=\"0\" /><input id=\"post_foo_comment_ids_1\" name=\"post[foo][comment_ids]\" type=\"checkbox\" value=\"1\" /> + # => <input name=\"post[foo][comment_ids]\" type=\"hidden\" value=\"0\" /><input id=\"post_foo_comment_ids_1\" name=\"post[foo][comment_ids]\" type=\"checkbox\" value=\"1\" /> After: check_box("post", "comment_ids", { multiple: true, index: "foo" }, 1) - #=> <input name=\"post[foo][comment_ids][]\" type=\"hidden\" value=\"0\" /><input id=\"post_foo_comment_ids_1\" name=\"post[foo][comment_ids][]\" type=\"checkbox\" value=\"1\" /> + # => <input name=\"post[foo][comment_ids][]\" type=\"hidden\" value=\"0\" /><input id=\"post_foo_comment_ids_1\" name=\"post[foo][comment_ids][]\" type=\"checkbox\" value=\"1\" /> Fix #8108. *Daniel Fox, Grant Hutchins & Trace Wax* -* `BestStandardsSupport` middleware now appends it's `X-UA-Compatible` value to app's - returned value if any. - Fix #8086. - - *Nikita Afanasenko* - * `date_select` helper accepts `with_css_classes: true` to add css classes similar with type of generated select tags. @@ -470,10 +461,6 @@ * Failsafe exception returns `text/plain`. *Steve Klabnik* -* Remove `rack-cache` dependency from Action Pack and declare it on Gemfile - - *Guillermo Iguaran* - * Rename internal variables on `ActionController::TemplateAssertions` to prevent naming collisions. `@partials`, `@templates` and `@layouts` are now prefixed with an underscore. Fix #7459. @@ -559,9 +546,7 @@ *Guillermo Iguaran* * `ActionDispatch::Session::MemCacheStore` now uses `dalli` instead of the deprecated - `memcache-client` gem. As side effect the autoloading of unloaded classes objects - saved as values in session isn't supported anymore when mem_cache session store is - used, this can have an impact in apps only when config.cache_classes is false. + `memcache-client` gem. *Arun Agrawal + Guillermo Iguaran* @@ -835,9 +820,9 @@ * `assert_generates`, `assert_recognizes`, and `assert_routing` all raise `Assertion` instead of `RoutingError` *David Chelimsky* -* URL path parameters with invalid encoding now raise ActionController::BadRequest. *Andrew White* +* URL path parameters with invalid encoding now raise `ActionController::BadRequest`. *Andrew White* -* Malformed query and request parameter hashes now raise ActionController::BadRequest. *Andrew White* +* Malformed query and request parameter hashes now raise `ActionController::BadRequest`. *Andrew White* * Add `divider` option to `grouped_options_for_select` to generate a separator `optgroup` automatically, and deprecate `prompt` as third argument, in favor @@ -864,7 +849,7 @@ *Andrew White* -* `respond_to` and `respond_with` now raise ActionController::UnknownFormat instead +* `respond_to` and `respond_with` now raise `ActionController::UnknownFormat` instead of directly returning head 406. The exception is rescued and converted to 406 in the exception handling middleware. *Steven Soroka* @@ -894,7 +879,7 @@ * Remove the leading \n added by textarea on `assert_select`. *Santiago Pastorino* * Changed default value for `config.action_view.embed_authenticity_token_in_remote_forms` - to `false`. This change breaks remote forms that need to work also without javascript, + to `false`. This change breaks remote forms that need to work also without JavaScript, so if you need such behavior, you can either set it to `true` or explicitly pass `authenticity_token: true` in form options. @@ -989,9 +974,6 @@ * `check_box` with `:form` html5 attribute will now replicate the `:form` attribute to the hidden field as well. *Carlos Antonio da Silva* -* Turn off verbose mode of rack-cache, we still have X-Rack-Cache to - check that info. Closes #5245. *Santiago Pastorino* - * `label` form helper accepts `for: nil` to not generate the attribute. *Carlos Antonio da Silva* * Add `:format` option to `number_to_percentage`. *Rodrigo Flores* diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 93568da9ef..834d44f045 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -420,7 +420,7 @@ module ActionController #:nodoc: end def response - @responses[format] || @responses[Mime::ALL] + @responses.fetch(format, @responses[Mime::ALL]) end def negotiate_format(request) diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 17379cf7ac..d275a854fd 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -50,6 +50,10 @@ module ActionController #:nodoc: config_accessor :request_forgery_protection_token self.request_forgery_protection_token ||= :authenticity_token + # Holds the class which implements the request forgery protection. + config_accessor :forgery_protection_strategy + self.forgery_protection_strategy = nil + # Controls whether request forgery protection is turned on or not. Turned off by default only in test mode. config_accessor :allow_forgery_protection self.allow_forgery_protection = true if allow_forgery_protection.nil? @@ -82,14 +86,14 @@ module ActionController #:nodoc: # * <tt>:reset_session</tt> - Resets the session. # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified. def protect_from_forgery(options = {}) - include protection_method_module(options[:with] || :null_session) + self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token prepend_before_action :verify_authenticity_token, options end private - def protection_method_module(name) + def protection_method_class(name) ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify) rescue NameError raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session' @@ -97,17 +101,22 @@ module ActionController #:nodoc: end module ProtectionMethods - module NullSession - protected + class NullSession + def initialize(controller) + @controller = controller + end # This is the method that defines the application behavior when a request is found to be unverified. def handle_unverified_request + request = @controller.request request.session = NullSessionHash.new(request.env) request.env['action_dispatch.request.flash_hash'] = nil request.env['rack.session.options'] = { skip: true } request.env['action_dispatch.cookies'] = NullCookieJar.build(request) end + protected + class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: def initialize(env) super(nil, env) @@ -135,16 +144,20 @@ module ActionController #:nodoc: end end - module ResetSession - protected + class ResetSession + def initialize(controller) + @controller = controller + end def handle_unverified_request - reset_session + @controller.reset_session end end - module Exception - protected + class Exception + def initialize(controller) + @controller = controller + end def handle_unverified_request raise ActionController::InvalidAuthenticityToken @@ -153,6 +166,10 @@ module ActionController #:nodoc: end protected + def handle_unverified_request + forgery_protection_strategy.new(self).handle_unverified_request + end + # The actual before_action that is used. Modify this to change how you handle unverified requests. def verify_authenticity_token unless verified_request? diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 7e720ca6f5..e4dcd3213f 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -191,9 +191,9 @@ module ActionController # # +:name+ passes it is a key of +params+ whose associated value is of type # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+, - # +Date+, +Time+, +DateTime+, +StringIO+, +IO+, or - # +ActionDispatch::Http::UploadedFile+. Otherwise, the key +:name+ is - # filtered out. + # +Date+, +Time+, +DateTime+, +StringIO+, +IO+, + # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+. + # Otherwise, the key +:name+ is filtered out. # # You may declare that the parameter should be an array of permitted scalars # by mapping it to an empty array: @@ -374,6 +374,7 @@ module ActionController StringIO, IO, ActionDispatch::Http::UploadedFile, + Rack::Test::UploadedFile, ] def permitted_scalar?(value) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 0a41ed0fcf..a8e225d61c 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -50,7 +50,6 @@ module ActionDispatch class Mapping #:nodoc: IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format] ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - SHORTHAND_REGEX = %r{/[\w/]+$} WILDCARD_PATH = %r{\*([^/\)]+)\)?$} attr_reader :scope, :path, :options, :requirements, :conditions, :defaults @@ -111,17 +110,7 @@ module ActionDispatch @options[:controller] ||= /.+?/ end - if using_match_shorthand?(path_without_format, @options) - to_shorthand = @options[:to].blank? - @options[:to] ||= path_without_format.gsub(/\(.*\)/, "")[1..-1].sub(%r{/([^/]*)$}, '#\1') - end - - @options.merge!(default_controller_and_action(to_shorthand)) - end - - # match "account/overview" - def using_match_shorthand?(path, options) - path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX + @options.merge!(default_controller_and_action) end def normalize_format! @@ -214,7 +203,7 @@ module ActionDispatch Constraints.new(endpoint, blocks, @set.request_class) end - def default_controller_and_action(to_shorthand=nil) + def default_controller_and_action if to.respond_to?(:call) { } else @@ -227,7 +216,7 @@ module ActionDispatch controller ||= default_controller action ||= default_action - unless controller.is_a?(Regexp) || to_shorthand + unless controller.is_a?(Regexp) controller = [@scope[:module], controller].compact.join("/").presence end @@ -1383,6 +1372,11 @@ module ActionDispatch paths = [path] + rest end + path_without_format = path.to_s.sub(/\(\.:format\)$/, '') + if using_match_shorthand?(path_without_format, options) + options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') + end + options[:anchor] = true unless options.key?(:anchor) if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) @@ -1393,6 +1387,10 @@ module ActionDispatch self end + def using_match_shorthand?(path, options) + path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$} + end + def decomposed_match(path, options) # :nodoc: if on = options.delete(:on) send(on) { decomposed_match(path, options) } diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index a989966613..d3953c26b7 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -642,6 +642,8 @@ module ActionView # <time datetime="2010-11-03">Yesterday</time> # time_tag Date.today, pubdate: true # => # <time datetime="2010-11-04" pubdate="pubdate">November 04, 2010</time> + # time_tag Date.today, datetime: Date.today.strftime('%G-W%V') # => + # <time datetime="2010-W44">November 04, 2010</time> # # <%= time_tag Time.now do %> # <span>Right now</span> @@ -651,7 +653,7 @@ module ActionView options = args.extract_options! format = options.delete(:format) || :long content = args.first || I18n.l(date_or_time, :format => format) - datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.rfc3339 + datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601 content_tag(:time, content, options.reverse_merge(:datetime => datetime), &block) end diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb index 34fc23ac1a..c29c1b1eea 100644 --- a/actionpack/lib/action_view/helpers/debug_helper.rb +++ b/actionpack/lib/action_view/helpers/debug_helper.rb @@ -32,7 +32,7 @@ module ActionView content_tag(:pre, object, :class => "debug_dump") rescue Exception # errors from Marshal or YAML # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback - content_tag(:code, object.to_yaml, :class => "debug_dump") + content_tag(:code, object.inspect, :class => "debug_dump") end end end diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index 49473dd129..377819a80c 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -560,19 +560,19 @@ module ActionView def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone) zone_options = "".html_safe - zones = model.all.dup + zones = model.all convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } } if priority_zones if priority_zones.is_a?(Regexp) - priority_zones = zones.select { |z| z =~ priority_zones } + priority_zones = zones.grep(priority_zones) end zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected) zone_options.safe_concat content_tag(:option, '-------------', :value => '', :disabled => 'disabled') zone_options.safe_concat "\n" - zones.reject! { |z| priority_zones.include?(z) } + zones = zones - priority_zones end zone_options.safe_concat options_for_select(convert_zones[zones], selected) diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 86d9f94067..1adc8225f1 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -33,7 +33,7 @@ module ActionView # (by passing <tt>false</tt>). Remote forms may omit the embedded authenticity token # by setting <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>. # This is helpful when you're fragment-caching the form. Remote forms get the - # authenticity from the <tt>meta</tt> tag, so embedding is unnecessary unless you + # authenticity token from the <tt>meta</tt> tag, so embedding is unnecessary unless you # support browsers without JavaScript. # * A list of parameters to feed to the URL the form will be posted to. # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index ed013e2185..a9c62899b5 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -80,6 +80,13 @@ class RespondToController < ActionController::Base respond_to(:html, :xml) end + def using_defaults_with_all + respond_to do |type| + type.html + type.all{ render text: "ALL" } + end + end + def made_for_content_type respond_to do |type| type.rss { render :text => "RSS" } @@ -301,6 +308,20 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "<p>Hello world!</p>\n", @response.body end + def test_using_defaults_with_all + @request.accept = "*/*" + get :using_defaults_with_all + assert_equal "HTML!", @response.body.strip + + @request.accept = "text/html" + get :using_defaults_with_all + assert_equal "HTML!", @response.body.strip + + @request.accept = "application/json" + get :using_defaults_with_all + assert_equal "ALL", @response.body + end + def test_using_defaults_with_type_list @request.accept = "*/*" get :using_defaults_with_type_list diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index aadb142660..437da43d9b 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -32,7 +32,8 @@ class ParametersPermitTest < ActiveSupport::TestCase values += [0, 1.0, 2**128, BigDecimal.new(1)] values += [true, false] values += [Date.today, Time.now, DateTime.now] - values += [STDOUT, StringIO.new, ActionDispatch::Http::UploadedFile.new(tempfile: __FILE__)] + values += [STDOUT, StringIO.new, ActionDispatch::Http::UploadedFile.new(tempfile: __FILE__), + Rack::Test::UploadedFile.new(__FILE__)] values.each do |value| params = ActionController::Parameters.new(id: value) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 143733254b..37ad9ddb6b 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1146,6 +1146,33 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'api/products#list', @response.body end + def test_match_shorthand_inside_scope_with_variables_with_controller + draw do + scope ':locale' do + match 'questions/new', via: [:get] + end + end + + get '/de/questions/new' + assert_equal 'questions#new', @response.body + assert_equal 'de', @request.params[:locale] + end + + def test_match_shorthand_inside_nested_namespaces_and_scopes_with_controller + draw do + namespace :api do + namespace :v3 do + scope ':locale' do + get "products/list" + end + end + end + end + + get '/api/v3/en/products/list' + assert_equal 'api/v3/products#list', @response.body + end + def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper draw do resources :replies do diff --git a/actionpack/test/fixtures/respond_to/using_defaults_with_all.html.erb b/actionpack/test/fixtures/respond_to/using_defaults_with_all.html.erb new file mode 100644 index 0000000000..9f1f855269 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/using_defaults_with_all.html.erb @@ -0,0 +1 @@ +HTML! diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index f11adefad8..242b56a1fd 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -1510,42 +1510,42 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on") end - + def test_date_select_with_selected @post = Post.new @post.written_on = Date.new(2004, 6, 15) - + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} expected << "</select>\n" - + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7" selected="selected">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} expected << "</select>\n" - + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10" selected="selected">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} - + expected << "</select>\n" - + assert_dom_equal expected, date_select("post", "written_on", :selected => Date.new(2004, 07, 10)) end def test_date_select_with_selected_nil @post = Post.new @post.written_on = Date.new(2004, 6, 15) - + expected = '<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="1"/>' + "\n" - + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} expected << %{<option value=""></option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} expected << "</select>\n" - + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} expected << %{<option value=""></option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} - + expected << "</select>\n" - + assert_dom_equal expected, date_select("post", "written_on", include_blank: true, discard_year: true, selected: nil) end @@ -2296,7 +2296,7 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, datetime_select("post", "updated_at", discard_year: true, selected: nil) end - + def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set # The love zone is UTC+0 mytz = Class.new(ActiveSupport::TimeZone) { @@ -3160,14 +3160,14 @@ class DateHelperTest < ActionView::TestCase end def test_time_tag_with_date - date = Date.today - expected = "<time datetime=\"#{date.rfc3339}\">#{I18n.l(date, :format => :long)}</time>" + date = Date.new(2013, 2, 20) + expected = '<time datetime="2013-02-20">February 20, 2013</time>' assert_equal expected, time_tag(date) end def test_time_tag_with_time - time = Time.now - expected = "<time datetime=\"#{time.xmlschema}\">#{I18n.l(time, :format => :long)}</time>" + time = Time.new(2013, 2, 20, 0, 0, 0, '+00:00') + expected = '<time datetime="2013-02-20T00:00:00+00:00">February 20, 2013 00:00</time>' assert_equal expected, time_tag(time) end @@ -3184,8 +3184,8 @@ class DateHelperTest < ActionView::TestCase end def test_time_tag_with_different_format - time = Time.now - expected = "<time datetime=\"#{time.xmlschema}\">#{I18n.l(time, :format => :short)}</time>" + time = Time.new(2013, 2, 20, 0, 0, 0, '+00:00') + expected = '<time datetime="2013-02-20T00:00:00+00:00">20 Feb 00:00</time>' assert_equal expected, time_tag(time, :format => :short) end diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index 757b05dbc1..04cdd068c8 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -21,10 +21,10 @@ class FormOptionsHelperTest < ActionView::TestCase end def setup - @fake_timezones = %w(A B C D E).inject([]) do |zones, id| + @fake_timezones = %w(A B C D E).map do |id| tz = TZInfo::Timezone.loaded_zones[id] = stub(:name => id, :to_s => id) ActiveSupport::TimeZone.stubs(:[]).with(id).returns(tz) - zones << tz + tz end ActiveSupport::TimeZone.stubs(:all).returns(@fake_timezones) end @@ -351,7 +351,7 @@ class FormOptionsHelperTest < ActionView::TestCase ) end - def test_time_zone_options_no_parms + def test_time_zone_options_no_params opts = time_zone_options_for_select assert_dom_equal "<option value=\"A\">A</option>\n" + "<option value=\"B\">B</option>\n" + @@ -416,11 +416,11 @@ class FormOptionsHelperTest < ActionView::TestCase "<option value=\"D\">D</option>", opts end - + def test_time_zone_options_with_priority_zones_does_not_mutate_time_zones original_zones = ActiveSupport::TimeZone.all.dup zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ] - opts = time_zone_options_for_select( nil, zones ) + time_zone_options_for_select(nil, zones) assert_equal original_zones, ActiveSupport::TimeZone.all end @@ -1086,11 +1086,13 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select_with_priority_zones_as_regexp @firm = Firm.new("D") + + priority_zones = /A|D/ @fake_timezones.each_with_index do |tz, i| - tz.stubs(:=~).returns(i.zero? || i == 3) + priority_zones.stubs(:===).with(tz).returns(i.zero? || i == 3) end - html = time_zone_select("firm", "time_zone", /A|D/) + html = time_zone_select("firm", "time_zone", priority_zones) assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + "<option value=\"A\">A</option>\n" + "<option value=\"D\" selected=\"selected\">D</option>" + diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 09e6ede064..416922cb89 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -19,7 +19,7 @@ *Roberto Vasquez Angel* -* `[attribute]_changed?` now returns `false` after a call to `reset_[attribute]!` +* `[attribute]_changed?` now returns `false` after a call to `reset_[attribute]!`. *Renato Mascarenhas* @@ -27,9 +27,11 @@ *Rafael Mendonça França* -* Specify type of singular association during serialization *Steve Klabnik* +* Specify type of singular association during serialization. -* Fixed length validator to correctly handle nil values. Fixes #7180. + *Steve Klabnik* + +* Fixed length validator to correctly handle `nil`. Fixes #7180. *Michal Zima* @@ -53,24 +55,27 @@ *Guillermo Iguaran* -* Due to a change in builder, nil values and empty strings now generates +* Due to a change in builder, `nil` and empty strings now generate closed tags, so instead of this: <pseudonyms nil=\"true\"></pseudonyms> - It generates this: + it generates this: <pseudonyms nil=\"true\"/> *Carlos Antonio da Silva* -* Changed inclusion and exclusion validators to accept a symbol for `:in` option. +* Inclusion/exclusion validators accept a method name passed as a symbol to the + `:in` option. - This allows to use dynamic inclusion/exclusion values using methods, besides the current lambda/proc support. + This allows to use dynamic inclusion/exclusion values using methods, besides + the current lambda/proc support. *Gabriel Sobrinho* -* `AM::Validation#validates` ability to pass custom exception to `:strict` option. +* `ActiveModel::Validation#validates` ability to pass custom exception to the + `:strict` option. *Bogdan Gusiev* @@ -81,7 +86,7 @@ *Anthony Alberto* -* Changed `AM::Serializers::JSON.include_root_in_json' default value to false. +* Changed `ActiveModel::Serializers::JSON.include_root_in_json` default value to false. Now, AM Serializers and AR objects have the same default behaviour. Fixes #6578. class User < ActiveRecord::Base; end @@ -108,16 +113,31 @@ *Francesco Rodriguez* -* Passing false hash values to `validates` will no longer enable the corresponding validators *Steve Purcell* +* Passing false hash values to `validates` will no longer enable the corresponding validators. + + *Steve Purcell* + +* `ConfirmationValidator` error messages will attach to `:#{attribute}_confirmation` instead of `attribute`. + + *Brian Cardarella* + +* Added `ActiveModel::Model`, a mixin to make Ruby objects work with AP out of box. -* `ConfirmationValidator` error messages will attach to `:#{attribute}_confirmation` instead of `attribute` *Brian Cardarella* + *Guillermo Iguaran* + +* `AM::Errors#to_json`: support `:full_messages` parameter. + + *Bogdan Gusiev* -* Added ActiveModel::Model, a mixin to make Ruby objects work with AP out of box *Guillermo Iguaran* +* Trim down Active Model API by removing `valid?` and `errors.full_messages`. -* `AM::Errors#to_json`: support `:full_messages` parameter *Bogdan Gusiev* + *José Valim* -* Trim down Active Model API by removing `valid?` and `errors.full_messages` *José Valim* +* When `^` or `$` are used in the regular expression provided to `validates_format_of` + and the `:multiline` option is not set to true, an exception will be raised. This is + to prevent security vulnerabilities when using `validates_format_of`. The problem is + described in detail in the Rails security guide. -* When `^` or `$` are used in the regular expression provided to `validates_format_of` and the :multiline option is not set to true, an exception will be raised. This is to prevent security vulnerabilities when using `validates_format_of`. The problem is described in detail in the Rails security guide *Jan Berdajs + Egor Homakov* + *Jan Berdajs + Egor Homakov* Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activemodel/CHANGELOG.md) for previous changes. diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a888b79391..ecdf829823 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,18 +1,89 @@ ## Rails 4.0.0 (unreleased) ## +* Fixing issue #8345. Now throwing an error when one attempts to touch a + new object that has not yet been persisted. For instance: + + Example: + + ball = Ball.new + ball.touch :updated_at # => raises error + + It is not until the ball object has been persisted that it can be touched. + This follows the behavior of update_column. + + *John Wang* + +* Preloading ordered `has_many :through` associations no longer applies + invalid ordering to the `:through` association. + Fixes #8663. + + *Yves Senn* + +* The auto explain feature has been removed. This feature was + activated by configuring `config.active_record.auto_explain_threshold_in_seconds`. + The configuration option was deprecated and has no more effect. + + You can still use `ActiveRecord::Relation#explain` to see the EXPLAIN output for + any given relation. + + *Yves Senn* + +* The `:on` option for `after_commit` and `after_rollback` now + accepts an Array of actions. + Fixes #988. + + Example: + + after_commit :update_cache on: [:create, :update] + + *Yves Senn* + +* Rename related indexes on `rename_table` and `rename_column`. This + does not affect indexes with custom names. + + *Yves Senn* + +* Prevent the creation of indices with too long names, which cause + internal operations to fail (sqlite3 adapter only). The method + `allowed_index_name_length` defines the length limit enforced by + rails. It's value defaults to `index_name_length` but can vary per adapter. + Fixes #8264. + + *Yves Senn* + +* Fixing issue #776. + + Memory bloat in transactions is handled by having the transaction hold only + the AR objects which it absolutely needs to know about. These are the AR + objects with callbacks (they need to be updated as soon as something in the + transaction occurs). + + All other AR objects can be updated lazily by keeping a reference to a + TransactionState object. If an AR object gets inside a transaction, then + the transaction will add its TransactionState to the AR object. When the + user makes a call to some attribute on an AR object (which has no + callbacks) associated with a transaction, the AR object will call the + sync_with_transaction_state method and make sure it is up to date with the + transaction. After it has synced with the transaction state, the AR object + will return the attribute that was requested. + + Most of the logic in the changes are used to handle multiple transactions, + in which case the AR object has to recursively follow parent pointers of + TransactionState objects. + + *John Wang* + * Descriptive error message when the necessary AR adapter gem was not found. - Fix #7313 + Fixes #7313. *Yves Senn* -* ActiveRecord now raises an error when blank arguments are passed to query - methods for which blank arguments do not make sense. This also occurs for - nil-like objects in arguments. +* Active Record now raises an error when blank arguments are passed to query + methods for which blank arguments do not make sense. Example: - Post.limit() # => raises error - Post.include([]) # => raises error + Post.includes() # => raises error *John Wang* @@ -110,8 +181,8 @@ *Justin George* -* The `DATABASE_URL` environment variable now converts ints, floats, and - the strings true and false to Ruby types. For example, SQLite requires +* The database adpters now converts the options passed thought `DATABASE_URL` + environment variable to the proper Ruby types before using. For example, SQLite requires that the timeout value is an integer, and PostgreSQL requires that the prepared_statements option is a boolean. These now work as expected: @@ -120,7 +191,7 @@ DATABASE_URL=sqlite3://localhost/test_db?timeout=500 DATABASE_URL=postgresql://localhost/test_db?prepared_statements=false - *Aaron Stone* + *Aaron Stone + Rafael Mendonça França* * `Relation#merge` now only overwrites where values on the LHS of the merge. Consider: @@ -177,7 +248,7 @@ *Lilibeth De La Cruz* * When `#count` is used in conjunction with `#uniq` we perform `count(:distinct => true)`. - Fix #6865. + Fixes #6865. Example: @@ -210,7 +281,7 @@ *John Wang* * Collection associations `#empty?` always respects builded records. - Fix #8879. + Fixes #8879. Example: @@ -299,18 +370,13 @@ *Yves Senn* * Add `ActiveRecord::Base.cache_timestamp_format` class attribute to control - the format of the timestamp value in the cache key. - This allows users to improve the precision of the cache key. + the format of the timestamp value in the cache key. Defaults to `:nsec`. Fixes #8195. *Rafael Mendonça França* -* Add `:nsec` date format. This can be used to improve the precision of cache key. - - *Jamie Gaskins* - * Session variables can be set for the `mysql`, `mysql2`, and `postgresql` adapters - in the `variables: <hash>` parameter in `database.yml`. The key-value pairs of this + in the `variables: <hash>` parameter in `config/database.yml`. The key-value pairs of this hash will be sent in a `SET key = value` query on new database connections. See also: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html http://www.postgresql.org/docs/8.3/static/sql-set.html @@ -333,7 +399,7 @@ to the update query. class User < ActiveRecord::Base - default_scope where(active: true) + default_scope -> { where(active: true) } end user = User.first @@ -470,11 +536,6 @@ *kennyj* -* Added `#none!` method for mutating `ActiveRecord::Relation` objects to a NullRelation. - It acts like `#none` but modifies relation in place. - - *Juanjo Bazán* - * Fix bug where `update_columns` and `update_column` would not let you update the primary key column. *Henrik Nyh* @@ -1252,13 +1313,6 @@ *Joshua Wood* -* Added bang methods for mutating `ActiveRecord::Relation` objects. - For example, while `foo.where(:bar)` will return a new object - leaving `foo` unchanged, `foo.where!(:bar)` will mutate the foo - object - - *Jon Leighton* - * Added `#find_by` and `#find_by!` to mirror the functionality provided by dynamic finders in a way that allows dynamic input more easily: diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 06bdabfced..513d1012ba 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -965,8 +965,8 @@ module ActiveRecord # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they # cause the records in the join table to be removed. # - # For +has_many+, <tt>destroy</tt> will always call the <tt>destroy</tt> method of the - # record(s) being removed so that callbacks are run. However <tt>delete</tt> will either + # For +has_many+, <tt>destroy</tt> and <tt>destory_all</tt> will always call the <tt>destroy</tt> method of the + # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index b0b1c13b0d..c4b50ab306 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -47,12 +47,12 @@ module ActiveRecord through_scope.where! reflection.foreign_type => options[:source_type] else unless reflection_scope.where_values.empty? - through_scope.includes_values = reflection_scope.values[:includes] || options[:source] + through_scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) through_scope.where_values = reflection_scope.values[:where] end - through_scope.order! reflection_scope.values[:order] through_scope.references! reflection_scope.values[:references] + through_scope.order! reflection_scope.values[:order] if through_scope.eager_loading? end through_scope diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 310f1b6e75..3e454b713a 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -8,27 +8,32 @@ module ActiveRecord # Returns this record's primary key value wrapped in an Array if one is # available. def to_key + sync_with_transaction_state key = self.id [key] if key end # Returns the primary key value. def id + sync_with_transaction_state read_attribute(self.class.primary_key) end # Sets the primary key value. def id=(value) + sync_with_transaction_state write_attribute(self.class.primary_key, value) if self.class.primary_key end # Queries the primary key value. def id? + sync_with_transaction_state query_attribute(self.class.primary_key) end # Returns the primary key value before type cast. def id_before_type_cast + sync_with_transaction_state read_attribute_before_type_cast(self.class.primary_key) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 30ccb8f0a4..2859fb31e8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -17,6 +17,15 @@ module ActiveRecord 64 end + # Returns the maximum allowed length for an index name. This + # limit is enforced by rails and Is less than or equal to + # <tt>index_name_length</tt>. The gap between + # <tt>index_name_length</tt> is to allow internal rails + # opreations to use prefixes in temporary opreations. + def allowed_index_name_length + index_name_length + end + # Returns the maximum length of an index name. def index_name_length 64 diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index cdc8433185..9bae880024 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -192,6 +192,14 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # + # Note that +create_join_table+ does not create any indices by default; you can use + # its block form to do so yourself: + # + # create_join_table :products, :categories do |t| + # t.index :products + # t.index :categories + # end + # # ====== Add a backend specific option to the generated SQL (MySQL) # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -647,10 +655,11 @@ module ActiveRecord index_name = index_name(table_name, column: column_names) if Hash === options # legacy support, since this param was a string - options.assert_valid_keys(:unique, :order, :name, :where, :length) + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal) index_type = options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) + max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length if supports_partial_index? index_options = options[:where] ? " WHERE #{options[:where]}" : "" @@ -665,10 +674,11 @@ module ActiveRecord end index_type = options + max_index_length = allowed_index_name_length end - if index_name.length > index_name_length - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" + if index_name.length > max_index_length + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" end if index_name_exists?(table_name, index_name, false) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" @@ -694,6 +704,28 @@ module ActiveRecord column_names.map {|column_name| quote_column_name(column_name) } end + def rename_table_indexes(table_name, new_name) + indexes(new_name).each do |index| + generated_index_name = index_name(table_name, column: index.columns) + if generated_index_name == index.name + rename_index new_name, generated_index_name, index_name(new_name, column: index.columns) + end + end + end + + def rename_column_indexes(table_name, column_name, new_column_name) + column_name, new_column_name = column_name.to_s, new_column_name.to_s + indexes(table_name).each do |index| + next unless index.columns.include?(new_column_name) + old_columns = index.columns.dup + old_columns[old_columns.index(new_column_name)] = column_name + generated_index_name = index_name(table_name, column: old_columns) + if generated_index_name == index.name + rename_index table_name, generated_index_name, index_name(table_name, column: index.columns) + end + end + end + private def table_definition TableDefinition.new(self) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 3ecef96b10..73c80a3220 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -5,7 +5,7 @@ module ActiveRecord def initialize(connection) @connection = connection - @state = TransactionState.new + @state = TransactionState.new end def state @@ -14,11 +14,13 @@ module ActiveRecord end class TransactionState + attr_accessor :parent VALID_STATES = Set.new([:committed, :rolledback, nil]) def initialize(state = nil) @state = state + @parent = nil end def committed? @@ -116,7 +118,11 @@ module ActiveRecord end def add_record(record) - records << record + if record.has_transactional_callbacks? + records << record + else + record.set_transaction_state(@state) + end end def rollback_records @@ -188,8 +194,9 @@ module ActiveRecord end def perform_commit + @state.set_state(:committed) + @state.parent = parent.state connection.release_savepoint - records.each { |r| parent.add_record(r) } end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 67d4d505f7..ff9de712bc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -61,12 +61,30 @@ module ActiveRecord include MonitorMixin include ColumnDumper + SIMPLE_INT = /\A\d+\z/ + define_callbacks :checkout, :checkin attr_accessor :visitor, :pool attr_reader :schema_cache, :last_use, :in_use, :logger alias :in_use? :in_use + def self.type_cast_config_to_integer(config) + if config =~ SIMPLE_INT + config.to_i + else + config + end + end + + def self.type_cast_config_to_boolean(config) + if config == "false" + false + else + config + end + end + def initialize(connection, logger = nil, pool = nil) #:nodoc: super() diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index e07dbc7da9..5480204511 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -31,7 +31,7 @@ module ActiveRecord return false if blob_or_text_column? #mysql forbids defaults on blob and text columns super end - + def blob_or_text_column? sql_type =~ /blob/i || type == :text end @@ -140,7 +140,7 @@ module ActiveRecord @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} - if config.fetch(:prepared_statements) { true } + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::MySQL.new self else @visitor = BindSubstitution.new self @@ -468,6 +468,7 @@ module ActiveRecord # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" + rename_table_indexes(table_name, new_name) end def add_column(table_name, column_name, type, options = {}) @@ -495,6 +496,7 @@ module ActiveRecord def rename_column(table_name, column_name, new_column_name) #:nodoc: execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") + rename_column_indexes(table_name, column_name, new_column_name) end # Maps logical Rails types to MySQL-specific data types. @@ -579,7 +581,7 @@ module ActiveRecord end def strict_mode? - @config.fetch(:strict, true) + self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end protected @@ -720,7 +722,7 @@ module ActiveRecord # Increase timeout so the server doesn't disconnect us. wait_timeout = @config[:wait_timeout] wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) - variables[:wait_timeout] = wait_timeout + variables[:wait_timeout] = self.class.type_cast_config_to_integer(wait_timeout) # Make MySQL reject illegal values rather than truncating or blanking them, see # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 5b60185f53..2c683fc3ac 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -65,10 +65,6 @@ module ActiveRecord ConnectionSpecification.new(spec, adapter_method) end - # For DATABASE_URL, accept a limited concept of ints and floats - SIMPLE_INT = /\A\d+\z/ - SIMPLE_FLOAT = /\A\d+\.\d+\z/ - def self.connection_url_to_hash(url) # :nodoc: config = URI.parse url adapter = config.scheme @@ -89,28 +85,11 @@ module ActiveRecord if config.query options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys - options.each { |key, value| options[key] = type_cast_value(value) } - spec.merge!(options) end spec end - - def type_cast_value(value) - case value - when SIMPLE_INT - value.to_i - when SIMPLE_FLOAT - value.to_f - when 'true' - true - when 'false' - false - else - value - end - end end 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 631f646f58..7544c2a783 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -126,7 +126,7 @@ module ActiveRecord def initialize(connection, logger, connection_options, config) super @statements = StatementPool.new(@connection, - config.fetch(:statement_limit) { 1000 }) + self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @client_encoding = nil connect end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 73ca2c8e61..3bc61c5e0c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -321,14 +321,16 @@ module ActiveRecord # # Example: # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) + def rename_table(table_name, new_name) clear_cache! - execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq == "#{name}_#{pk}_seq" + if seq == "#{table_name}_#{pk}_seq" new_seq = "#{new_name}_#{pk}_seq" execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" end + + rename_table_indexes(table_name, new_name) end # Adds a new column to the named table. @@ -370,6 +372,7 @@ module ActiveRecord def rename_column(table_name, column_name, new_column_name) clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" + rename_column_indexes(table_name, column_name, new_column_name) end def remove_index!(table_name, index_name) #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 0818760b11..2bb2557efd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -478,7 +478,7 @@ module ActiveRecord def initialize(connection, logger, connection_parameters, config) super(connection, logger) - if config.fetch(:prepared_statements) { true } + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::PostgreSQL.new self else @visitor = BindSubstitution.new self @@ -492,7 +492,7 @@ module ActiveRecord connect @statements = StatementPool.new @connection, - config.fetch(:statement_limit) { 1000 } + self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) if postgresql_version < 80200 raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" @@ -500,7 +500,7 @@ module ActiveRecord initialize_type_map @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] - @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true + @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true end # Clears the prepared statements cache. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 91444950be..981c4c96a0 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -26,7 +26,7 @@ module ActiveRecord :results_as_hash => true ) - db.busy_timeout(config[:timeout]) if config[:timeout] + db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] ConnectionAdapters::SQLite3Adapter.new(db, logger, config) end @@ -107,10 +107,10 @@ module ActiveRecord @active = nil @statements = StatementPool.new(@connection, - config.fetch(:statement_limit) { 1000 }) + self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @config = config - if config.fetch(:prepared_statements) { true } + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::SQLite.new self else @visitor = BindSubstitution.new self @@ -187,6 +187,13 @@ module ActiveRecord true end + # Returns 62. SQLite supports index names up to 64 + # characters. The rest is used by rails internally to perform + # temporary rename operations + def allowed_index_name_length + index_name_length - 2 + end + def native_database_types #:nodoc: { :primary_key => default_primary_key_type, @@ -428,8 +435,9 @@ module ActiveRecord # # Example: # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) - exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + def rename_table(table_name, new_name) + exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" + rename_table_indexes(table_name, new_name) end # See: http://www.sqlite.org/lang_altertable.html @@ -488,6 +496,7 @@ module ActiveRecord raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" end alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) + rename_column_indexes(table_name, column_name, new_column_name) end protected @@ -502,7 +511,7 @@ module ActiveRecord end def alter_table(table_name, options = {}) #:nodoc: - altered_table_name = "altered_#{table_name}" + altered_table_name = "a#{table_name}" caller = lambda {|definition| yield definition if block_given?} transaction do @@ -546,10 +555,10 @@ module ActiveRecord def copy_table_indexes(from, to, rename = {}) #:nodoc: indexes(from).each do |index| name = index.name - if to == "altered_#{from}" - name = "temp_#{name}" - elsif from == "altered_#{to}" - name = name[5..-1] + if to == "a#{from}" + name = "t#{name}" + elsif from == "a#{to}" + name = name[1..-1] end to_column_names = columns(to).map { |c| c.name } @@ -559,7 +568,7 @@ module ActiveRecord unless columns.empty? # index name can't be the same - opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_") } + opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true } opts[:unique] = true if index.unique add_index(to, columns, opts) end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 63a1197a56..6510433056 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -347,8 +347,54 @@ module ActiveRecord Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access end + def set_transaction_state(state) # :nodoc: + @transaction_state = state + end + + def has_transactional_callbacks? # :nodoc: + !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty? + end + private + # Updates the attributes on this particular ActiveRecord object so that + # if it is associated with a transaction, then the state of the AR object + # will be updated to reflect the current state of the transaction + # + # The @transaction_state variable stores the states of the associated + # transaction. This relies on the fact that a transaction can only be in + # one rollback or commit (otherwise a list of states would be required) + # Each AR object inside of a transaction carries that transaction's + # TransactionState. + # + # This method checks to see if the ActiveRecord object's state reflects + # the TransactionState, and rolls back or commits the ActiveRecord object + # as appropriate. + # + # Since ActiveRecord objects can be inside multiple transactions, this + # method recursively goes through the parent of the TransactionState and + # checks if the ActiveRecord object reflects the state of the object. + def sync_with_transaction_state + update_attributes_from_transaction_state(@transaction_state, 0) + end + + def update_attributes_from_transaction_state(transaction_state, depth) + if transaction_state && !has_transactional_callbacks? + unless @reflects_state[depth] + if transaction_state.committed? + committed! + elsif transaction_state.rolledback? + rolledback! + end + @reflects_state[depth] = true + end + + if transaction_state.parent && !@reflects_state[depth+1] + update_attributes_from_transaction_state(transaction_state.parent, depth+1) + end + end + end + # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements # of the array, and then rescues from the possible NoMethodError. If those elements are # ActiveRecord::Base's, then this triggers the various method_missing's that we have, @@ -376,7 +422,8 @@ module ActiveRecord @new_record = true @txn = nil @_start_transaction_state = {} - @transaction = nil + @transaction_state = nil + @reflects_state = [false] end end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 70683eb731..b2a9a54af1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -2,43 +2,7 @@ require 'active_support/lazy_load_hooks' module ActiveRecord module Explain - def self.extended(base) - base.mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false - end - - # If the database adapter supports explain and auto explain is enabled, - # this method triggers EXPLAIN logging for the queries triggered by the - # block if it takes more than the threshold as a whole. That is, the - # threshold is not checked against each individual query, but against the - # duration of the entire block. This approach is convenient for relations. - - # - # The available_queries_for_explain thread variable collects the queries - # to be explained. If the value is nil, it means queries are not being - # currently collected. A false value indicates collecting is turned - # off. Otherwise it is an array of queries. - def logging_query_plan # :nodoc: - return yield unless logger - - threshold = auto_explain_threshold_in_seconds - current = Thread.current - if connection.supports_explain? && threshold && current[:available_queries_for_explain].nil? - begin - queries = current[:available_queries_for_explain] = [] - start = Time.now - result = yield - logger.warn(exec_explain(queries)) if Time.now - start > threshold - result - ensure - current[:available_queries_for_explain] = nil - end - else - yield - end - end - - # Relation#explain needs to be able to collect the queries regardless of - # whether auto explain is enabled. This method serves that purpose. + # Relation#explain needs to be able to collect the queries. def collecting_queries_for_explain # :nodoc: current = Thread.current original, current[:available_queries_for_explain] = current[:available_queries_for_explain], [] @@ -68,20 +32,5 @@ module ActiveRecord end str end - - # Silences automatic EXPLAIN logging for the duration of the block. - # - # This has high priority, no EXPLAINs will be run even if downwards - # the threshold is set to 0. - # - # As the name of the method suggests this only applies to automatic - # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run. - def silence_auto_explain - current = Thread.current - original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false - yield - ensure - current[:available_queries_for_explain] = original - end end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 7f877a6471..32d35f0ec1 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -5,8 +5,10 @@ module ActiveRecord included do ## # :singleton-method: - # Indicates the format used to generate the timestamp format in the cache key. - # This is +:number+, by default. + # Indicates the format used to generate the timestamp in the cache key. + # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. + # + # This is +:nsec+, by default. class_attribute :cache_timestamp_format, :instance_writer => false self.cache_timestamp_format = :nsec end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 803cae7115..347f023793 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -69,11 +69,13 @@ module ActiveRecord # Returns true if this object hasn't been saved yet -- that is, a record # for the object doesn't exist in the data store yet; otherwise, returns false. def new_record? + sync_with_transaction_state @new_record end # Returns true if this object has been destroyed, otherwise returns false. def destroyed? + sync_with_transaction_state @destroyed end @@ -365,7 +367,16 @@ module ActiveRecord # # # triggers @brake.car.touch and @brake.car.corporation.touch # @brake.touch + # + # Note that +touch+ must be used on a persisted object, or else an + # ActiveRecordError will be thrown. For example: + # + # ball = Ball.new + # ball.touch(:updated_at) # => raises ActiveRecordError + # def touch(name = nil) + raise ActiveRecordError, "can not touch on a new record object" unless persisted? + attributes = timestamp_attributes_for_update_in_model attributes << name if name @@ -418,13 +429,22 @@ module ActiveRecord # Returns the number of affected rows. def update_record(attribute_names = @attributes.keys) attributes_with_values = arel_attributes_with_values_for_update(attribute_names) - if attributes_with_values.empty? 0 else klass = self.class - stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values) - klass.connection.update stmt + column_hash = klass.connection.schema_cache.columns_hash klass.table_name + db_columns_with_values = attributes_with_values.map { |attr,value| + real_column = column_hash[attr.name] + [real_column, value] + } + bind_attrs = attributes_with_values.dup + bind_attrs.keys.each_with_index do |column, i| + real_column = db_columns_with_values[i].first + bind_attrs[column] = klass.connection.substitute_at(real_column, i) + end + stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(bind_attrs) + klass.connection.update stmt, 'SQL', db_columns_with_values end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index f08b9c614d..e04a3d0976 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -33,18 +33,16 @@ module ActiveRecord # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # # => [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...] def find_by_sql(sql, binds = []) - logging_query_plan do - result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) - column_types = {} + result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) + column_types = {} - if result_set.respond_to? :column_types - column_types = result_set.column_types - else - ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" - end - - result_set.map { |record| instantiate(record, column_types) } + if result_set.respond_to? :column_types + column_types = result_set.column_types + else + ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" end + + result_set.map { |record| instantiate(record, column_types) } end # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. @@ -57,10 +55,8 @@ module ActiveRecord # # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" def count_by_sql(sql) - logging_query_plan do - sql = sanitize_conditions(sql) - connection.select_value(sql, "#{name} Count").to_i - end + sql = sanitize_conditions(sql) + connection.select_value(sql, "#{name} Count").to_i end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index a979be6999..64eac3aca7 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -112,7 +112,18 @@ module ActiveRecord `config/application.rb` file and any `mass_assignment_sanitizer` options from your `config/environments/*.rb` files. - See http://guides.rubyonrails.org/security.html#mass-assignment for more information + See http://guides.rubyonrails.org/security.html#mass-assignment for more information. + EOF + end + + unless app.config.active_record.delete(:auto_explain_threshold_in_seconds).nil? + ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, [] + The Active Record auto explain feature has been removed. + + To disable this message remove the `active_record.auto_explain_threshold_in_seconds` + option from the `config/environments/*.rb` config file. + + See http://guides.rubyonrails.org/4_0_release_notes.html for more information. EOF end @@ -124,7 +135,7 @@ module ActiveRecord To disable this message remove the `observers` option from your `config/application.rb` or from your initializers. - See http://guides.rubyonrails.org/4_0_release_notes.html for more information + See http://guides.rubyonrails.org/4_0_release_notes.html for more information. EOF end ensure @@ -146,13 +157,6 @@ module ActiveRecord end end - initializer "active_record.validate_explain_support" do |app| - if app.config.active_record[:auto_explain_threshold_in_seconds] && - !ActiveRecord::Base.connection.supports_explain? - warn "auto_explain_threshold_in_seconds is set but will be ignored because your adapter does not support this feature. Please unset the configuration to avoid this warning." - end - end - # Expose database runtime to controller for logging. initializer "active_record.log_runtime" do |app| require "active_record/railties/controller_runtime" diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 0053530f73..bc50802c4a 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -444,17 +444,7 @@ module ActiveRecord # # Post.where(published: true).load # => #<ActiveRecord::Relation> def load - unless loaded? - # We monitor here the entire execution rather than individual SELECTs - # because from the point of view of the user fetching the records of a - # relation is a single unit of work. You want to know if this call takes - # too long, not if the individual queries take too long. - # - # It could be the case that none of the queries involved surpass the - # threshold, and at the same time the sum of them all does. The user - # should get a query plan logged in that case. - logging_query_plan { exec_queries } - end + exec_queries unless loaded? self end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 615309964c..00a506c3a7 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -12,7 +12,7 @@ module ActiveRecord delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, - :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass + :connection, :columns_hash, :to => :klass module ClassSpecificRelation extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 7ddaea1bb0..14520381c9 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -58,7 +58,7 @@ module ActiveRecord # order. The order will depend on the database implementation. # If an order is supplied it will be respected. # - # Person.take # returns an object fetched by SELECT * FROM people + # Person.take # returns an object fetched by SELECT * FROM people LIMIT 1 # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 # Person.where(["name LIKE '%?'", name]).take def take(limit = nil) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 85534608ac..225677085f 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -11,11 +11,11 @@ module ActiveRecord @scope = scope end - # Returns a new relation expressing WHERE + NOT condition - # according to the conditions in the arguments. + # Returns a new relation expressing WHERE + NOT condition according to + # the conditions in the arguments. # - # #not accepts conditions in one of these formats: String, Array, Hash. - # See #where for more details on each format. + # +not+ accepts conditions as a string, array, or hash. See #where for + # more details on each format. # # User.where.not("name = 'Jon'") # # SELECT * FROM users WHERE NOT (name = 'Jon') @@ -31,6 +31,10 @@ module ActiveRecord # # User.where.not(name: %w(Ko1 Nobu)) # # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu') + # + # User.where.not(name: "Jon", role: "admin") + # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' + # def not(opts, *rest) where_value = @scope.send(:build_where, opts, rest).map do |rel| case rel @@ -108,7 +112,7 @@ module ActiveRecord # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) def includes(*args) - check_empty_arguments("includes", *args) + check_if_method_has_arguments!("includes", args) spawn.includes!(*args) end @@ -126,7 +130,7 @@ module ActiveRecord # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = # "users"."id" def eager_load(*args) - check_empty_arguments("eager_load", *args) + check_if_method_has_arguments!("eager_load", args) spawn.eager_load!(*args) end @@ -140,7 +144,7 @@ module ActiveRecord # User.preload(:posts) # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) - check_empty_arguments("preload", *args) + check_if_method_has_arguments!("preload", args) spawn.preload!(*args) end @@ -158,7 +162,7 @@ module ActiveRecord # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - check_empty_arguments("references", *args) + check_if_method_has_arguments!("references", args) spawn.references!(*args) end @@ -238,7 +242,7 @@ module ActiveRecord # User.group('name AS grouped_name, age') # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] def group(*args) - check_empty_arguments("group", *args) + check_if_method_has_arguments!("group", args) spawn.group!(*args) end @@ -269,7 +273,7 @@ module ActiveRecord # User.order(:name, email: :desc) # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC def order(*args) - check_empty_arguments("order", *args) + check_if_method_has_arguments!("order", args) spawn.order!(*args) end @@ -295,7 +299,7 @@ module ActiveRecord # # generates a query with 'ORDER BY name ASC, id ASC'. def reorder(*args) - check_empty_arguments("reorder", *args) + check_if_method_has_arguments!("reorder", args) spawn.reorder!(*args) end @@ -318,8 +322,8 @@ module ActiveRecord # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id") # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) - check_empty_arguments("joins", *args) - spawn.joins!(*args.flatten) + check_if_method_has_arguments!("joins", args) + spawn.joins!(*args.compact.flatten) end def joins!(*args) # :nodoc: @@ -465,8 +469,6 @@ module ActiveRecord end end - # #where! is identical to #where, except that instead of returning a new relation, it adds - # the condition to the existing relation. def where!(opts = :chain, *rest) # :nodoc: if opts == :chain WhereChain.new(self) @@ -483,7 +485,7 @@ module ActiveRecord # # Order.having('SUM(price) > 30').group('user_id') def having(opts, *rest) - check_empty_arguments("having", opts) + opts.blank? ? self : spawn.having!(opts, *rest) spawn.having!(opts, *rest) end @@ -632,7 +634,6 @@ module ActiveRecord spawn.from!(value, subquery_name) end - # Like #from, but modifies relation in place. def from!(value, subquery_name = nil) # :nodoc: self.from_value = [value, subquery_name] self @@ -921,7 +922,23 @@ module ActiveRecord end end - def check_empty_arguments(method_name, *args) + # Checks to make sure that the arguments are not blank. Note that if some + # blank-like object were initially passed into the query method, then this + # method will not raise an error. + # + # Example: + # + # Post.references() # => raises an error + # Post.references([]) # => does not raise an error + # + # This particular method should be called with a method_name and the args + # passed into that method as an input. For example: + # + # def references(*args) + # check_if_method_has_arguments!("references", args) + # ... + # end + def check_if_method_has_arguments!(method_name, args) if args.blank? raise ArgumentError, "The method .#{method_name}() must contain arguments." end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 4b7a388dc7..33718ef0e9 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -218,9 +218,8 @@ module ActiveRecord # after_commit :do_bar, on: :update # after_commit :do_baz, on: :destroy # - # Also, to have the callback fired on create and update, but not on destroy: - # - # after_commit :do_zoo, if: :persisted? + # after_commit :do_foo_bar, :on [:create, :update] + # after_commit :do_bar_baz, :on [:update, :destroy] # # Note that transactional fixtures do not play well with this feature. Please # use the +test_after_commit+ gem to have these hooks fired in tests. @@ -244,12 +243,14 @@ module ActiveRecord if options.is_a?(Hash) && options[:on] assert_valid_transaction_action(options[:on]) options[:if] = Array(options[:if]) - options[:if] << "transaction_include_action?(:#{options[:on]})" + fire_on = Array(options[:on]).map(&:to_sym) + options[:if] << "transaction_include_any_action?(#{fire_on})" end end - def assert_valid_transaction_action(action) - unless ACTIONS.include?(action.to_sym) + def assert_valid_transaction_action(actions) + actions = Array(actions) + if (actions - ACTIONS).any? raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}" end end @@ -378,14 +379,16 @@ module ActiveRecord end # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. - def transaction_include_action?(action) #:nodoc: - case action - when :create - transaction_record_state(:new_record) - when :destroy - destroyed? - when :update - !(transaction_record_state(:new_record) || destroyed?) + def transaction_include_any_action?(actions) #:nodoc: + actions.any? do |action| + case action + when :create + transaction_record_state(:new_record) + when :destroy + destroyed? + when :update + !(transaction_record_state(:new_record) || destroyed?) + end end end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index f9149c1819..0af7cbf74f 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "models/book" module ActiveRecord class AdapterTest < ActiveRecord::TestCase @@ -6,6 +7,19 @@ module ActiveRecord @connection = ActiveRecord::Base.connection end + ## + # PostgreSQL does not support null bytes in strings + unless current_adapter?(:PostgreSQLAdapter) + def test_update_prepared_statement + b = Book.create(name: "my \x00 book") + b.reload + assert_equal "my \x00 book", b.name + b.update_attributes(name: "my other \x00 book") + b.reload + assert_equal "my other \x00 book", b.name + end + end + def test_tables tables = @connection.tables assert tables.include?("accounts") diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index b67d70ede7..b965983fec 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -19,6 +19,9 @@ class MysqlConnectionTest < ActiveRecord::TestCase def test_connect_with_url run_without_connection do |orig| ar_config = ARTest.connection_config['arunit'] + + skip "This test doesn't work with custom socket location" if ar_config['socket'] + url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}" Klass.establish_connection(url) assert_equal ar_config['database'], Klass.connection.current_database diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 3bf9125013..46f3c38ac5 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -73,6 +73,11 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_has_many_through_with_order + authors = Author.includes(:favorite_authors).to_a + assert_no_queries { authors.map(&:favorite_authors) } + end + def test_with_two_tables_in_from_without_getting_double_quoted posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a assert_equal 2, posts.first.comments.size diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index fd6d531645..1ddd380f23 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -20,6 +20,8 @@ require 'models/car' require 'models/bulb' require 'models/engine' require 'models/categorization' +require 'models/minivan' +require 'models/speedometer' class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase class Invoice < ActiveRecord::Base @@ -1747,4 +1749,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase david.posts_with_special_categorizations = [] assert_equal [], david.posts_with_special_categorizations end + + test "does not duplicate associations when used with natural primary keys" do + speedometer = Speedometer.create!(id: '4') + speedometer.minivans.create!(minivan_id: 'a-van-red' ,name: 'a van', color: 'red') + + assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}" + assert_equal 1, speedometer.reload.minivans.to_a.size + end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index fbc66540d6..af1845c937 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1447,6 +1447,13 @@ class BasicsTest < ActiveRecord::TestCase assert_match(/\/#{dev.id}$/, dev.cache_key) end + def test_touch_should_raise_error_on_a_new_object + company = Company.new(:rating => 1, :name => "37signals", :firm_name => "37signals") + assert_raises(ActiveRecord::ActiveRecordError) do + company.touch :updated_at + end + end + def test_cache_key_format_is_precise_enough dev = Developer.first key = dev.cache_key diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 0a04117795..c8dfc3244b 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -43,27 +43,6 @@ module ActiveRecord encoding: "utf8" }, spec) end - def test_url_query_numeric - spec = resolve 'abstract://foo:123?encoding=utf8&int=500&float=10.9' - assert_equal({ - adapter: "abstract", - port: 123, - int: 500, - float: 10.9, - host: "foo", - encoding: "utf8" }, spec) - end - - def test_url_query_boolean - spec = resolve 'abstract://foo:123?true=true&false=false' - assert_equal({ - adapter: "abstract", - port: 123, - true: true, - false: false, - host: "foo" }, spec) - end - def test_encoded_password password = 'am@z1ng_p@ssw0rd#!' encoded_password = URI.encode_www_form_component(password) diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index aa2a6d7509..b1d276f9eb 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -14,46 +14,9 @@ if ActiveRecord::Base.connection.supports_explain? base.connection end - def test_logging_query_plan_with_logger - base.logger.expects(:warn).with do |message| - message.starts_with?('EXPLAIN for:') - end - - with_threshold(0) do - Car.where(:name => 'honda').to_a - end - end - - def test_logging_query_plan_without_logger - original = base.logger - base.logger = nil - - class << base.logger - def warn; raise "Should not be called" end - end - - with_threshold(0) do - car = Car.where(:name => 'honda').first - assert_equal 'honda', car.name - end - ensure - base.logger = original - end - - def test_collect_queries_for_explain - base.auto_explain_threshold_in_seconds = nil - queries = Thread.current[:available_queries_for_explain] = [] - - with_threshold(0) do - Car.where(:name => 'honda').to_a - end - - sql, binds = queries[0] - assert_match "SELECT", sql - assert_match "honda", sql - assert_equal [], binds - ensure - Thread.current[:available_queries_for_explain] = nil + def test_relation_explain + message = Car.where(:name => 'honda').explain + assert_match(/^EXPLAIN for:/, message) end def test_collecting_queries_for_explain @@ -68,16 +31,6 @@ if ActiveRecord::Base.connection.supports_explain? assert_equal [cars(:honda)], result end - def test_logging_query_plan_when_counting_by_sql - base.logger.expects(:warn).with do |message| - message.starts_with?('EXPLAIN for:') - end - - with_threshold(0) do - Car.count_by_sql "SELECT COUNT(*) FROM cars WHERE name = 'honda'" - end - end - def test_exec_explain_with_no_binds sqls = %w(foo bar) binds = [[], []] @@ -113,25 +66,8 @@ if ActiveRecord::Base.connection.supports_explain? base.logger.expects(:warn).never - with_threshold(0) do - Car.where(:name => 'honda').to_a - end - end - - def test_silence_auto_explain - base.expects(:collecting_sqls_for_explain).never - base.logger.expects(:warn).never - base.silence_auto_explain do - with_threshold(0) { Car.all.to_a } - end + Car.where(:name => 'honda').to_a end - def with_threshold(threshold) - current_threshold = base.auto_explain_threshold_in_seconds - base.auto_explain_threshold_in_seconds = threshold - yield - ensure - base.auto_explain_threshold_in_seconds = current_threshold - end end end diff --git a/activerecord/test/cases/migration/rename_column_test.rb b/activerecord/test/cases/migration/columns_test.rb index 8f6918d06a..e52809f0f8 100644 --- a/activerecord/test/cases/migration/rename_column_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -2,7 +2,7 @@ require "cases/migration/helper" module ActiveRecord class Migration - class RenameColumnTest < ActiveRecord::TestCase + class ColumnsTest < ActiveRecord::TestCase include ActiveRecord::Migration::TestHelper self.use_transactional_fixtures = false @@ -86,8 +86,37 @@ module ActiveRecord assert_equal 1, connection.indexes('test_models').size rename_column "test_models", "hat_name", "name" - # FIXME: should we rename the index if it's name was autogenerated by rails? - assert_equal ['index_test_models_on_hat_name'], connection.indexes('test_models').map(&:name) + + assert_equal ['index_test_models_on_name'], connection.indexes('test_models').map(&:name) + end + + def test_rename_column_with_multi_column_index + add_column "test_models", :hat_size, :integer + add_column "test_models", :hat_style, :string, limit: 100 + add_index "test_models", ["hat_style", "hat_size"], unique: true + + rename_column "test_models", "hat_size", 'size' + if current_adapter? :OracleAdapter + assert_equal ['i_test_models_hat_style_size'], connection.indexes('test_models').map(&:name) + else + assert_equal ['index_test_models_on_hat_style_and_size'], connection.indexes('test_models').map(&:name) + end + + rename_column "test_models", "hat_style", 'style' + if current_adapter? :OracleAdapter + assert_equal ['i_test_models_style_size'], connection.indexes('test_models').map(&:name) + else + assert_equal ['index_test_models_on_style_and_size'], connection.indexes('test_models').map(&:name) + end + end + + def test_rename_column_does_not_rename_custom_named_index + add_column "test_models", :hat_name, :string + add_index :test_models, :hat_name, :name => 'idx_hat_name' + + assert_equal 1, connection.indexes('test_models').size + rename_column "test_models", "hat_name", "name" + assert_equal ['idx_hat_name'], connection.indexes('test_models').map(&:name) end def test_remove_column_with_index @@ -107,7 +136,7 @@ module ActiveRecord assert_equal 1, connection.indexes('test_models').size remove_column("test_models", "hat_size") - # Every database and/or database adapter has their own behavior + # Every database and/or database adapter has their own behavior # if it drops the multi-column index when any of the indexed columns dropped by remove_column. if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) assert_equal [], connection.indexes('test_models').map(&:name) @@ -197,6 +226,17 @@ module ActiveRecord assert_equal ['test_models_categories_idx'], connection.indexes('test_models').map(&:name) end + def test_change_column_with_long_index_name + table_name_prefix = 'test_models_' + long_index_name = table_name_prefix + ('x' * (connection.allowed_index_name_length - table_name_prefix.length)) + add_column "test_models", "category", :string + add_index :test_models, :category, name: long_index_name + + change_column "test_models", "category", :string, null: false, default: 'article' + + assert_equal [long_index_name], connection.indexes('test_models').map(&:name) + end + def test_change_column_default add_column "test_models", "first_name", :string connection.change_column_default "test_models", "first_name", "Tester" @@ -213,6 +253,20 @@ module ActiveRecord def test_remove_column_no_second_parameter_raises_exception assert_raise(ArgumentError) { connection.remove_column("funny") } end + + def test_removing_and_renaming_column_preserves_custom_primary_key + connection.create_table "my_table", primary_key: "my_table_id", force: true do |t| + t.integer "col_one" + t.string "col_two", limit: 128, null: false + end + + remove_column("my_table", "col_two") + rename_column("my_table", "col_one", "col_three") + + assert_equal 'my_table_id', connection.primary_key('my_table') + ensure + connection.drop_table(:my_table) rescue nil + end end end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index a41f2c10f0..0e375af6e8 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -55,19 +55,31 @@ module ActiveRecord assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") } end - def test_add_index_name_length_limit - good_index_name = 'x' * connection.index_name_length + def test_add_index_works_with_long_index_names + connection.add_index(table_name, "foo", name: good_index_name) + + assert connection.index_name_exists?(table_name, good_index_name, false) + connection.remove_index(table_name, name: good_index_name) + end + + def test_add_index_does_not_accept_too_long_index_names too_long_index_name = good_index_name + 'x' - assert_raises(ArgumentError) { - connection.add_index(table_name, "foo", :name => too_long_index_name) + e = assert_raises(ArgumentError) { + connection.add_index(table_name, "foo", name: too_long_index_name) } + assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) assert_not connection.index_name_exists?(table_name, too_long_index_name, false) connection.add_index(table_name, "foo", :name => good_index_name) + end + + def test_internal_index_with_name_matching_database_limit + good_index_name = 'x' * connection.index_name_length + connection.add_index(table_name, "foo", name: good_index_name, internal: true) assert connection.index_name_exists?(table_name, good_index_name, false) - connection.remove_index(table_name, :name => good_index_name) + connection.remove_index(table_name, name: good_index_name) end def test_index_symbol_names @@ -196,6 +208,12 @@ module ActiveRecord connection.remove_index("testings", "last_name") assert !connection.index_exists?("testings", "last_name") end + + private + def good_index_name + 'x' * connection.allowed_index_name_length + end + end end end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 21901bec3c..22dbd7c38b 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -63,7 +63,17 @@ module ActiveRecord connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") - assert connection.indexes(:octopi).first.columns.include?("url") + index = connection.indexes(:octopi).first + assert index.columns.include?("url") + assert_equal 'index_octopi_on_url', index.name + end + + def test_rename_table_does_not_rename_custom_named_index + add_index :test_models, :url, name: 'special_url_idx' + + rename_table :test_models, :octopi + + assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name) end def test_rename_table_for_postgresql_should_also_rename_default_sequence diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 08dbf19e7b..8156f99037 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -400,6 +400,8 @@ class PersistencesTest < ActiveRecord::TestCase end def test_string_ids + # FIXME: Fix this failing test + skip "Failing test. We need this fixed before 4.0.0" mv = Minivan.where(:minivan_id => 1234).first_or_initialize assert mv.new_record? assert_equal '1234', mv.minivan_id diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 379c0c0758..8298d7534c 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -321,6 +321,22 @@ class RelationTest < ActiveRecord::TestCase assert_equal 1, person_with_reader_and_post.size end + def test_no_arguments_to_query_methods_raise_errors + assert_raises(ArgumentError) { Topic.references() } + assert_raises(ArgumentError) { Topic.includes() } + assert_raises(ArgumentError) { Topic.preload() } + assert_raises(ArgumentError) { Topic.group() } + assert_raises(ArgumentError) { Topic.reorder() } + end + + def test_blank_like_arguments_to_query_methods_dont_raise_errors + assert_nothing_raised { Topic.references([]) } + assert_nothing_raised { Topic.includes([]) } + assert_nothing_raised { Topic.preload([]) } + assert_nothing_raised { Topic.group([]) } + assert_nothing_raised { Topic.reorder([]) } + end + def test_scoped_responds_to_delegated_methods relation = Topic.all diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index fd5651b4e0..eb4ffd4498 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -312,3 +312,38 @@ class SaveFromAfterCommitBlockTest < ActiveRecord::TestCase assert_equal true, topic.record_updated end end + +class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class TopicWithCallbacksOnMultipleActions < ActiveRecord::Base + self.table_name = :topics + + after_commit(on: [:create, :destroy]) { |record| record.history << :create_and_destroy } + after_commit(on: [:create, :update]) { |record| record.history << :create_and_update } + after_commit(on: [:update, :destroy]) { |record| record.history << :update_and_destroy } + + def clear_history + @history = [] + end + + def history + @history ||= [] + end + end + + def test_after_commit_on_multiple_actions + topic = TopicWithCallbacksOnMultipleActions.new + topic.save + assert_equal [:create_and_update, :create_and_destroy], topic.history + + topic.clear_history + topic.approved = true + topic.save + assert_equal [:update_and_destroy, :create_and_update], topic.history + + topic.clear_history + topic.destroy + assert_equal [:update_and_destroy, :create_and_destroy], topic.history + end +end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 546737b398..6d66342fa5 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -460,7 +460,7 @@ class TransactionTest < ActiveRecord::TestCase assert !transaction.state.committed? transaction.perform_rollback - + assert transaction.state.rolledback? assert !transaction.state.committed? end @@ -474,7 +474,7 @@ class TransactionTest < ActiveRecord::TestCase assert !transaction.state.committed? transaction.perform_commit - + assert !transaction.state.rolledback? assert transaction.state.committed? end diff --git a/activerecord/test/models/speedometer.rb b/activerecord/test/models/speedometer.rb index 0a7d38d8ec..497c3aba9a 100644 --- a/activerecord/test/models/speedometer.rb +++ b/activerecord/test/models/speedometer.rb @@ -1,4 +1,6 @@ class Speedometer < ActiveRecord::Base self.primary_key = :speedometer_id belongs_to :dashboard + + has_many :minivans end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 5f7559b5a6..b07ab749a3 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,13 +1,54 @@ ## Rails 4.0.0 (unreleased) ## -* ActiveSupport::Gzip.compress allows two optional arguments for compression +* Prevent `DateTime#change` from truncating the second fraction, when seconds + do not need to be changed. + + *Chris Baynes* + +* Added `ActiveSupport::TimeWithZone#to_r` for `Time#at` compatibility. + + Before this change: + + Time.zone = 'Tokyo' + time = Time.zone.now + time == Time.at(time) # => false + + After the change: + + Time.zone = 'Tokyo' + time = Time.zone.now + time == Time.at(time) # => true + + *stopdropandrew* + +* `ActiveSupport::NumberHelper#number_to_human` returns the number unaltered when + the given units hash does not contain the needed key, e.g. when the number provided + is less than the largest key provided. + Fixes #9269. + + Examples: + + number_to_human(123, units: {}) # => 123 + number_to_human(123, units: { thousand: 'k' }) # => 123 + + *Michael Hoffman* + +* Added `beginning_of_minute` support to core ext calculations for `Time` and `DateTime`. + + *Gagan Awhad* + +* Add `:nsec` date format. + + *Jamie Gaskins* + +* `ActiveSupport::Gzip.compress` allows two optional arguments for compression level and strategy. *Beyond* * Modify `TimeWithZone#as_json` to include 3 decimal places of sub-second accuracy by default, which is optional as per the ISO8601 spec, but extremely useful. Also - the default behaviour of Date#toJSON() in recent versions of Chrome, Safari and + the default behaviour of `Date#toJSON()` in recent versions of Chrome, Safari and Firefox. *James Harton* @@ -20,7 +61,7 @@ *Andrew White* * Extract `ActiveSupport::Testing::Performance` into https://github.com/rails/rails-perftest - You can add the gem to your Gemfile to keep using performance tests. + You can add the gem to your `Gemfile` to keep using performance tests. gem 'rails-perftest' @@ -47,7 +88,7 @@ *Kelly Stannard* * It's now possible to compare `Date`, `DateTime`, `Time` and `TimeWithZone` - with `Infinity`. This allows to create date/time ranges with one infinite bound. + with `Float::INFINITY`. This allows to create date/time ranges with one infinite bound. Example: range = Range.new(Date.today, Float::INFINITY) @@ -80,13 +121,13 @@ * Remove surrogate unicode character encoding from `ActiveSupport::JSON.encode` The encoding scheme was broken for unicode characters outside the basic multilingual plane; - since json is assumed to be `UTF-8`, and we already force the encoding to `UTF-8`, + since json is assumed to be UTF-8, and we already force the encoding to UTF-8, simply pass through the un-encoded characters. *Brett Carter* * Deprecate `Time.time_with_date_fallback`, `Time.utc_time` and `Time.local_time`. - These methods were added to handle the limited range of Ruby's native Time + These methods were added to handle the limited range of Ruby's native `Time` implementation. Those limitations no longer apply so we are deprecating them in 4.0 and they will be removed in 4.1. @@ -94,16 +135,17 @@ * Deprecate `Date#to_time_in_current_zone` and add `Date#in_time_zone`. *Andrew White* -* Add `String#in_time_zone` method to convert a string to an ActiveSupport::TimeWithZone. *Andrew White* +* Add `String#in_time_zone` method to convert a string to an `ActiveSupport::TimeWithZone`. *Andrew White* * Deprecate `ActiveSupport::BasicObject` in favor of `ActiveSupport::ProxyObject`. - This class is used for proxy classes. It avoids confusion with Ruby's BasicObject + This class is used for proxy classes. It avoids confusion with Ruby's `BasicObject` class. *Francesco Rodriguez* -* Patched Marshal#load to work with constant autoloading. - Fixes autoloading with cache stores that relay on Marshal(MemCacheStore and FileStore). [fixes #8167] +* Patched `Marshal#load` to work with constant autoloading. Fixes autoloading + with cache stores that rely on `Marshal` (`MemCacheStore` and `FileStore`). + Fixes #8167. *Uriel Katz* @@ -118,9 +160,9 @@ *Olek Janiszewski* -* No longer proxy ActiveSupport::Multibyte#class. *Steve Klabnik* +* No longer proxy `ActiveSupport::Multibyte#class`. *Steve Klabnik* -* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead. *Carlos Antonio da Silva* +* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from minitest instead. *Carlos Antonio da Silva* * `XmlMini.with_backend` now may be safely used with threads: @@ -135,18 +177,17 @@ *Nikita Afanasenko* -* Dependencies no longer trigger Kernel#autoload in remove_constant [fixes #8213]. *Xavier Noria* +* Dependencies no longer trigger `Kernel#autoload` in `remove_constant`. Fixes #8213. *Xavier Noria* -* Simplify mocha integration and remove monkey-patches, bumping mocha to 0.13.0. *James Mead* +* Simplify `mocha` integration and remove monkey-patches, bumping `mocha` to 0.13.0. *James Mead* -* `#as_json` isolates options when encoding a hash. - Fix #8182 +* `#as_json` isolates options when encoding a hash. Fixes #8182. *Yves Senn* -* Deprecate Hash#diff in favor of MiniTest's #diff. *Steve Klabnik* +* Deprecate `Hash#diff` in favor of minitest's #diff. *Steve Klabnik* -* Kernel#capture can catch output from subprocesses *Dmitry Vorotilin* +* `Kernel#capture` can catch output from subprocesses. *Dmitry Vorotilin* * `to_xml` conversions now use builder's `tag!` method instead of explicit invocation of `method_missing`. @@ -154,27 +195,30 @@ * Fixed timezone mapping of the Solomon Islands. *Steve Klabnik* -* Make callstack attribute optional in - ActiveSupport::Deprecation::Reporting methods `warn` and `deprecation_warning` +* Make callstack attribute optional in `ActiveSupport::Deprecation::Reporting` + methods `warn` and `deprecation_warning`. *Alexey Gaziev* -* Implement HashWithIndifferentAccess#replace so key? works correctly. *David Graham* +* Implement `HashWithIndifferentAccess#replace` so `key?` works correctly. *David Graham* -* Handle the possible Permission Denied errors atomic.rb might trigger due to its chown and chmod calls. *Daniele Sluijters* +* Handle the possible permission denied errors `atomic.rb` might trigger due to its `chown` + and `chmod` calls. -* Hash#extract! returns only those keys that present in the receiver. + *Daniele Sluijters* + +* `Hash#extract!` returns only those keys that present in the receiver. {a: 1, b: 2}.extract!(:a, :x) # => {:a => 1} *Mikhail Dieterle* -* Hash#extract! returns the same subclass, that the receiver is. I.e. - HashWithIndifferentAccess#extract! returns HashWithIndifferentAccess instance. +* `Hash#extract!` returns the same subclass, that the receiver is. I.e. + `HashWithIndifferentAccess#extract!` returns a `HashWithIndifferentAccess` instance. *Mikhail Dieterle* -* Optimize ActiveSupport::Cache::Entry to reduce memory and processing overhead. *Brian Durand* +* Optimize `ActiveSupport::Cache::Entry` to reduce memory and processing overhead. *Brian Durand* * Tests tag the Rails log with the current test class and test case: @@ -183,7 +227,7 @@ *Jeremy Kemper* -* Add `logger.push_tags` and `.pop_tags` to complement logger.tagged: +* Add `logger.push_tags` and `.pop_tags` to complement `logger.tagged`: class Job def before @@ -219,6 +263,7 @@ class User include ActiveSupport::Configurable + config_accessor :hair_colors do [:brown, :black, :blonde, :red] end @@ -228,7 +273,7 @@ *Larry Lv* -* ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of +* `ActiveSupport::Benchmarkable#silence` has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1. *Steve Klabnik* @@ -238,14 +283,14 @@ *Pranas Kiziela* -* ActiveSupport::Deprecation is now a class. It is possible to create an instance +* `ActiveSupport::Deprecation` is now a class. It is possible to create an instance of deprecator. Backwards compatibility has been preserved. You can choose which instance of the deprecator will be used. deprecate :method_name, deprecator: deprecator_instance - You can use ActiveSupport::Deprecation in your gem. + You can use `ActiveSupport::Deprecation` in your gem. require 'active_support/deprecation' require 'active_support/core_ext/module/deprecation' @@ -272,7 +317,7 @@ * `ERB::Util.html_escape` encodes single quote as `#39`. Decimal form has better support in old browsers. *Kalys Osmonov* * `ActiveSupport::Callbacks`: deprecate monkey patch of object callbacks. - Using the #filter method like this: + Using the `filter` method like this: before_filter MyFilter.new @@ -305,7 +350,7 @@ *Akira Matsuda* -* Replace deprecated `memcache-client` gem with `dalli` in ActiveSupport::Cache::MemCacheStore +* Replace deprecated `memcache-client` gem with `dalli` in `ActiveSupport::Cache::MemCacheStore`. *Guillermo Iguaran* @@ -319,14 +364,14 @@ *Erich Menge* -* Add String#indent. *fxn & Ace Suares* +* Add `String#indent`. *fxn & Ace Suares* * Inflections can now be defined per locale. `singularize` and `pluralize` accept locale as an extra argument. *David Celis* -* `Object#try` will now return nil instead of raise a NoMethodError if the +* `Object#try` will now return `nil` instead of raise a `NoMethodError` if the receiving object does not implement the method, but you can still get the old behavior by using the new `Object#try!`. @@ -354,62 +399,64 @@ *Francesco Rodriguez* -* ActionView::Helpers::NumberHelper methods have been moved to ActiveSupport::NumberHelper and are now available via - Numeric#to_s. Numeric#to_s now accepts the formatting options :phone, :currency, :percentage, :delimited, - :rounded, :human, and :human_size. *Andrew Mutz* +* `ActionView::Helpers::NumberHelper` methods have been moved to `ActiveSupport::NumberHelper` and are now available via + `Numeric#to_s`. `Numeric#to_s` now accepts the formatting options `:phone`, `:currency`, `:percentage`, `:delimited`, + `:rounded`, `:human`, and `:human_size`. + + *Andrew Mutz* * Add `Hash#transform_keys`, `Hash#transform_keys!`, `Hash#deep_transform_keys`, and `Hash#deep_transform_keys!`. *Mark McSpadden* -* Changed xml type `datetime` to `dateTime` (with upper case letter `T`). *Angelo Capilleri* +* Changed XML type `datetime` to `dateTime` (with upper case letter `T`). *Angelo Capilleri* * Add `:instance_accessor` option for `class_attribute`. *Alexey Vakhov* * `constantize` now looks in the ancestor chain. *Marc-Andre Lafortune & Andrew White* -* Adds `Hash#deep_stringify_keys` and `Hash#deep_stringify_keys!` to convert all keys from a +Hash+ instance into strings *Lucas Húngaro* +* Adds `Hash#deep_stringify_keys` and `Hash#deep_stringify_keys!` to convert all keys from a `Hash` instance into strings. *Lucas Húngaro* -* Adds `Hash#deep_symbolize_keys` and `Hash#deep_symbolize_keys!` to convert all keys from a +Hash+ instance into symbols *Lucas Húngaro* +* Adds `Hash#deep_symbolize_keys` and `Hash#deep_symbolize_keys!` to convert all keys from a `Hash` instance into symbols. *Lucas Húngaro* * `Object#try` can't call private methods. *Vasiliy Ermolovich* * `AS::Callbacks#run_callbacks` remove `key` argument. *Francesco Rodriguez* -* `deep_dup` works more expectedly now and duplicates also values in +Hash+ instances and elements in +Array+ instances. *Alexey Gaziev* +* `deep_dup` works more expectedly now and duplicates also values in `Hash` instances and elements in `Array` instances. *Alexey Gaziev* -* Inflector no longer applies ice -> ouse to words like slice, police, ets *Wes Morgan* +* Inflector no longer applies ice -> ouse to words like "slice", "police", etc. *Wes Morgan* -* Add `ActiveSupport::Deprecations.behavior = :silence` to completely ignore Rails runtime deprecations *twinturbo* +* Add `ActiveSupport::Deprecations.behavior = :silence` to completely ignore Rails runtime deprecations. *twinturbo* -* Make Module#delegate stop using `send` - can no longer delegate to private methods. *dasch* +* Make `Module#delegate` stop using `send` - can no longer delegate to private methods. *dasch* -* AS::Callbacks: deprecate `:rescuable` option. *Bogdan Gusiev* +* `ActiveSupport::Callbacks`: deprecate `:rescuable` option. *Bogdan Gusiev* -* Adds Integer#ordinal to get the ordinal suffix string of an integer. *Tim Gildea* +* Adds `Integer#ordinal` to get the ordinal suffix string of an integer. *Tim Gildea* -* AS::Callbacks: `:per_key` option is no longer supported +* `ActiveSupport::Callbacks`: `:per_key` option is no longer supported. *Bogdan Gusiev* -* `AS::Callbacks#define_callbacks`: add `:skip_after_callbacks_if_terminated` option. +* `ActiveSupport::Callbacks#define_callbacks`: add `:skip_after_callbacks_if_terminated` option. *Bogdan Gusiev* -* Add html_escape_once to ERB::Util, and delegate escape_once tag helper to it. *Carlos Antonio da Silva* +* Add `html_escape_once` to `ERB::Util`, and delegate the `escape_once` tag helper to it. *Carlos Antonio da Silva* -* Deprecates the compatibility method Module#local_constant_names, - use Module#local_constants instead (which returns symbols). *fxn* +* Deprecates the compatibility method `Module#local_constant_names`, + use `Module#local_constants` instead (which returns symbols). *Xavier Noria* -* Deletes the compatibility method Module#method_names, - use Module#methods from now on (which returns symbols). *fxn* +* Deletes the compatibility method `Module#method_names`, + use `Module#methods` from now on (which returns symbols). *Xavier Noria* -* Deletes the compatibility method Module#instance_method_names, - use Module#instance_methods from now on (which returns symbols). *fxn* +* Deletes the compatibility method `Module#instance_method_names`, + use `Module#instance_methods` from now on (which returns symbols). *Xavier Noria* -* BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger - from Ruby stdlib. +* `BufferedLogger` is deprecated. Use `ActiveSupport::Logger`, or the logger + from the Ruby standard library. -* Unicode database updated to 6.1.0. + *Aaron Patterson* -* Adds `encode_big_decimal_as_string` option to force JSON serialization of BigDecimals as numeric instead - of wrapping them in strings for safety. +* Unicode database updated to 6.1.0. *Norman Clarke* -* Remove deprecated ActiveSupport::JSON::Variable. *Erich Menge* +* Adds `encode_big_decimal_as_string` option to force JSON serialization of `BigDecimal` as numeric instead + of wrapping them in strings for safety. * Optimize log subscribers to check log level before doing any processing. *Brian Durand* diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 1d3682eaf2..9f0864d9bb 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -59,7 +59,7 @@ class DateTime options.fetch(:day, day), options.fetch(:hour, hour), options.fetch(:min, options[:hour] ? 0 : min), - options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec), + options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec + sec_fraction), options.fetch(:offset, offset), options.fetch(:start, start) ) @@ -124,6 +124,18 @@ class DateTime end alias :at_end_of_hour :end_of_hour + # Returns a new DateTime representing the start of the minute (hh:mm:00). + def beginning_of_minute + change(:sec => 0) + end + alias :at_beginning_of_minute :beginning_of_minute + + # Returns a new DateTime representing the end of the minute (hh:mm:59). + def end_of_minute + change(:sec => 59) + end + alias :at_end_of_minute :end_of_minute + # Adjusts DateTime to UTC by adding its offset value; offset is set to 0. # # DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)) # => Mon, 21 Feb 2005 10:11:12 -0600 diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 1f95f62229..a3ce7dbe3f 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -188,6 +188,21 @@ class Time end alias :at_end_of_hour :end_of_hour + # Returns a new Time representing the start of the minute (x:xx:00) + def beginning_of_minute + change(:sec => 0) + end + alias :at_beginning_of_minute :beginning_of_minute + + # Returns a new Time representing the end of the minute, x:xx:59.999999 (.999999999 in ruby1.9) + def end_of_minute + change( + :sec => 59, + :usec => Rational(999999999, 1000) + ) + end + alias :at_end_of_minute :end_of_minute + # Returns a Range representing the whole day of the current time. def all_day beginning_of_day..end_of_day diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 2191471daa..cc935e6cb9 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -580,7 +580,7 @@ module ActiveSupport unit = case units when Hash - units[DECIMAL_UNITS[display_exponent]] + units[DECIMAL_UNITS[display_exponent]] || '' when String, Symbol I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) else diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 0e6d12a186..98c866ac43 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -317,6 +317,10 @@ module ActiveSupport end alias_method :tv_sec, :to_i + def to_r + utc.to_r + end + # Return an instance of Time in the system timezone. def to_time utc.to_time diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 24e62cc2b9..7be578599b 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -88,6 +88,14 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005,2,4,19,59,59), DateTime.civil(2005,2,4,19,30,10).end_of_hour end + def test_beginning_of_minute + assert_equal DateTime.civil(2005,2,4,19,30,0), DateTime.civil(2005,2,4,19,30,10).beginning_of_minute + end + + def test_end_of_minute + assert_equal DateTime.civil(2005,2,4,19,30,59), DateTime.civil(2005,2,4,19,30,10).end_of_minute + end + def test_end_of_month assert_equal DateTime.civil(2005,3,31,23,59,59), DateTime.civil(2005,3,20,10,10,10).end_of_month assert_equal DateTime.civil(2005,2,28,23,59,59), DateTime.civil(2005,2,20,10,10,10).end_of_month @@ -121,6 +129,9 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005,2,22,16), DateTime.civil(2005,2,22,15,15,10).change(:hour => 16) assert_equal DateTime.civil(2005,2,22,16,45), DateTime.civil(2005,2,22,15,15,10).change(:hour => 16, :min => 45) assert_equal DateTime.civil(2005,2,22,15,45), DateTime.civil(2005,2,22,15,15,10).change(:min => 45) + + # datetime with fractions of a second + assert_equal DateTime.civil(2005,2,1,15,15,10.7), DateTime.civil(2005,2,22,15,15,10.7).change(:day => 1) end def test_advance diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 43c92003dc..2864d7a57f 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -121,6 +121,10 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2005,2,4,19,0,0), Time.local(2005,2,4,19,30,10).beginning_of_hour end + def test_beginning_of_minute + assert_equal Time.local(2005,2,4,19,30,0), Time.local(2005,2,4,19,30,10).beginning_of_minute + end + def test_end_of_day assert_equal Time.local(2007,8,12,23,59,59,Rational(999999999, 1000)), Time.local(2007,8,12,10,10,10).end_of_day with_env_tz 'US/Eastern' do @@ -137,6 +141,10 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2005,2,4,19,59,59,Rational(999999999, 1000)), Time.local(2005,2,4,19,30,10).end_of_hour end + def test_end_of_minute + assert_equal Time.local(2005,2,4,19,30,59,Rational(999999999, 1000)), Time.local(2005,2,4,19,30,10).end_of_minute + end + def test_last_year assert_equal Time.local(2004,6,5,10), Time.local(2005,6,5,10,0,0).last_year end diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index c2b3676aac..0f5699fd63 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -326,6 +326,17 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal 946684800, twz.to_i end + def test_to_r + result = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone['Hawaii']).to_r + assert_equal Rational(946684800, 1), result + assert_kind_of Rational, result + end + + def test_time_at + time = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone['Hawaii']) + assert_equal time, Time.at(time) + end + def test_to_time with_env_tz 'US/Eastern' do assert_equal Time, @twz.to_time.class @@ -539,6 +550,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Fri, 31 Dec 1999 19:59:59 EST -05:00", twz.end_of_hour.inspect end + def test_beginning_of_minute + utc = Time.utc(2000, 1, 1, 0, 30, 10) + twz = ActiveSupport::TimeWithZone.new(utc, @time_zone) + assert_equal "Fri, 31 Dec 1999 19:30:10 EST -05:00", twz.inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", twz.beginning_of_hour.inspect + end + + def test_end_of_minute + utc = Time.utc(2000, 1, 1, 0, 30, 10) + twz = ActiveSupport::TimeWithZone.new(utc, @time_zone) + assert_equal "Fri, 31 Dec 1999 19:30:10 EST -05:00", twz.inspect + assert_equal "Fri, 31 Dec 1999 19:30:59 EST -05:00", twz.end_of_minute.inspect + end + def test_since assert_equal "Fri, 31 Dec 1999 19:00:01 EST -05:00", @twz.since(1).inspect end diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 5f54587f93..1fadef3637 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -301,6 +301,13 @@ module ActiveSupport end end + def test_number_to_human_with_custom_units_that_are_missing_the_needed_key + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '123', number_helper.number_to_human(123, units: { thousand: 'k'}) + assert_equal '123', number_helper.number_to_human(123, units: {}) + end + end + def test_number_to_human_with_custom_format [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| assert_equal '123 times Thousand', number_helper.number_to_human(123456, :format => "%n times %u") diff --git a/guides/code/getting_started/config/environments/development.rb b/guides/code/getting_started/config/environments/development.rb index ed2667ac58..d169e9452c 100644 --- a/guides/code/getting_started/config/environments/development.rb +++ b/guides/code/getting_started/config/environments/development.rb @@ -22,10 +22,6 @@ Blog::Application.configure do # Only use best-standards-support built into browsers. config.action_dispatch.best_standards_support = :builtin - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL). - config.active_record.auto_explain_threshold_in_seconds = 0.5 - # Raise an error on page load if there are pending migrations config.active_record.migration_error = :page_load diff --git a/guides/code/getting_started/config/environments/production.rb b/guides/code/getting_started/config/environments/production.rb index 58dab2a319..368a735122 100644 --- a/guides/code/getting_started/config/environments/production.rb +++ b/guides/code/getting_started/config/environments/production.rb @@ -72,10 +72,6 @@ Blog::Application.configure do # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL). - # config.active_record.auto_explain_threshold_in_seconds = 0.5 - # Disable automatic flushing of the log to improve performance. # config.autoflush_log = false diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index 9c157ec0b3..463da488f2 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -200,6 +200,8 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activ * Remove IdentityMap. +* Remove automatic execution of EXPLAIN queries. The option `active_record.auto_explain_threshold_in_seconds` is no longer used and should be removed. + * Adds `ActiveRecord::NullRelation` and `ActiveRecord::Relation#none` implementing the null object pattern for the Relation class. * Added `create_join_table` migration helper to create HABTM join tables. diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 516457bcd3..bb42fab101 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -342,19 +342,17 @@ By using the `after_commit` callback we can account for this case. ```ruby class PictureFile < ActiveRecord::Base - attr_accessor :delete_file + after_commit :delete_picture_file_from_disk, :on => [:destroy] - after_destroy do |picture_file| - picture_file.delete_file = picture_file.filepath - end - - after_commit do |picture_file| - if picture_file.delete_file && File.exist?(picture_file.delete_file) - File.delete(picture_file.delete_file) - picture_file.delete_file = nil + def delete_picture_file_from_disk + if File.exist?(filepath) + File.delete(filepath) end end end ``` +NOTE: the `:on` option specifies when a callback will be fired. If you +don't supply the `:on` option the callback will fire for every action. + The `after_commit` and `after_rollback` callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don't interfere with the other callbacks. As such, if your callback code could raise an exception, you'll need to rescue it and handle it appropriately within the callback. diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 62d6294ae5..bc3b1669d2 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1609,45 +1609,6 @@ EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1) under MySQL. -### Automatic EXPLAIN - -Active Record is able to run EXPLAIN automatically on slow queries and log its -output. This feature is controlled by the configuration parameter - -```ruby -config.active_record.auto_explain_threshold_in_seconds -``` - -If set to a number, any query exceeding those many seconds will have its EXPLAIN -automatically triggered and logged. In the case of relations, the threshold is -compared to the total time needed to fetch records. So, a relation is seen as a -unit of work, no matter whether the implementation of eager loading involves -several queries under the hood. - -A threshold of `nil` disables automatic EXPLAINs. - -The default threshold in development mode is 0.5 seconds, and `nil` in test and -production modes. - -INFO. Automatic EXPLAIN gets disabled if Active Record has no logger, regardless -of the value of the threshold. - -#### Disabling Automatic EXPLAIN - -Automatic EXPLAIN can be selectively silenced with `ActiveRecord::Base.silence_auto_explain`: - -```ruby -ActiveRecord::Base.silence_auto_explain do - # no automatic EXPLAIN is triggered here -end -``` - -That may be useful for queries you know are slow but fine, like a heavyweight -report of an admin interface. - -As its name suggests, `silence_auto_explain` only silences automatic EXPLAINs. -Explicit calls to `ActiveRecord::Relation#explain` run. - ### Interpreting EXPLAIN Interpretation of the output of EXPLAIN is beyond the scope of this guide. The diff --git a/guides/source/configuring.md b/guides/source/configuring.md index be46e15078..dbbeec7126 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -268,8 +268,6 @@ config.middleware.delete "Rack::MethodOverride" * `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is true by default. -* `config.active_record.auto_explain_threshold_in_seconds` configures the threshold for automatic EXPLAINs (`nil` disables this feature). Queries exceeding the threshold get their query plan logged. Default is 0.5 in development mode. - * +config.active_record.cache_timestamp_format+ controls the format of the timestamp value in the cache key. Default is +:number+. The MySQL adapter adds one additional configuration option: diff --git a/guides/source/migrations.md b/guides/source/migrations.md index c4fbae8925..d738d847e9 100644 --- a/guides/source/migrations.md +++ b/guides/source/migrations.md @@ -344,6 +344,16 @@ create_join_table :products, :categories, column_options: {null: true} will create the `product_id` and `category_id` with the `:null` option as `true`. +`create_join_table` also accepts a block, which you can use to add indices +(which are not created by default) or additional columns: + +```ruby +create_join_table :products, :categories do |t| + t.index :products + t.index :categories +end +``` + ### Changing Tables A close cousin of `create_table` is `change_table`, used for changing existing diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 18baed55b5..915a008a92 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -45,6 +45,12 @@ Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must rep * Rails 4.0 has changed `serialized_attributes` and `attr_readonly` to class methods only. Now you shouldn't use instance methods, it's deprecated. You must change them, e.g. `self.serialized_attributes` to `self.class.serialized_attributes`. +* Rails 4.0 has removed `attr_accessible` and `attr_protected` feature in favor of Strong Parameters. You can use the [Protected Attributes gem](https://github.com/rails/protected_attributes) to a smoothly upgrade path. + +### Active Resource + +Rails 4.0 extracted Active Resource to its our gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile. + ### Active Model * Rails 4.0 has changed how errors attach with the `ActiveModel::Validations::ConfirmationValidator`. Now when confirmation validations fail the error will be attached to `:#{attribute}_confirmation` instead of `attribute`. diff --git a/rails.gemspec b/rails.gemspec index 34957c9455..770847e87e 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -27,5 +27,5 @@ Gem::Specification.new do |s| s.add_dependency 'railties', version s.add_dependency 'bundler', '>= 1.2.2', '< 2.0' - s.add_dependency 'sprockets-rails', '~> 2.0.0.rc1' + s.add_dependency 'sprockets-rails', '~> 2.0.0.rc3' end diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index e2f2148911..fc362837d6 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,4 +1,16 @@ ## Rails 4.0.0 (unreleased) ## + +* Improve `rake stats` for JavaScript and CoffeeScript: ignore block comments + and calculates number of functions. + + *Hendy Tanata* + +* Ability to use a custom builder by passing `--builder` (or `-b`) has been removed. Consider + using application template instead. See this guide for more detail: + http://guides.rubyonrails.org/rails_application_templates.html + + *Prem Sichanugrist* + * fix rake db:* tasks to work with DATABASE_URL and without config/database.yml *Terence Lee* diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb index 8937e10db3..44f4d3dabc 100644 --- a/railties/lib/rails/app_rails_loader.rb +++ b/railties/lib/rails/app_rails_loader.rb @@ -3,12 +3,16 @@ require 'pathname' module Rails module AppRailsLoader RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"] - EXECUTABLE = 'bin/rails' + EXECUTABLES = ['bin/rails', 'script/rails'] def self.exec_app_rails - cwd = Dir.pwd - return unless in_rails_application_or_engine? || in_rails_application_or_engine_subdirectory? - exec RUBY, EXECUTABLE, *ARGV if in_rails_application_or_engine? + cwd = Dir.pwd + + exe = find_executable + exe ||= find_executable_in_parent_path + return unless exe + + exec RUBY, exe, *ARGV if find_executable Dir.chdir("..") do # Recurse in a chdir block: if the search fails we want to be sure # the application is generated in the original working directory. @@ -18,12 +22,16 @@ module Rails # could not chdir, no problem just return end - def self.in_rails_application_or_engine? - File.exists?(EXECUTABLE) && File.read(EXECUTABLE) =~ /(APP|ENGINE)_PATH/ + def self.find_executable + EXECUTABLES.find do |exe| + File.exists?(exe) && File.read(exe) =~ /(APP|ENGINE)_PATH/ + end end - def self.in_rails_application_or_engine_subdirectory?(path = Pathname.new(Dir.pwd)) - File.exists?(File.join(path, EXECUTABLE)) || !path.root? && in_rails_application_or_engine_subdirectory?(path.parent) + def self.find_executable_in_parent_path(path = Pathname.new(Dir.pwd)) + EXECUTABLES.find do |exe| + File.exists?(File.join(path, exe)) || !path.root? && find_executable_in_parent_path(path.parent) + end end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 1b88b834c7..17763b39c5 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -97,9 +97,9 @@ module Rails self end - # Loads and returns the contents of the #database_configuration_file. The - # contents of the file are processed via ERB before being sent through - # YAML::load. + # Loads and returns the configuration of the database. + # First, looks at If ENV['DATABASE_URL'] if it's not present it uses the #paths["config/database"] + # The contents of the file are processed via ERB before being sent through YAML::load. def database_configuration if ENV['DATABASE_URL'] {Rails.env => ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.connection_url_to_hash(ENV['DATABASE_URL']).stringify_keys} diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb index b717b026de..e5341ac436 100644 --- a/railties/lib/rails/cli.rb +++ b/railties/lib/rails/cli.rb @@ -3,9 +3,6 @@ require 'rails/app_rails_loader' # If we are inside a Rails application this method performs an exec and thus # the rest of this script is not run. -# -# TODO: when we hit this, advise adding ./bin to $PATH instead. Then the -# app's `rails` executable is run immediately. Rails::AppRailsLoader.exec_app_rails require 'rails/ruby_version_check' diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 039360fcf6..0ae6d2a455 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -1,3 +1,5 @@ +require 'rails/code_statistics_calculator' + class CodeStatistics #:nodoc: TEST_TYPES = ['Controller tests', @@ -33,64 +35,38 @@ class CodeStatistics #:nodoc: end def calculate_directory_statistics(directory, pattern = /.*\.(rb|js|coffee)$/) - stats = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 } + stats = CodeStatisticsCalculator.new Dir.foreach(directory) do |file_name| - if File.directory?(directory + "/" + file_name) and (/^\./ !~ file_name) - newstats = calculate_directory_statistics(directory + "/" + file_name, pattern) - stats.each { |k, v| stats[k] += newstats[k] } + path = "#{directory}/#{file_name}" + + if File.directory?(path) && (/^\./ !~ file_name) + stats.add(calculate_directory_statistics(path, pattern)) end next unless file_name =~ pattern - comment_started = false - - case file_name - when /.*\.js$/ - comment_pattern = /^\s*\/\// - else - comment_pattern = /^\s*#/ - end - - File.open(directory + "/" + file_name) do |f| - while line = f.gets - stats["lines"] += 1 - if(comment_started) - if line =~ /^=end/ - comment_started = false - end - next - else - if line =~ /^=begin/ - comment_started = true - next - end - end - stats["classes"] += 1 if line =~ /^\s*class\s+[_A-Z]/ - stats["methods"] += 1 if line =~ /^\s*def\s+[_a-z]/ - stats["codelines"] += 1 unless line =~ /^\s*$/ || line =~ comment_pattern - end - end + stats.add_by_file_path(path) end stats end def calculate_total - total = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 } - @statistics.each_value { |pair| pair.each { |k, v| total[k] += v } } - total + @statistics.each_with_object(CodeStatisticsCalculator.new) do |pair, total| + total.add(pair.last) + end end def calculate_code code_loc = 0 - @statistics.each { |k, v| code_loc += v['codelines'] unless TEST_TYPES.include? k } + @statistics.each { |k, v| code_loc += v.code_lines unless TEST_TYPES.include? k } code_loc end def calculate_tests test_loc = 0 - @statistics.each { |k, v| test_loc += v['codelines'] if TEST_TYPES.include? k } + @statistics.each { |k, v| test_loc += v.code_lines if TEST_TYPES.include? k } test_loc end @@ -105,15 +81,15 @@ class CodeStatistics #:nodoc: end def print_line(name, statistics) - m_over_c = (statistics["methods"] / statistics["classes"]) rescue m_over_c = 0 - loc_over_m = (statistics["codelines"] / statistics["methods"]) - 2 rescue loc_over_m = 0 - - puts "| #{name.ljust(20)} " + - "| #{statistics["lines"].to_s.rjust(5)} " + - "| #{statistics["codelines"].to_s.rjust(5)} " + - "| #{statistics["classes"].to_s.rjust(7)} " + - "| #{statistics["methods"].to_s.rjust(7)} " + - "| #{m_over_c.to_s.rjust(3)} " + + m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0 + loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0 + + puts "| #{name.ljust(20)} " \ + "| #{statistics.lines.to_s.rjust(5)} " \ + "| #{statistics.code_lines.to_s.rjust(5)} " \ + "| #{statistics.classes.to_s.rjust(7)} " \ + "| #{statistics.methods.to_s.rjust(7)} " \ + "| #{m_over_c.to_s.rjust(3)} " \ "| #{loc_over_m.to_s.rjust(5)} |" end diff --git a/railties/lib/rails/code_statistics_calculator.rb b/railties/lib/rails/code_statistics_calculator.rb new file mode 100644 index 0000000000..60e4aef9b7 --- /dev/null +++ b/railties/lib/rails/code_statistics_calculator.rb @@ -0,0 +1,79 @@ +class CodeStatisticsCalculator #:nodoc: + attr_reader :lines, :code_lines, :classes, :methods + + PATTERNS = { + rb: { + line_comment: /^\s*#/, + begin_block_comment: /^=begin/, + end_block_comment: /^=end/, + class: /^\s*class\s+[_A-Z]/, + method: /^\s*def\s+[_a-z]/, + }, + js: { + line_comment: %r{^\s*//}, + begin_block_comment: %r{^\s*/\*}, + end_block_comment: %r{\*/}, + method: /function(\s+[_a-zA-Z][\da-zA-Z]*)?\s*\(/, + }, + coffee: { + line_comment: /^\s*#/, + begin_block_comment: /^\s*###/, + end_block_comment: /^\s*###/, + class: /^\s*class\s+[_A-Z]/, + method: /[-=]>/, + } + } + + def initialize(lines = 0, code_lines = 0, classes = 0, methods = 0) + @lines = lines + @code_lines = code_lines + @classes = classes + @methods = methods + end + + def add(code_statistics_calculator) + @lines += code_statistics_calculator.lines + @code_lines += code_statistics_calculator.code_lines + @classes += code_statistics_calculator.classes + @methods += code_statistics_calculator.methods + end + + def add_by_file_path(file_path) + File.open(file_path) do |f| + self.add_by_io(f, file_type(file_path)) + end + end + + def add_by_io(io, file_type) + patterns = PATTERNS[file_type] || {} + + comment_started = false + + while line = io.gets + @lines += 1 + + if comment_started + if patterns[:end_block_comment] && line =~ patterns[:end_block_comment] + comment_started = false + end + next + else + if patterns[:begin_block_comment] && line =~ patterns[:begin_block_comment] + comment_started = true + next + end + end + + @classes += 1 if patterns[:class] && line =~ patterns[:class] + @methods += 1 if patterns[:method] && line =~ patterns[:method] + if line !~ /^\s*$/ && (patterns[:line_comment].nil? || line !~ patterns[:line_comment]) + @code_lines += 1 + end + end + end + + private + def file_type(file_path) + File.extname(file_path).sub(/\A\./, '').downcase.to_sym + end +end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 99d80b3245..3080b055a0 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -19,9 +19,6 @@ module Rails argument :app_path, type: :string def self.add_shared_options_for(name) - class_option :builder, type: :string, aliases: '-b', - desc: "Path to some #{name} builder (can be a filesystem path or URL)" - class_option :template, type: :string, aliases: '-m', desc: "Path to some #{name} template (can be a filesystem path or URL)" @@ -81,17 +78,6 @@ module Rails def builder @builder ||= begin - if path = options[:builder] - if URI(path).is_a?(URI::HTTP) - contents = open(path, "Accept" => "application/x-thor-template") {|io| io.read } - else - contents = open(File.expand_path(path, @original_wd)) {|io| io.read } - end - - prok = eval("proc { #{contents} }", TOPLEVEL_BINDING, path, 1) - instance_eval(&prok) - end - builder_class = get_builder_class builder_class.send(:include, ActionMethods) builder_class.new(self) @@ -210,7 +196,7 @@ module Rails # Gems used only for assets and not required # in production environments by default. group :assets do - gem 'sprockets-rails', '~> 2.0.0.rc1' + gem 'sprockets-rails', '~> 2.0.0.rc3' gem 'sass-rails', '~> 4.0.0.beta' gem 'coffee-rails', '~> 4.0.0.beta' 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 c4cc1162a4..d0e62d09cc 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 @@ -20,10 +20,6 @@ config.active_support.deprecation = :log <%- unless options.skip_active_record? -%> - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL). - config.active_record.auto_explain_threshold_in_seconds = 0.5 - # Raise an error on page load if there are pending migrations config.active_record.migration_error = :page_load <%- 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 0ab91d9864..5669fe6d64 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 @@ -76,12 +76,6 @@ # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify - <%- unless options.skip_active_record? -%> - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL). - # config.active_record.auto_explain_threshold_in_seconds = 0.5 - <%- end -%> - # Disable automatic flushing of the log to improve performance. # config.autoflush_log = false diff --git a/railties/lib/rails/tasks/documentation.rake b/railties/lib/rails/tasks/documentation.rake index ea6c074bdc..0057b0f887 100644 --- a/railties/lib/rails/tasks/documentation.rake +++ b/railties/lib/rails/tasks/documentation.rake @@ -102,7 +102,7 @@ namespace :doc do # desc "Generate Rails Guides" task :guides do # FIXME: Reaching outside lib directory is a bad idea - require File.expand_path('../../../../guides/rails_guides', __FILE__) + require File.expand_path('../../../../../guides/rails_guides', __FILE__) RailsGuides::Generator.new(Rails.root.join("doc/guides")).generate end end diff --git a/railties/test/app_rails_loader_test.rb b/railties/test/app_rails_loader_test.rb index 87e0ad7bd7..63ed9eaef0 100644 --- a/railties/test/app_rails_loader_test.rb +++ b/railties/test/app_rails_loader_test.rb @@ -2,40 +2,47 @@ require 'abstract_unit' require 'rails/app_rails_loader' class AppRailsLoaderTest < ActiveSupport::TestCase - test "is in a rails application if bin/rails exists and contains APP_PATH" do - File.stubs(:exists?).returns(true) - File.stubs(:read).with('bin/rails').returns('APP_PATH') - assert Rails::AppRailsLoader.in_rails_application_or_engine? - end - test "is not in a rails application if bin/rails exists but doesn't contain APP_PATH" do - File.stubs(:exists?).returns(true) - File.stubs(:read).with('bin/rails').returns('railties bin/rails') - assert !Rails::AppRailsLoader.in_rails_application_or_engine? + setup do + File.stubs(:exists?).returns(false) end - test "is in a rails application if parent directory has bin/rails containing APP_PATH" do - File.stubs(:exists?).with("/foo/bar/bin/rails").returns(false) - File.stubs(:exists?).with("/foo/bin/rails").returns(true) - File.stubs(:read).with('/foo/bin/rails').returns('APP_PATH') - assert Rails::AppRailsLoader.in_rails_application_or_engine_subdirectory?(Pathname.new("/foo/bar")) - end + ['bin/rails', 'script/rails'].each do |exe| + test "is in a rails application if #{exe} exists and contains APP_PATH" do + File.stubs(:exists?).with(exe).returns(true) + File.stubs(:read).with(exe).returns('APP_PATH') + assert Rails::AppRailsLoader.find_executable + end - test "is not in a rails application if at the root directory and doesn't have bin/rails" do - Pathname.any_instance.stubs(:root?).returns true - assert !Rails::AppRailsLoader.in_rails_application_or_engine? - end + test "is not in a rails application if #{exe} exists but doesn't contain APP_PATH" do + File.stubs(:exists?).with(exe).returns(true) + File.stubs(:read).with(exe).returns("railties #{exe}") + assert !Rails::AppRailsLoader.find_executable + end - test "is in a rails engine if parent directory has bin/rails containing ENGINE_PATH" do - File.stubs(:exists?).with("/foo/bar/bin/rails").returns(false) - File.stubs(:exists?).with("/foo/bin/rails").returns(true) - File.stubs(:read).with('/foo/bin/rails').returns('ENGINE_PATH') - assert Rails::AppRailsLoader.in_rails_application_or_engine_subdirectory?(Pathname.new("/foo/bar")) - end + test "is in a rails application if parent directory has #{exe} containing APP_PATH" do + File.stubs(:exists?).with("/foo/bar/#{exe}").returns(false) + File.stubs(:exists?).with("/foo/#{exe}").returns(true) + File.stubs(:read).with("/foo/#{exe}").returns('APP_PATH') + assert Rails::AppRailsLoader.find_executable_in_parent_path(Pathname.new("/foo/bar")) + end + + test "is not in a rails application if at the root directory and doesn't have #{exe}" do + Pathname.any_instance.stubs(:root?).returns true + assert !Rails::AppRailsLoader.find_executable + end + + test "is in a rails engine if parent directory has #{exe} containing ENGINE_PATH" do + File.stubs(:exists?).with("/foo/bar/#{exe}").returns(false) + File.stubs(:exists?).with("/foo/#{exe}").returns(true) + File.stubs(:read).with("/foo/#{exe}").returns('ENGINE_PATH') + assert Rails::AppRailsLoader.find_executable_in_parent_path(Pathname.new("/foo/bar")) + end - test "is in a rails engine if bin/rails exists containing ENGINE_PATH" do - File.stubs(:exists?).returns(true) - File.stubs(:read).with('bin/rails').returns('ENGINE_PATH') - assert Rails::AppRailsLoader.in_rails_application_or_engine? + test "is in a rails engine if #{exe} exists containing ENGINE_PATH" do + File.stubs(:exists?).with(exe).returns(true) + File.stubs(:read).with(exe).returns('ENGINE_PATH') + assert Rails::AppRailsLoader.find_executable + end end end diff --git a/railties/test/application/initializers/boot_test.rb b/railties/test/application/initializers/boot_test.rb deleted file mode 100644 index 7eda15894e..0000000000 --- a/railties/test/application/initializers/boot_test.rb +++ /dev/null @@ -1,20 +0,0 @@ -require "isolation/abstract_unit" - -module ApplicationTests - class BootTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation - - def setup - # build_app - # boot_rails - end - - def teardown - # teardown_app - end - - test "booting rails sets the load paths correctly" do - # This test is pending reworking the boot process - end - end -end diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb new file mode 100644 index 0000000000..b3eabf5024 --- /dev/null +++ b/railties/test/code_statistics_calculator_test.rb @@ -0,0 +1,288 @@ +require 'abstract_unit' +require 'rails/code_statistics_calculator' + +class CodeStatisticsCalculatorTest < ActiveSupport::TestCase + def setup + @code_statistics_calculator = CodeStatisticsCalculator.new + end + + test 'add statistics to another using #add' do + code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4) + @code_statistics_calculator.add(code_statistics_calculator_1) + + assert_equal 1, @code_statistics_calculator.lines + assert_equal 2, @code_statistics_calculator.code_lines + assert_equal 3, @code_statistics_calculator.classes + assert_equal 4, @code_statistics_calculator.methods + + code_statistics_calculator_2 = CodeStatisticsCalculator.new(2, 3, 4, 5) + @code_statistics_calculator.add(code_statistics_calculator_2) + + assert_equal 3, @code_statistics_calculator.lines + assert_equal 5, @code_statistics_calculator.code_lines + assert_equal 7, @code_statistics_calculator.classes + assert_equal 9, @code_statistics_calculator.methods + end + + test 'accumulate statistics using #add_by_io' do + code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4) + @code_statistics_calculator.add(code_statistics_calculator_1) + + code = <<-'CODE' + def foo + puts 'foo' + end + + def bar; end + class A; end + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rb) + + assert_equal 7, @code_statistics_calculator.lines + assert_equal 7, @code_statistics_calculator.code_lines + assert_equal 4, @code_statistics_calculator.classes + assert_equal 6, @code_statistics_calculator.methods + end + + test 'calculate statistics using #add_by_file_path' do + tmp_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp')) + FileUtils.mkdir_p(tmp_path) + + code = <<-'CODE' + def foo + puts 'foo' + # bar + end + CODE + + file_path = "#{tmp_path}/stats.rb" + File.open(file_path, 'w') { |f| f.write(code) } + + @code_statistics_calculator.add_by_file_path(file_path) + + assert_equal 4, @code_statistics_calculator.lines + assert_equal 3, @code_statistics_calculator.code_lines + assert_equal 0, @code_statistics_calculator.classes + assert_equal 1, @code_statistics_calculator.methods + + FileUtils.rm_rf(tmp_path) + end + + test 'calculate number of Ruby methods' do + code = <<-'CODE' + def foo + puts 'foo' + end + + def bar; end + + class Foo + def bar(abc) + end + end + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rb) + + assert_equal 3, @code_statistics_calculator.methods + end + + test 'calculate Ruby LOCs' do + code = <<-'CODE' + def foo + puts 'foo' + end + + # def bar; end + + class A < B + end + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rb) + + assert_equal 8, @code_statistics_calculator.lines + assert_equal 5, @code_statistics_calculator.code_lines + end + + test 'calculate number of Ruby classes' do + code = <<-'CODE' + class Foo < Bar + def foo + puts 'foo' + end + end + + class Z; end + + # class A + # end + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rb) + + assert_equal 2, @code_statistics_calculator.classes + end + + test 'skip Ruby comments' do + code = <<-'CODE' +=begin + class Foo + def foo + puts 'foo' + end + end +=end + + # class A + # end + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rb) + + assert_equal 10, @code_statistics_calculator.lines + assert_equal 0, @code_statistics_calculator.code_lines + assert_equal 0, @code_statistics_calculator.classes + assert_equal 0, @code_statistics_calculator.methods + end + + test 'calculate number of JS methods' do + code = <<-'CODE' + function foo(x, y, z) { + doX(); + } + + $(function () { + bar(); + }) + + var baz = function ( x ) { + } + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :js) + + assert_equal 3, @code_statistics_calculator.methods + end + + test 'calculate JS LOCs' do + code = <<-'CODE' + function foo() + alert('foo'); + end + + // var b = 2; + + var a = 1; + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :js) + + assert_equal 7, @code_statistics_calculator.lines + assert_equal 4, @code_statistics_calculator.code_lines + end + + test 'skip JS comments' do + code = <<-'CODE' + /* + * var f = function () { + 1 / 2; + } + */ + + // call(); + // + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :js) + + assert_equal 8, @code_statistics_calculator.lines + assert_equal 0, @code_statistics_calculator.code_lines + assert_equal 0, @code_statistics_calculator.classes + assert_equal 0, @code_statistics_calculator.methods + end + + test 'calculate number of CoffeeScript methods' do + code = <<-'CODE' + square = (x) -> x * x + + math = + cube: (x) -> x * square x + + fill = (container, liquid = "coffee") -> + "Filling the #{container} with #{liquid}..." + + $('.shopping_cart').bind 'click', (event) => + @customer.purchase @cart + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee) + + assert_equal 4, @code_statistics_calculator.methods + end + + test 'calculate CoffeeScript LOCs' do + code = <<-'CODE' + # Assignment: + number = 42 + opposite = true + + ### + CoffeeScript Compiler v1.4.0 + Released under the MIT License + ### + + # Conditions: + number = -42 if opposite + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee) + + assert_equal 11, @code_statistics_calculator.lines + assert_equal 3, @code_statistics_calculator.code_lines + end + + test 'calculate number of CoffeeScript classes' do + code = <<-'CODE' + class Animal + constructor: (@name) -> + + move: (meters) -> + alert @name + " moved #{meters}m." + + class Snake extends Animal + move: -> + alert "Slithering..." + super 5 + + # class Horse + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee) + + assert_equal 2, @code_statistics_calculator.classes + end + + test 'skip CoffeeScript comments' do + code = <<-'CODE' +### +class Animal + constructor: (@name) -> + + move: (meters) -> + alert @name + " moved #{meters}m." + ### + + # class Horse + alert 'hello' + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee) + + assert_equal 10, @code_statistics_calculator.lines + assert_equal 1, @code_statistics_calculator.code_lines + assert_equal 0, @code_statistics_calculator.classes + assert_equal 0, @code_statistics_calculator.methods + end +end diff --git a/railties/test/fixtures/lib/app_builders/empty_builder.rb b/railties/test/fixtures/lib/app_builders/empty_builder.rb deleted file mode 100644 index babd9c2461..0000000000 --- a/railties/test/fixtures/lib/app_builders/empty_builder.rb +++ /dev/null @@ -1,2 +0,0 @@ -class AppBuilder -end
\ No newline at end of file diff --git a/railties/test/fixtures/lib/app_builders/simple_builder.rb b/railties/test/fixtures/lib/app_builders/simple_builder.rb deleted file mode 100644 index 993d3a2aa2..0000000000 --- a/railties/test/fixtures/lib/app_builders/simple_builder.rb +++ /dev/null @@ -1,7 +0,0 @@ -class AppBuilder - def gitignore - create_file ".gitignore", <<-R.strip -foobar - R - end -end diff --git a/railties/test/fixtures/lib/app_builders/tweak_builder.rb b/railties/test/fixtures/lib/app_builders/tweak_builder.rb deleted file mode 100644 index cb50be01cb..0000000000 --- a/railties/test/fixtures/lib/app_builders/tweak_builder.rb +++ /dev/null @@ -1,7 +0,0 @@ -class AppBuilder < Rails::AppBuilder - def gitignore - create_file ".gitignore", <<-R.strip -foobar - R - end -end diff --git a/railties/test/fixtures/lib/plugin_builders/empty_builder.rb b/railties/test/fixtures/lib/plugin_builders/empty_builder.rb deleted file mode 100644 index 5c5607621c..0000000000 --- a/railties/test/fixtures/lib/plugin_builders/empty_builder.rb +++ /dev/null @@ -1,2 +0,0 @@ -class PluginBuilder -end diff --git a/railties/test/fixtures/lib/plugin_builders/simple_builder.rb b/railties/test/fixtures/lib/plugin_builders/simple_builder.rb deleted file mode 100644 index 08f6c5535d..0000000000 --- a/railties/test/fixtures/lib/plugin_builders/simple_builder.rb +++ /dev/null @@ -1,7 +0,0 @@ -class PluginBuilder - def gitignore - create_file ".gitignore", <<-R.strip -foobar - R - end -end diff --git a/railties/test/fixtures/lib/plugin_builders/spec_builder.rb b/railties/test/fixtures/lib/plugin_builders/spec_builder.rb deleted file mode 100644 index 2721429599..0000000000 --- a/railties/test/fixtures/lib/plugin_builders/spec_builder.rb +++ /dev/null @@ -1,19 +0,0 @@ -class PluginBuilder < Rails::PluginBuilder - def test - create_file "spec/spec_helper.rb" - append_file "Rakefile", <<-EOF -# spec tasks in rakefile - -task default: :spec - EOF - end - - def generate_test_dummy - dummy_path("spec/dummy") - super - end - - def skip_test_unit? - true - end -end diff --git a/railties/test/fixtures/lib/plugin_builders/tweak_builder.rb b/railties/test/fixtures/lib/plugin_builders/tweak_builder.rb deleted file mode 100644 index 1e801409a4..0000000000 --- a/railties/test/fixtures/lib/plugin_builders/tweak_builder.rb +++ /dev/null @@ -1,7 +0,0 @@ -class PluginBuilder < Rails::PluginBuilder - def gitignore - create_file ".gitignore", <<-R.strip -foobar - R - end -end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 83f43d1025..0697035871 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -342,15 +342,6 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_generated_environments_file_for_auto_explain - run_generator [destination_root, "--skip-active-record"] - %w(development production).each do |env| - assert_file "config/environments/#{env}.rb" do |file| - assert_no_match %r(auto_explain_threshold_in_seconds), file - end - end - end - def test_pretend_option output = run_generator [File.join(destination_root, "myapp"), "--pretend"] assert_no_match(/run bundle install/, output) @@ -366,28 +357,3 @@ protected assert_file "Gemfile", /^gem\s+["']#{gem}["']$/ end end - -class CustomAppGeneratorTest < Rails::Generators::TestCase - include GeneratorsTestHelper - tests Rails::Generators::AppGenerator - - arguments [destination_root] - include SharedCustomGeneratorTests - -protected - def default_files - ::DEFAULT_APP_FILES - end - - def builders_dir - "app_builders" - end - - def builder_class - :AppBuilder - end - - def action(*args, &block) - silence(:stdout) { generator.send(*args, &block) } - end -end diff --git a/railties/test/generators/plugin_new_generator_test.rb b/railties/test/generators/plugin_new_generator_test.rb index 4bf5a2f08b..904c1db57e 100644 --- a/railties/test/generators/plugin_new_generator_test.rb +++ b/railties/test/generators/plugin_new_generator_test.rb @@ -371,38 +371,3 @@ protected ::DEFAULT_PLUGIN_FILES end end - -class CustomPluginGeneratorTest < Rails::Generators::TestCase - include GeneratorsTestHelper - tests Rails::Generators::PluginNewGenerator - - destination File.join(Rails.root, "tmp/bukkits") - arguments [destination_root] - include SharedCustomGeneratorTests - - def test_overriding_test_framework - FileUtils.cd(destination_root) - run_generator([destination_root, "-b", "#{Rails.root}/lib/plugin_builders/spec_builder.rb"]) - assert_file 'spec/spec_helper.rb' - assert_file 'spec/dummy' - assert_file 'Rakefile', /task default: :spec/ - assert_file 'Rakefile', /# spec tasks in rakefile/ - end - -protected - def default_files - ::DEFAULT_PLUGIN_FILES - end - - def builder_class - :PluginBuilder - end - - def builders_dir - "plugin_builders" - end - - def action(*args, &block) - silence(:stdout){ generator.send(*args, &block) } - end -end diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 2081f4aba8..2724882a23 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -140,61 +140,3 @@ module SharedGeneratorTests assert_no_file('app/mailers/.keep') end end - -module SharedCustomGeneratorTests - def setup - Rails.application = TestApp::Application - super - Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) - end - - def teardown - super - Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) - Object.class_eval do - remove_const :AppBuilder if const_defined?(:AppBuilder) - remove_const :PluginBuilder if const_defined?(:PluginBuilder) - end - Rails.application = TestApp::Application.instance - end - - def test_builder_option_with_empty_app_builder - FileUtils.cd(destination_root) - run_generator([destination_root, "-b", "#{Rails.root}/lib/#{builders_dir}/empty_builder.rb"]) - default_files.each{ |path| assert_no_file path } - end - - def test_builder_option_with_simple_plugin_builder - FileUtils.cd(destination_root) - run_generator([destination_root, "-b", "#{Rails.root}/lib/#{builders_dir}/simple_builder.rb"]) - (default_files - ['.gitignore']).each{ |path| assert_no_file path } - assert_file ".gitignore", "foobar" - end - - def test_builder_option_with_relative_path - here = File.expand_path(File.dirname(__FILE__)) - FileUtils.cd(here) - run_generator([destination_root, "-b", "../fixtures/lib/#{builders_dir}/simple_builder.rb"]) - FileUtils.cd(destination_root) - (default_files - ['.gitignore']).each{ |path| assert_no_file path } - assert_file ".gitignore", "foobar" - end - - def test_builder_option_with_tweak_plugin_builder - FileUtils.cd(destination_root) - run_generator([destination_root, "-b", "#{Rails.root}/lib/#{builders_dir}/tweak_builder.rb"]) - default_files.each{ |path| assert_file path } - assert_file ".gitignore", "foobar" - end - - def test_builder_option_with_http - url = "https://gist.github.com/josevalim/103208/raw/" - template = "class #{builder_class}; end" - template.instance_eval "def read; self; end" # Make the string respond to read - - generator([destination_root], builder: url).expects(:open).with(url, 'Accept' => 'application/x-thor-template').returns(template) - quietly { generator.invoke_all } - - default_files.each{ |path| assert_no_file(path) } - end -end |