diff options
47 files changed, 619 insertions, 590 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 9b8b175450..a2e2c74c33 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,20 +1,31 @@ ## Rails 4.0.0 (unreleased) ## -* Fix `time_zone_options_for_select` to call `dup` on the returned TimeZone array. - - 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. - - *Brian McManus* - +* 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. + + Example: + + # this will route to questions#new + scope ':locale' do + get 'questions/new' + end + + *Yves Senn* + +* Remove support for parsing XML parameters from request. If you still want to parse XML + parameters, please install `actionpack-xml_parser' gem. + + *Prem Sichanugrist* + * Remove support for parsing YAML parameters from request. *Aaron Patterson* * 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* @@ -28,31 +39,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 directory notation instead: - resources :posts, :controller => "admin/posts" + Use path notation instead: + + 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'} @@ -60,23 +70,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* @@ -96,35 +108,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' } @@ -158,7 +153,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* @@ -196,12 +191,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* @@ -225,8 +221,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* @@ -251,7 +247,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, @@ -297,7 +293,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/' @@ -396,23 +392,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. @@ -465,10 +455,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. @@ -554,9 +540,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* @@ -830,9 +814,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 @@ -859,7 +843,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* @@ -889,7 +873,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. @@ -984,9 +968,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/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/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 0898ad82dd..0fa1e9b859 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -13,10 +13,7 @@ module ActionDispatch end end - DEFAULT_PARSERS = { - Mime::XML => :xml_simple, - Mime::JSON => :json - } + DEFAULT_PARSERS = { Mime::JSON => :json } def initialize(app, parsers = {}) @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) @@ -36,19 +33,13 @@ module ActionDispatch return false if request.content_length.zero? - mime_type = content_type_from_legacy_post_data_format_header(env) || - request.content_mime_type - - strategy = @parsers[mime_type] + strategy = @parsers[request.content_mime_type] return false unless strategy case strategy when Proc strategy.call(request.raw_post) - when :xml_simple, :xml_node - data = request.deep_munge(Hash.from_xml(request.body.read) || {}) - data.with_indifferent_access when :json data = ActiveSupport::JSON.decode(request.body) data = {:_json => data} unless data.is_a?(Hash) @@ -56,23 +47,12 @@ module ActionDispatch else false end - rescue Exception => e # YAML, XML or Ruby code block errors + rescue Exception => e # JSON or Ruby code block errors logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" raise ParseError.new(e.message, e) end - def content_type_from_legacy_post_data_format_header(env) - if x_post_format = env['HTTP_X_POST_DATA_FORMAT'] - case x_post_format.to_s.downcase - when 'yaml' then return Mime::YAML - when 'xml' then return Mime::XML - end - end - - nil - end - def logger(env) env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr) end 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/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/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/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb index 19d5652d81..b2dfd96606 100644 --- a/actionpack/test/controller/webservice_test.rb +++ b/actionpack/test/controller/webservice_test.rb @@ -32,169 +32,53 @@ class WebServiceTest < ActionDispatch::IntegrationTest end end - def test_post_xml + def test_post_json with_test_route_set do - post "/", '<entry attributed="true"><summary>content...</summary></entry>', - {'CONTENT_TYPE' => 'application/xml'} + post "/", '{"entry":{"summary":"content..."}}', 'CONTENT_TYPE' => 'application/json' assert_equal 'entry', @controller.response.body assert @controller.params.has_key?(:entry) assert_equal 'content...', @controller.params["entry"]['summary'] - assert_equal 'true', @controller.params["entry"]['attributed'] end end - def test_put_xml + def test_put_json with_test_route_set do - put "/", '<entry attributed="true"><summary>content...</summary></entry>', - {'CONTENT_TYPE' => 'application/xml'} + put "/", '{"entry":{"summary":"content..."}}', 'CONTENT_TYPE' => 'application/json' assert_equal 'entry', @controller.response.body assert @controller.params.has_key?(:entry) assert_equal 'content...', @controller.params["entry"]['summary'] - assert_equal 'true', @controller.params["entry"]['attributed'] end end - def test_put_xml_using_a_type_node + def test_register_and_use_json_simple with_test_route_set do - put "/", '<type attributed="true"><summary>content...</summary></type>', - {'CONTENT_TYPE' => 'application/xml'} - - assert_equal 'type', @controller.response.body - assert @controller.params.has_key?(:type) - assert_equal 'content...', @controller.params["type"]['summary'] - assert_equal 'true', @controller.params["type"]['attributed'] - end - end - - def test_put_xml_using_a_type_node_and_attribute - with_test_route_set do - put "/", '<type attributed="true"><summary type="boolean">false</summary></type>', - {'CONTENT_TYPE' => 'application/xml'} - - assert_equal 'type', @controller.response.body - assert @controller.params.has_key?(:type) - assert_equal false, @controller.params["type"]['summary'] - assert_equal 'true', @controller.params["type"]['attributed'] - end - end - - def test_post_xml_using_a_type_node - with_test_route_set do - post "/", '<font attributed="true"><type>arial</type></font>', - {'CONTENT_TYPE' => 'application/xml'} - - assert_equal 'font', @controller.response.body - assert @controller.params.has_key?(:font) - assert_equal 'arial', @controller.params['font']['type'] - assert_equal 'true', @controller.params["font"]['attributed'] - end - end - - def test_post_xml_using_a_root_node_named_type - with_test_route_set do - post "/", '<type type="integer">33</type>', - {'CONTENT_TYPE' => 'application/xml'} - - assert @controller.params.has_key?(:type) - assert_equal 33, @controller.params['type'] - end - end - - def test_post_xml_using_an_attributted_node_named_type - with_test_route_set do - with_params_parsers Mime::XML => Proc.new { |data| Hash.from_xml(data)['request'].with_indifferent_access } do - post "/", '<request><type type="string">Arial,12</type><z>3</z></request>', - {'CONTENT_TYPE' => 'application/xml'} - - assert_equal 'type, z', @controller.response.body - assert @controller.params.has_key?(:type) - assert_equal 'Arial,12', @controller.params['type'], @controller.params.inspect - assert_equal '3', @controller.params['z'], @controller.params.inspect - end - end - end - - def test_post_xml_using_a_disallowed_type_attribute - $stderr = StringIO.new - with_test_route_set do - post '/', '<foo type="symbol">value</foo>', 'CONTENT_TYPE' => 'application/xml' - assert_response 500 - - post '/', '<foo type="yaml">value</foo>', 'CONTENT_TYPE' => 'application/xml' - assert_response 500 - end - ensure - $stderr = STDERR - end - - def test_register_and_use_xml_simple - with_test_route_set do - with_params_parsers Mime::XML => Proc.new { |data| Hash.from_xml(data)['request'].with_indifferent_access } do - post "/", '<request><summary>content...</summary><title>SimpleXml</title></request>', - {'CONTENT_TYPE' => 'application/xml'} + with_params_parsers Mime::JSON => Proc.new { |data| JSON.parse(data)['request'].with_indifferent_access } do + post "/", '{"request":{"summary":"content...","title":"JSON"}}', + 'CONTENT_TYPE' => 'application/json' assert_equal 'summary, title', @controller.response.body assert @controller.params.has_key?(:summary) assert @controller.params.has_key?(:title) assert_equal 'content...', @controller.params["summary"] - assert_equal 'SimpleXml', @controller.params["title"] + assert_equal 'JSON', @controller.params["title"] end end end - def test_use_xml_ximple_with_empty_request + def test_use_json_with_empty_request with_test_route_set do - assert_nothing_raised { post "/", "", {'CONTENT_TYPE' => 'application/xml'} } + assert_nothing_raised { post "/", "", 'CONTENT_TYPE' => 'application/json' } assert_equal '', @controller.response.body end end - def test_dasherized_keys_as_xml - with_test_route_set do - post "/?full=1", "<first-key>\n<sub-key>...</sub-key>\n</first-key>", - {'CONTENT_TYPE' => 'application/xml'} - assert_equal 'action, controller, first_key(sub_key), full', @controller.response.body - assert_equal "...", @controller.params[:first_key][:sub_key] - end - end - - def test_typecast_as_xml - with_test_route_set do - xml = <<-XML - <data> - <a type="integer">15</a> - <b type="boolean">false</b> - <c type="boolean">true</c> - <d type="date">2005-03-17</d> - <e type="datetime">2005-03-17T21:41:07Z</e> - <f>unparsed</f> - <g type="integer">1</g> - <g>hello</g> - <g type="date">1974-07-25</g> - </data> - XML - post "/", xml, {'CONTENT_TYPE' => 'application/xml'} - - params = @controller.params - assert_equal 15, params[:data][:a] - assert_equal false, params[:data][:b] - assert_equal true, params[:data][:c] - assert_equal Date.new(2005,3,17), params[:data][:d] - assert_equal Time.utc(2005,3,17,21,41,7), params[:data][:e] - assert_equal "unparsed", params[:data][:f] - assert_equal [1, "hello", Date.new(1974,7,25)], params[:data][:g] - end - end - - def test_entities_unescaped_as_xml_simple + def test_dasherized_keys_as_json with_test_route_set do - xml = <<-XML - <data><foo "bar's" & friends></data> - XML - post "/", xml, {'CONTENT_TYPE' => 'application/xml'} - assert_equal %(<foo "bar's" & friends>), @controller.params[:data] + post "/?full=1", '{"first-key":{"sub-key":"..."}}', 'CONTENT_TYPE' => 'application/json' + assert_equal 'action, controller, first-key(sub-key), full', @controller.response.body + assert_equal "...", @controller.params['first-key']['sub-key'] end end diff --git a/actionpack/test/dispatch/request/xml_params_parsing_test.rb b/actionpack/test/dispatch/request/xml_params_parsing_test.rb deleted file mode 100644 index f13b64a3c7..0000000000 --- a/actionpack/test/dispatch/request/xml_params_parsing_test.rb +++ /dev/null @@ -1,182 +0,0 @@ -require 'abstract_unit' - -class XmlParamsParsingTest < ActionDispatch::IntegrationTest - class TestController < ActionController::Base - class << self - attr_accessor :last_request_parameters - end - - def parse - self.class.last_request_parameters = request.request_parameters - head :ok - end - end - - def teardown - TestController.last_request_parameters = nil - end - - test "parses a strict rack.input" do - class Linted - undef call if method_defined?(:call) - def call(env) - bar = env['action_dispatch.request.request_parameters']['foo'] - result = "<ok>#{bar}</ok>" - [200, {"Content-Type" => "application/xml", "Content-Length" => result.length.to_s}, [result]] - end - end - req = Rack::MockRequest.new(ActionDispatch::ParamsParser.new(Linted.new)) - resp = req.post('/', "CONTENT_TYPE" => "application/xml", :input => "<foo>bar</foo>", :lint => true) - assert_equal "<ok>bar</ok>", resp.body - end - - def assert_parses(expected, xml) - with_test_routing do - post "/parse", xml, default_headers - assert_response :ok - assert_equal(expected, TestController.last_request_parameters) - end - end - - test "nils are stripped from collections" do - assert_parses( - {"hash" => { "person" => nil} }, - "<hash><person type=\"array\"><person nil=\"true\"/></person></hash>") - assert_parses( - {"hash" => { "person" => ['foo']} }, - "<hash><person type=\"array\"><person>foo</person><person nil=\"true\"/></person>\n</hash>") - end - - test "parses hash params" do - with_test_routing do - xml = "<person><name>David</name></person>" - post "/parse", xml, default_headers - assert_response :ok - assert_equal({"person" => {"name" => "David"}}, TestController.last_request_parameters) - end - end - - test "parses single file" do - with_test_routing do - xml = "<person><name>David</name><avatar type='file' name='me.jpg' content_type='image/jpg'>#{::Base64.encode64('ABC')}</avatar></person>" - post "/parse", xml, default_headers - assert_response :ok - - person = TestController.last_request_parameters - assert_equal "image/jpg", person['person']['avatar'].content_type - assert_equal "me.jpg", person['person']['avatar'].original_filename - assert_equal "ABC", person['person']['avatar'].read - end - end - - test "logs error if parsing unsuccessful" do - with_test_routing do - output = StringIO.new - xml = "<person><name>David</name><avatar type='file' name='me.jpg' content_type='image/jpg'>#{::Base64.encode64('ABC')}</avatar></pineapple>" - post "/parse", xml, default_headers.merge('action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => ActiveSupport::Logger.new(output)) - assert_response :error - output.rewind && err = output.read - assert err =~ /Error occurred while parsing request parameters/ - end - end - - test "occurring a parse error if parsing unsuccessful" do - with_test_routing do - begin - $stderr = StringIO.new # suppress the log - xml = "<person><name>David</name></pineapple>" - exception = assert_raise(ActionDispatch::ParamsParser::ParseError) { post "/parse", xml, default_headers.merge('action_dispatch.show_exceptions' => false) } - assert_equal REXML::ParseException, exception.original_exception.class - assert_equal exception.original_exception.message, exception.message - ensure - $stderr = STDERR - end - end - end - - test "parses multiple files" do - xml = <<-end_body - <person> - <name>David</name> - <avatars> - <avatar type='file' name='me.jpg' content_type='image/jpg'>#{::Base64.encode64('ABC')}</avatar> - <avatar type='file' name='you.gif' content_type='image/gif'>#{::Base64.encode64('DEF')}</avatar> - </avatars> - </person> - end_body - - with_test_routing do - post "/parse", xml, default_headers - assert_response :ok - end - - person = TestController.last_request_parameters - - assert_equal "image/jpg", person['person']['avatars']['avatar'].first.content_type - assert_equal "me.jpg", person['person']['avatars']['avatar'].first.original_filename - assert_equal "ABC", person['person']['avatars']['avatar'].first.read - - assert_equal "image/gif", person['person']['avatars']['avatar'].last.content_type - assert_equal "you.gif", person['person']['avatars']['avatar'].last.original_filename - assert_equal "DEF", person['person']['avatars']['avatar'].last.read - end - - private - def with_test_routing - with_routing do |set| - set.draw do - post ':action', :to => ::XmlParamsParsingTest::TestController - end - yield - end - end - - def default_headers - {'CONTENT_TYPE' => 'application/xml'} - end -end - -class LegacyXmlParamsParsingTest < XmlParamsParsingTest - private - def default_headers - {'HTTP_X_POST_DATA_FORMAT' => 'xml'} - end -end - -class RootLessXmlParamsParsingTest < ActionDispatch::IntegrationTest - class TestController < ActionController::Base - wrap_parameters :person, :format => :xml - - class << self - attr_accessor :last_request_parameters - end - - def parse - self.class.last_request_parameters = request.request_parameters - head :ok - end - end - - def teardown - TestController.last_request_parameters = nil - end - - test "parses hash params" do - with_test_routing do - xml = "<name>David</name>" - post "/parse", xml, {'CONTENT_TYPE' => 'application/xml'} - assert_response :ok - assert_equal({"name" => "David", "person" => {"name" => "David"}}, TestController.last_request_parameters) - end - end - - private - def with_test_routing - with_routing do |set| - set.draw do - post ':action', :to => ::RootLessXmlParamsParsingTest::TestController - end - yield - end - end -end 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/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/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 00b2c431ac..3410f38063 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,14 +1,52 @@ ## Rails 4.0.0 (unreleased) ## -* 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. +* 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. + Fixes #7313. + + *Yves Senn* + +* 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* * Simplified type casting code for timezone aware attributes to use the @@ -172,7 +210,7 @@ *Lilibeth De La Cruz* * When `#count` is used in conjunction with `#uniq` we perform `count(:distinct => true)`. - Fix #6865. + Fixes #6865. Example: @@ -205,7 +243,7 @@ *John Wang* * Collection associations `#empty?` always respects builded records. - Fix #8879. + Fixes #8879. Example: @@ -294,18 +332,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 @@ -328,7 +361,7 @@ to the update query. class User < ActiveRecord::Base - default_scope where(active: true) + default_scope -> { where(active: true) } end user = User.first @@ -465,11 +498,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* @@ -1247,13 +1275,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/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_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index e07dbc7da9..0871454cfe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -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. diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 577a362568..de418eab14 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -38,7 +38,7 @@ module ActiveRecord private def resolve_string_connection(spec) # :nodoc: hash = configurations.fetch(spec) do |k| - connection_url_to_hash(k) + self.class.connection_url_to_hash(k) end raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash @@ -51,10 +51,13 @@ module ActiveRecord raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) + path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter" begin - require "active_record/connection_adapters/#{spec[:adapter]}_adapter" + require path_to_adapter + rescue Gem::LoadError => e + raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile." rescue LoadError => e - raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace + raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end adapter_method = "#{spec[:adapter]}_connection" @@ -66,7 +69,7 @@ module ActiveRecord SIMPLE_INT = /\A\d+\z/ SIMPLE_FLOAT = /\A\d+\.\d+\z/ - def connection_url_to_hash(url) # :nodoc: + def self.connection_url_to_hash(url) # :nodoc: config = URI.parse url adapter = config.scheme adapter = "postgresql" if adapter == "postgres" @@ -94,7 +97,7 @@ module ActiveRecord spec end - def type_cast_value(value) + def self.type_cast_value(value) case value when SIMPLE_INT value.to_i 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/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 91444950be..6ede5f3390 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -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/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..066f93635a 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 diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index aceb70bc45..a979be6999 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -141,9 +141,7 @@ module ActiveRecord # and then establishes the connection. initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do - unless ENV['DATABASE_URL'] - self.configurations = app.config.database_configuration - end + self.configurations = app.config.database_configuration establish_connection end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 85534608ac..20e23cb003 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + # has_arguments?("references", args) + # ... + # end + def has_arguments?(method_name, args) if args.blank? raise ArgumentError, "The method .#{method_name}() must contain arguments." end diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index f0a2cdca1a..0a04117795 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -20,48 +20,48 @@ module ActiveRecord def test_url_host_no_db spec = resolve 'abstract://foo?encoding=utf8' assert_equal({ - :adapter => "abstract", - :host => "foo", - :encoding => "utf8" }, spec) + adapter: "abstract", + host: "foo", + encoding: "utf8" }, spec) end def test_url_host_db spec = resolve 'abstract://foo/bar?encoding=utf8' assert_equal({ - :adapter => "abstract", - :database => "bar", - :host => "foo", - :encoding => "utf8" }, spec) + adapter: "abstract", + database: "bar", + host: "foo", + encoding: "utf8" }, spec) end def test_url_port spec = resolve 'abstract://foo:123?encoding=utf8' assert_equal({ - :adapter => "abstract", - :port => 123, - :host => "foo", - :encoding => "utf8" }, spec) + adapter: "abstract", + port: 123, + host: "foo", + 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) + 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) + adapter: "abstract", + port: 123, + true: true, + false: false, + host: "foo" }, spec) end def test_encoded_password @@ -70,6 +70,14 @@ module ActiveRecord spec = resolve "abstract://foo:#{encoded_password}@localhost/bar" assert_equal password, spec[:password] end + + def test_descriptive_error_message_when_adapter_is_missing + error = assert_raise(LoadError) do + resolve(adapter: 'non-existing') + end + + assert_match "Could not load 'active_record/connection_adapters/non-existing_adapter'", error.message + 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_column_test.rb b/activerecord/test/cases/migration/rename_column_test.rb index 8f6918d06a..88bea2211d 100644 --- a/activerecord/test/cases/migration/rename_column_test.rb +++ b/activerecord/test/cases/migration/rename_column_test.rb @@ -86,8 +86,29 @@ 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' + assert_equal ['index_test_models_on_hat_style_and_size'], connection.indexes('test_models').map(&:name) + + rename_column "test_models", "hat_style", 'style' + assert_equal ['index_test_models_on_style_and_size'], connection.indexes('test_models').map(&:name) + 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 @@ -197,6 +218,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" 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/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/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 5f7559b5a6..df8cf5f8ec 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,13 @@ ## Rails 4.0.0 (unreleased) ## +* 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. 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..82cc46aa37 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -123,6 +123,18 @@ class DateTime change(:min => 59, :sec => 59) end alias :at_end_of_hour :end_of_hour + + # Returns a new DateTime representing the start of the hour (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. # diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 1f95f62229..c18169fab5 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 hour (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 hour, x:59: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/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 24e62cc2b9..59ca2a4059 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 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..21fef88d79 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -539,6 +539,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/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 7260a48c8c..da155628f3 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -116,9 +116,9 @@ When this form is submitted, the value of `params[:client]` will be `{"name" => Note that the `params` hash is actually an instance of `ActiveSupport::HashWithIndifferentAccess`, which acts like a hash that lets you use symbols and strings interchangeably as keys. -### JSON/XML parameters +### JSON parameters -If you're writing a web service application, you might find yourself more comfortable on accepting parameters in JSON or XML format. Rails will automatically convert your parameters into `params` hash, which you'll be able to access like you would normally do with form data. +If you're writing a web service application, you might find yourself more comfortable on accepting parameters in JSON format. Rails will automatically convert your parameters into `params` hash, which you'll be able to access like you would normally do with form data. So for example, if you are sending this JSON parameter: @@ -128,7 +128,7 @@ So for example, if you are sending this JSON parameter: You'll get `params[:company]` as `{ :name => "acme", "address" => "123 Carrot Street" }`. -Also, if you've turned on `config.wrap_parameters` in your initializer or calling `wrap_parameters` in your controller, you can safely omit the root element in the JSON/XML parameter. The parameters will be cloned and wrapped in the key according to your controller's name by default. So the above parameter can be written as: +Also, if you've turned on `config.wrap_parameters` in your initializer or calling `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. The parameters will be cloned and wrapped in the key according to your controller's name by default. So the above parameter can be written as: ```json { "name": "acme", "address": "123 Carrot Street" } @@ -142,6 +142,8 @@ And assume that you're sending the data to `CompaniesController`, it would then You can customize the name of the key or specific parameters you want to wrap by consulting the [API documentation](http://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html) +NOTE: A support for parsing XML parameters has been extracted into a gem named `actionpack-xml_parser` + ### Routing Parameters The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id` will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL: 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/railties/CHANGELOG.md b/railties/CHANGELOG.md index 85069fbd9a..1d7513a84f 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,4 +1,13 @@ ## Rails 4.0.0 (unreleased) ## +* fix rake db:* tasks to work with DATABASE_URL and without config/database.yml + + *Terence Lee* + +* The configuration option `config.assets.precompile` is no longer + in `config/environments/production.rb` but in + `config/application.rb`. + + *Yves Senn* * Add notice message for destroy action in scaffold generator. diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 2c7ddd86e7..17763b39c5 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -97,12 +97,16 @@ 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 - require 'erb' - YAML.load ERB.new(IO.read(paths["config/database"].first)).result + if ENV['DATABASE_URL'] + {Rails.env => ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.connection_url_to_hash(ENV['DATABASE_URL']).stringify_keys} + else + require 'erb' + YAML.load ERB.new(IO.read(paths["config/database"].first)).result + end rescue Psych::SyntaxError => e raise "YAML syntax error occurred while parsing #{paths["config/database"].first}. " \ "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb index daf399a538..1275bce7e6 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -31,6 +31,10 @@ module <%= app_const_base %> # Disable the asset pipeline. config.assets.enabled = false +<% else %> + # Precompile additional assets. + # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. + # config.assets.precompile += %w( search.js ) <% end -%> end 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..1f0111368b 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 @@ -59,12 +59,6 @@ # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = "http://assets.example.com" - <%- unless options.skip_sprockets? -%> - # Precompile additional assets. - # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. - # config.assets.precompile += %w( search.js ) - <%- end -%> - # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false 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/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index ccb47663d4..820b838702 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -21,6 +21,8 @@ module ApplicationTests def set_database_url ENV['DATABASE_URL'] = "sqlite3://:@localhost/#{database_url_db_name}" + # ensure it's using the DATABASE_URL + FileUtils.rm_rf("#{app_path}/config/database.yml") end def expected @@ -166,9 +168,16 @@ module ApplicationTests end test 'db:test:load_structure with database_url' do - require "#{app_path}/config/environment" - set_database_url - db_test_load_structure + old_rails_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = 'test' + + begin + require "#{app_path}/config/environment" + set_database_url + db_test_load_structure + ensure + ENV["RAILS_ENV"] = old_rails_env + end end end end |