diff options
author | Leonel Galรกn <leonel@getstealz.com> | 2017-04-25 10:39:46 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-25 10:39:46 -0500 |
commit | f5b2a0ef408f01dd9f12b456f5e4bb41ebb4cb76 (patch) | |
tree | 4afabfef205d5fbd546946a16bc6cf5eb36279e9 | |
parent | 8648f289f01656b2258687126108c389127108e0 (diff) | |
parent | b32d64df5e5160e25804c7019e2da76bd115f3a6 (diff) | |
download | rails-f5b2a0ef408f01dd9f12b456f5e4bb41ebb4cb76.tar.gz rails-f5b2a0ef408f01dd9f12b456f5e4bb41ebb4cb76.tar.bz2 rails-f5b2a0ef408f01dd9f12b456f5e4bb41ebb4cb76.zip |
Merge branch 'master' into bug/filtered_parameters_class
182 files changed, 2143 insertions, 823 deletions
diff --git a/.travis.yml b/.travis.yml index af19cd41de..5b8cfa9dbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -93,9 +93,14 @@ matrix: - "GEM=ar:mysql2 MYSQL=mariadb" addons: mariadb: 10.0 - - rvm: 2.4.1 + - rvm: 2.3.4 env: - "GEM=ar:sqlite3_mem" + - rvm: 2.3.4 + env: + - "GEM=ar:postgresql POSTGRES=9.2" + addons: + postgresql: "9.2" - rvm: jruby-9.1.8.0 jdk: oraclejdk8 env: @@ -104,12 +109,6 @@ matrix: jdk: oraclejdk8 env: - "GEM=am,amo,aj" - # Test with old (< 9.4.2) postgresql - - rvm: 2.4.1 - env: - - "GEM=ar:postgresql" - addons: - postgresql: "9.4" allow_failures: - rvm: ruby-head - rvm: jruby-9.1.8.0 diff --git a/Gemfile.lock b/Gemfile.lock index 4feb8e2968..da7e5a8cab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -141,7 +141,7 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.0.4) + concurrent-ruby (1.0.5) connection_pool (2.2.1) cookiejar (0.3.3) curses (1.0.2) @@ -187,8 +187,8 @@ GEM ffi (1.9.17) ffi (1.9.17-x64-mingw32) ffi (1.9.17-x86-mingw32) - globalid (0.3.7) - activesupport (>= 4.1.0) + globalid (0.4.0) + activesupport (>= 4.2.0) hiredis (0.6.1) http_parser.rb (0.6.0) i18n (0.8.1) @@ -280,7 +280,7 @@ GEM redis (~> 3.3) resque (~> 1.26) rufus-scheduler (~> 3.2) - rubocop (0.47.1) + rubocop (0.48.1) parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) @@ -341,18 +341,18 @@ GEM rack (>= 1, < 3) thor (0.19.4) thread (0.1.7) - thread_safe (0.3.5) + thread_safe (0.3.6) tilt (2.0.5) turbolinks (5.0.1) turbolinks-source (~> 5) turbolinks-source (5.0.0) - tzinfo (1.2.2) + tzinfo (1.2.3) thread_safe (~> 0.1) tzinfo-data (1.2016.10) tzinfo (>= 1.0.0) uglifier (3.0.4) execjs (>= 0.3.0, < 3) - unicode-display_width (1.1.3) + unicode-display_width (1.2.1) useragent (0.16.8) vegas (0.1.11) rack (>= 1.0.0) diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index d5bd58cfdb..f0502c06ff 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1 +1,7 @@ +* ActionCable socket errors are now logged to the console + + Previously any socket errors were ignored and this made it hard to diagnose socket issues (e.g. as discussed in #28362). + + *Edward Poot* + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actioncable/CHANGELOG.md) for previous changes. diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index 0a517a532d..ac5f405dea 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -126,7 +126,8 @@ module ActionCable end def on_error(message) # :nodoc: - # ignore + # log errors to make diagnosing socket errors easier + logger.error "WebSocket error occurred: #{message}" end def on_close(reason, code) # :nodoc: diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb index 98a114a5f4..30ac1e9c38 100644 --- a/actioncable/test/client_test.rb +++ b/actioncable/test/client_test.rb @@ -68,12 +68,33 @@ class ClientTest < ActionCable::TestCase server.min_threads = 1 server.max_threads = 4 - t = Thread.new { server.run.join } - yield port + thread = server.run - ensure - server.stop(true) if server - t.join if t + begin + yield port + + ensure + server.stop + + begin + thread.join + + rescue IOError + # Work around https://bugs.ruby-lang.org/issues/13405 + # + # Puma's sometimes raising while shutting down, when it closes + # its internal pipe. We can safely ignore that, but we do need + # to do the step skipped by the exception: + server.binder.close + + rescue RuntimeError => ex + # Work around https://bugs.ruby-lang.org/issues/13239 + raise unless ex.message =~ /can't modify frozen IOError/ + + # Handle this as if it were the IOError: do the same as above. + server.binder.close + end + end end class SyncClient diff --git a/actionmailer/test/parameterized_test.rb b/actionmailer/test/parameterized_test.rb index 914ed12312..e988fffcb9 100644 --- a/actionmailer/test/parameterized_test.rb +++ b/actionmailer/test/parameterized_test.rb @@ -14,7 +14,6 @@ class ParameterizedTest < ActiveSupport::TestCase @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name ActionMailer::Base.deliver_later_queue_name = :test_queue - ActionMailer::Base.delivery_method = :test @mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index c5b679207a..51cec82801 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1 +1,13 @@ +* Add `action_controller_api` and `action_controller_base` load hooks to be called in `ActiveSupport.on_load` + + `ActionController::Base` and `ActionController::API` have differing implementations. This means that + the one umbrella hook `action_controller` is not able to address certain situations where a method + may not exist in a certain implementation. + + This is fixed by adding two new hooks so you can target `ActionController::Base` vs `ActionController::API` + + Fixes #27013. + + *Julian Nadeau* + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index 0d1af0d0bd..94698df730 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -141,6 +141,7 @@ module ActionController include mod end + ActiveSupport.run_load_hooks(:action_controller_api, self) ActiveSupport.run_load_hooks(:action_controller, self) end end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index b420e00c78..8c2b111f89 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -267,6 +267,7 @@ module ActionController end end + ActiveSupport.run_load_hooks(:action_controller_base, self) ActiveSupport.run_load_hooks(:action_controller, self) end end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index d9a8b9c12d..5051c02a62 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -213,7 +213,11 @@ module ActionController #:nodoc: if !verified_request? if logger && log_warning_on_csrf_failure - logger.warn "Can't verify CSRF token authenticity." + if valid_request_origin? + logger.warn "Can't verify CSRF token authenticity." + else + logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})" + end end handle_unverified_request end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index baf15570d5..7864f9decd 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -43,6 +43,18 @@ module ActionController end end + # Raised when a Parameters instance is not marked as permitted and + # an operation to transform it to hash is called. + # + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.to_h + # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash + class UnfilteredParameters < ArgumentError + def initialize # :nodoc: + super("unable to convert unpermitted parameters to hash") + end + end + # == Action Controller \Parameters # # Allows you to choose which attributes should be whitelisted for mass updating @@ -53,9 +65,9 @@ module ActionController # # params = ActionController::Parameters.new({ # person: { - # name: 'Francesco', + # name: "Francesco", # age: 22, - # role: 'admin' + # role: "admin" # } # }) # @@ -103,7 +115,7 @@ module ActionController # You can fetch values of <tt>ActionController::Parameters</tt> using either # <tt>:key</tt> or <tt>"key"</tt>. # - # params = ActionController::Parameters.new(key: 'value') + # params = ActionController::Parameters.new(key: "value") # params[:key] # => "value" # params["key"] # => "value" class Parameters @@ -203,13 +215,13 @@ module ActionController # class Person < ActiveRecord::Base # end # - # params = ActionController::Parameters.new(name: 'Francesco') + # params = ActionController::Parameters.new(name: "Francesco") # params.permitted? # => false # Person.new(params) # => ActiveModel::ForbiddenAttributesError # # ActionController::Parameters.permit_all_parameters = true # - # params = ActionController::Parameters.new(name: 'Francesco') + # params = ActionController::Parameters.new(name: "Francesco") # params.permitted? # => true # Person.new(params) # => #<Person id: nil, name: "Francesco"> def initialize(parameters = {}) @@ -228,13 +240,14 @@ module ActionController end # Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt> - # representation of this parameter with all unpermitted keys removed. + # representation of the parameters with all unpermitted keys removed. # # params = ActionController::Parameters.new({ - # name: 'Senjougahara Hitagi', - # oddity: 'Heavy stone crab' + # name: "Senjougahara Hitagi", + # oddity: "Heavy stone crab" # }) - # params.to_h # => {} + # params.to_h + # # => ActionController::UnfilteredParameters: unable to convert unfiltered parameters to hash # # safe_params = params.permit(:name) # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} @@ -242,17 +255,61 @@ module ActionController if permitted? convert_parameters_to_hashes(@parameters, :to_h) else - slice(*self.class.always_permitted_parameters).permit!.to_h + raise UnfilteredParameters end end + # Returns a safe <tt>Hash</tt> representation of the parameters + # with all unpermitted keys removed. + # + # params = ActionController::Parameters.new({ + # name: "Senjougahara Hitagi", + # oddity: "Heavy stone crab" + # }) + # params.to_hash + # # => ActionController::UnfilteredParameters: unable to convert unfiltered parameters to hash + # + # safe_params = params.permit(:name) + # safe_params.to_hash # => {"name"=>"Senjougahara Hitagi"} + def to_hash + to_h.to_hash + end + + # Returns a string representation of the receiver suitable for use as a URL + # query string: + # + # params = ActionController::Parameters.new({ + # name: "David", + # nationality: "Danish" + # }) + # params.to_query + # # => "name=David&nationality=Danish" + # + # An optional namespace can be passed to enclose key names: + # + # params = ActionController::Parameters.new({ + # name: "David", + # nationality: "Danish" + # }) + # params.to_query("user") + # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish" + # + # The string pairs "key=value" that conform the query string + # are sorted lexicographically in ascending order. + # + # This method is also aliased as +to_param+. + def to_query(*args) + to_h.to_query(*args) + end + alias_method :to_param, :to_query + # Returns an unsafe, unfiltered - # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this - # parameter. + # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the + # parameters. # # params = ActionController::Parameters.new({ - # name: 'Senjougahara Hitagi', - # oddity: 'Heavy stone crab' + # name: "Senjougahara Hitagi", + # oddity: "Heavy stone crab" # }) # params.to_unsafe_h # # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"} @@ -297,7 +354,7 @@ module ActionController # class Person < ActiveRecord::Base # end # - # params = ActionController::Parameters.new(name: 'Francesco') + # params = ActionController::Parameters.new(name: "Francesco") # params.permitted? # => false # Person.new(params) # => ActiveModel::ForbiddenAttributesError # params.permit! @@ -319,7 +376,7 @@ module ActionController # When passed a single key, if it exists and its associated value is # either present or the singleton +false+, returns said value: # - # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person) + # ActionController::Parameters.new(person: { name: "Francesco" }).require(:person) # # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # # Otherwise raises <tt>ActionController::ParameterMissing</tt>: @@ -352,7 +409,7 @@ module ActionController # Technically this method can be used to fetch terminal values: # # # CAREFUL - # params = ActionController::Parameters.new(person: { name: 'Finn' }) + # params = ActionController::Parameters.new(person: { name: "Finn" }) # name = params.require(:person).require(:name) # CAREFUL # # but take into account that at some point those ones have to be permitted: @@ -382,7 +439,7 @@ module ActionController # for the object to +true+. This is useful for limiting which attributes # should be allowed for mass updating. # - # params = ActionController::Parameters.new(user: { name: 'Francesco', age: 22, role: 'admin' }) + # params = ActionController::Parameters.new(user: { name: "Francesco", age: 22, role: "admin" }) # permitted = params.require(:user).permit(:name, :age) # permitted.permitted? # => true # permitted.has_key?(:name) # => true @@ -402,7 +459,7 @@ module ActionController # You may declare that the parameter should be an array of permitted scalars # by mapping it to an empty array: # - # params = ActionController::Parameters.new(tags: ['rails', 'parameters']) + # params = ActionController::Parameters.new(tags: ["rails", "parameters"]) # params.permit(tags: []) # # Sometimes it is not possible or convenient to declare the valid keys of @@ -418,11 +475,11 @@ module ActionController # # params = ActionController::Parameters.new({ # person: { - # name: 'Francesco', + # name: "Francesco", # age: 22, # pets: [{ - # name: 'Purplish', - # category: 'dogs' + # name: "Purplish", + # category: "dogs" # }] # } # }) @@ -441,8 +498,8 @@ module ActionController # params = ActionController::Parameters.new({ # person: { # contact: { - # email: 'none@test.com', - # phone: '555-1234' + # email: "none@test.com", + # phone: "555-1234" # } # } # }) @@ -475,7 +532,7 @@ module ActionController # Returns a parameter for the given +key+. If not found, # returns +nil+. # - # params = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params = ActionController::Parameters.new(person: { name: "Francesco" }) # params[:person] # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # params[:none] # => nil def [](key) @@ -494,11 +551,11 @@ module ActionController # if more arguments are given, then that will be returned; if a block # is given, then that will be run and its result returned. # - # params = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params = ActionController::Parameters.new(person: { name: "Francesco" }) # params.fetch(:person) # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none - # params.fetch(:none, 'Francesco') # => "Francesco" - # params.fetch(:none) { 'Francesco' } # => "Francesco" + # params.fetch(:none, "Francesco") # => "Francesco" + # params.fetch(:none) { "Francesco" } # => "Francesco" def fetch(key, *args) convert_value_to_parameters( @parameters.fetch(key) { @@ -715,8 +772,6 @@ module ActionController end end - undef_method :to_param - # Returns duplicate of object including all parameters. def deep_dup self.class.new(@parameters.deep_dup).tap do |duplicate| diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 60d4789a63..87dd1eba38 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -254,14 +254,5 @@ module ActionDispatch SEPARATORS = %w( / . ? ) #:nodoc: HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc: - - #:stopdoc: - INSECURE_URL_PARAMETERS_MESSAGE = <<-MSG.squish - Attempting to generate a URL from non-sanitized request parameters! - - An attacker can inject malicious data into the generated URL, such as - changing the host. Whitelist and sanitize passed parameters to be secure. - MSG - #:startdoc: end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 8ad17504ae..74904e3d45 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -54,6 +54,7 @@ module ActionDispatch class Mapping #:nodoc: ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z} attr_reader :requirements, :defaults attr_reader :to, :default_controller, :default_action @@ -93,7 +94,7 @@ module ActionDispatch end def self.optional_format?(path, format) - format != false && !path.include?(":format") && !path.end_with?("/") + format != false && path !~ OPTIONAL_FORMAT_REGEX end def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 129e90037e..e1f9fc9ecc 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -318,11 +318,7 @@ module ActionDispatch when Hash args.pop when ActionController::Parameters - if last.permitted? - args.pop.to_h - else - raise ArgumentError, ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE - end + args.pop.to_h end helper.call self, args, options end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 008216cc80..a9bdefa775 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -171,17 +171,10 @@ module ActionDispatch case options when nil _routes.url_for(url_options.symbolize_keys) - when Hash + when Hash, ActionController::Parameters route_name = options.delete :use_route - _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), - route_name) - when ActionController::Parameters - unless options.permitted? - raise ArgumentError.new(ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE) - end - route_name = options.delete :use_route - _routes.url_for(options.to_h.symbolize_keys. - reverse_merge!(url_options), route_name) + merged_url_options = options.to_h.symbolize_keys.reverse_merge!(url_options) + _routes.url_for(merged_url_options, route_name) when String options when Symbol diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 3e067314d6..ae2b45c9f0 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -377,17 +377,17 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal "32", @params[:person].permit([ :age ])[:age] end - test "to_h returns empty hash on unpermitted params" do - assert @params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess - assert_not @params.to_h.is_a? ActionController::Parameters - assert @params.to_h.empty? + test "to_h raises UnfilteredParameters on unfiltered params" do + assert_raises(ActionController::UnfilteredParameters) do + @params.to_h + end end test "to_h returns converted hash on permitted params" do @params.permit! - assert @params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess - assert_not @params.to_h.is_a? ActionController::Parameters + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @params.to_h + assert_not_kind_of ActionController::Parameters, @params.to_h end test "to_h returns converted hash when .permit_all_parameters is set" do @@ -395,39 +395,71 @@ class ParametersPermitTest < ActiveSupport::TestCase ActionController::Parameters.permit_all_parameters = true params = ActionController::Parameters.new(crab: "Senjougahara Hitagi") - assert params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess - assert_not @params.to_h.is_a? ActionController::Parameters + assert_instance_of ActiveSupport::HashWithIndifferentAccess, params.to_h + assert_not_kind_of ActionController::Parameters, params.to_h assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_h) ensure ActionController::Parameters.permit_all_parameters = false end end - test "to_h returns always permitted parameter on unpermitted params" do - params = ActionController::Parameters.new( - controller: "users", - action: "create", - user: { - name: "Sengoku Nadeko" - } - ) + test "to_hash raises UnfilteredParameters on unfiltered params" do + assert_raises(ActionController::UnfilteredParameters) do + @params.to_hash + end + end + + test "to_hash returns converted hash on permitted params" do + @params.permit! + + assert_instance_of Hash, @params.to_hash + assert_not_kind_of ActionController::Parameters, @params.to_hash + end + + test "parameters can be implicit converted to Hash" do + params = ActionController::Parameters.new + params.permit! + + assert_equal({ a: 1 }, { a: 1 }.merge!(params)) + end - assert_equal({ "controller" => "users", "action" => "create" }, params.to_h) + test "to_hash returns converted hash when .permit_all_parameters is set" do + begin + ActionController::Parameters.permit_all_parameters = true + params = ActionController::Parameters.new(crab: "Senjougahara Hitagi") + + assert_instance_of Hash, params.to_hash + assert_not_kind_of ActionController::Parameters, params.to_hash + assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_hash) + assert_equal({ "crab" => "Senjougahara Hitagi" }, params) + ensure + ActionController::Parameters.permit_all_parameters = false + end end test "to_unsafe_h returns unfiltered params" do - assert @params.to_unsafe_h.is_a? ActiveSupport::HashWithIndifferentAccess - assert_not @params.to_unsafe_h.is_a? ActionController::Parameters + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @params.to_unsafe_h + assert_not_kind_of ActionController::Parameters, @params.to_unsafe_h end test "to_unsafe_h returns unfiltered params even after accessing few keys" do params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] }) expected = { "f" => { "language_facet" => ["Tibetan"] } } - assert params["f"].is_a? ActionController::Parameters + assert_instance_of ActionController::Parameters, params["f"] assert_equal expected, params.to_unsafe_h end + test "to_unsafe_h does not mutate the parameters" do + params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] }) + params[:f] + + params.to_unsafe_h + + assert_not_predicate params, :permitted? + assert_not_predicate params[:f], :permitted? + end + test "to_h only deep dups Ruby collections" do company = Class.new do attr_reader :dupped diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index f06a1f4d23..5b16af78c4 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -285,10 +285,10 @@ class RedirectTest < ActionController::TestCase end def test_redirect_to_params - error = assert_raise(ArgumentError) do + error = assert_raise(ActionController::UnfilteredParameters) do get :redirect_to_params end - assert_equal ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE, error.message + assert_equal "unable to convert unpermitted parameters to hash", error.message end def test_redirect_to_with_block diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index d645ddfdbe..521d93f02e 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -35,6 +35,22 @@ module RequestForgeryProtectionActions render inline: "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>" end + def form_with_remote + render inline: "<%= form_with(scope: :some_resource) {} %>" + end + + def form_with_remote_with_token + render inline: "<%= form_with(scope: :some_resource, authenticity_token: true) {} %>" + end + + def form_with_local_with_token + render inline: "<%= form_with(scope: :some_resource, local: true, authenticity_token: true) {} %>" + end + + def form_with_remote_with_external_token + render inline: "<%= form_with(scope: :some_resource, authenticity_token: 'external_token') {} %>" + end + def same_origin_js render js: "foo();" end @@ -235,6 +251,80 @@ module RequestForgeryProtectionTests end end + def test_should_render_form_with_with_token_tag_if_remote + assert_not_blocked do + get :form_with_remote + end + assert_match(/authenticity_token/, response.body) + end + + def test_should_render_form_with_without_token_tag_if_remote_and_embedding_token_is_off + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = false + assert_not_blocked do + get :form_with_remote + end + assert_no_match(/authenticity_token/, response.body) + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + + def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested_and_embedding_is_on + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true + assert_not_blocked do + get :form_with_remote_with_external_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token" + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + + def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested + assert_not_blocked do + get :form_with_remote_with_external_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token" + end + + def test_should_render_form_with_with_token_tag_if_remote_and_authenticity_token_requested + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_with_remote_with_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + end + end + + def test_should_render_form_with_with_token_tag_with_authenticity_token_requested + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_with_local_with_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + end + end + + def test_should_render_form_with_with_token_tag_if_remote_and_embedding_token_is_on + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true + + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_with_remote + end + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + def test_should_allow_get assert_not_blocked { get :index } end @@ -347,6 +437,10 @@ module RequestForgeryProtectionTests end def test_should_block_post_with_origin_checking_and_wrong_origin + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + forgery_protection_origin_check do session[:_csrf_token] = @token @controller.stub :form_authenticity_token, @token do @@ -356,6 +450,13 @@ module RequestForgeryProtectionTests end end end + + assert_match( + "HTTP Origin header (http://bad.host) didn't match request.base_url (http://test.host)", + logger.logged(:warn).last + ) + ensure + ActionController::Base.logger = old_logger end def test_should_warn_on_missing_csrf_token diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb index dd07c2486b..46bb374b3f 100644 --- a/actionpack/test/controller/required_params_test.rb +++ b/actionpack/test/controller/required_params_test.rb @@ -72,9 +72,27 @@ class ParametersRequireTest < ActiveSupport::TestCase assert params.value?("cinco") end - test "to_query is not supported" do - assert_raises(NoMethodError) do - ActionController::Parameters.new(foo: "bar").to_param + test "to_param works like in a Hash" do + params = ActionController::Parameters.new(nested: { key: "value" }).permit! + assert_equal({ nested: { key: "value" } }.to_param, params.to_param) + + params = { root: ActionController::Parameters.new(nested: { key: "value" }).permit! } + assert_equal({ root: { nested: { key: "value" } } }.to_param, params.to_param) + + assert_raise(ActionController::UnfilteredParameters) do + ActionController::Parameters.new(nested: { key: "value" }).to_param + end + end + + test "to_query works like in a Hash" do + params = ActionController::Parameters.new(nested: { key: "value" }).permit! + assert_equal({ nested: { key: "value" } }.to_query, params.to_query) + + params = { root: ActionController::Parameters.new(nested: { key: "value" }).permit! } + assert_equal({ root: { nested: { key: "value" } } }.to_query, params.to_query) + + assert_raise(ActionController::UnfilteredParameters) do + ActionController::Parameters.new(nested: { key: "value" }).to_query end end end diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 862dcf01c3..2afe67ed91 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -386,7 +386,7 @@ module AbstractController def test_url_action_controller_parameters add_host! - assert_raise(ArgumentError) do + assert_raise(ActionController::UnfilteredParameters) do W.new.url_for(ActionController::Parameters.new(controller: "c", action: "a", protocol: "javascript", f: "%0Aeval(name)")) end end diff --git a/actionpack/test/dispatch/routing/custom_url_helpers_test.rb b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb index cb5ca5888b..338992dda5 100644 --- a/actionpack/test/dispatch/routing/custom_url_helpers_test.rb +++ b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb @@ -165,8 +165,8 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest assert_equal "/", params_path(@safe_params) assert_equal "/", Routes.url_helpers.params_path(@safe_params) - assert_raises(ArgumentError) { params_path(@unsafe_params) } - assert_raises(ArgumentError) { Routes.url_helpers.params_path(@unsafe_params) } + assert_raises(ActionController::UnfilteredParameters) { params_path(@unsafe_params) } + assert_raises(ActionController::UnfilteredParameters) { Routes.url_helpers.params_path(@unsafe_params) } assert_equal "/basket", symbol_path assert_equal "/basket", Routes.url_helpers.symbol_path @@ -208,8 +208,8 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest assert_equal "http://www.example.com/", params_url(@safe_params) assert_equal "http://www.example.com/", Routes.url_helpers.params_url(@safe_params) - assert_raises(ArgumentError) { params_url(@unsafe_params) } - assert_raises(ArgumentError) { Routes.url_helpers.params_url(@unsafe_params) } + assert_raises(ActionController::UnfilteredParameters) { params_url(@unsafe_params) } + assert_raises(ActionController::UnfilteredParameters) { Routes.url_helpers.params_url(@unsafe_params) } assert_equal "http://www.example.com/basket", symbol_url assert_equal "http://www.example.com/basket", Routes.url_helpers.symbol_url diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 64818e6ca1..d64917e0d3 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3633,7 +3633,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end params = ActionController::Parameters.new(id: "1") - assert_raises ArgumentError do + assert_raises ActionController::UnfilteredParameters do root_path(params) end end @@ -3706,6 +3706,24 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal "/bar", bar_root_path end + def test_nested_routes_under_format_resource + draw do + resources :formats do + resources :items + end + end + + get "/formats/1/items.json" + assert_equal 200, @response.status + assert_equal "items#index", @response.body + assert_equal "/formats/1/items.json", format_items_path(1, :json) + + get "/formats/1/items/2.json" + assert_equal 200, @response.status + assert_equal "items#show", @response.body + assert_equal "/formats/1/items/2.json", format_item_path(1, 2, :json) + end + private def draw(&block) diff --git a/actionpack/test/fixtures/layouts/builder.builder b/actionpack/test/fixtures/layouts/builder.builder index 7c7d4b2dd1..c55488edd0 100644 --- a/actionpack/test/fixtures/layouts/builder.builder +++ b/actionpack/test/fixtures/layouts/builder.builder @@ -1,3 +1,3 @@ xml.wrapper do xml << yield -end
\ No newline at end of file +end diff --git a/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder b/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder index 598d62e2fc..15c8a7f5cf 100644 --- a/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder +++ b/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder @@ -1 +1 @@ -xml.p "Hello world!"
\ No newline at end of file +xml.p "Hello world!" diff --git a/actionpack/test/fixtures/respond_to/using_defaults.xml.builder b/actionpack/test/fixtures/respond_to/using_defaults.xml.builder index 598d62e2fc..15c8a7f5cf 100644 --- a/actionpack/test/fixtures/respond_to/using_defaults.xml.builder +++ b/actionpack/test/fixtures/respond_to/using_defaults.xml.builder @@ -1 +1 @@ -xml.p "Hello world!"
\ No newline at end of file +xml.p "Hello world!" diff --git a/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder b/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder index 598d62e2fc..15c8a7f5cf 100644 --- a/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder +++ b/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder @@ -1 +1 @@ -xml.p "Hello world!"
\ No newline at end of file +xml.p "Hello world!" diff --git a/actionpack/test/fixtures/test/formatted_xml_erb.builder b/actionpack/test/fixtures/test/formatted_xml_erb.builder index 14fd3549fb..f98aaa34a5 100644 --- a/actionpack/test/fixtures/test/formatted_xml_erb.builder +++ b/actionpack/test/fixtures/test/formatted_xml_erb.builder @@ -1 +1 @@ -xml.test 'failed'
\ No newline at end of file +xml.test "failed" diff --git a/actionpack/test/fixtures/test/hello_xml_world.builder b/actionpack/test/fixtures/test/hello_xml_world.builder index e7081b89fe..d16bb6b5cb 100644 --- a/actionpack/test/fixtures/test/hello_xml_world.builder +++ b/actionpack/test/fixtures/test/hello_xml_world.builder @@ -8,4 +8,4 @@ xml.html do xml.p "monks" xml.p "wiseguys" end -end
\ No newline at end of file +end diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee index 9af515beda..26df7b9a3f 100644 --- a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee @@ -29,6 +29,7 @@ Rails.ajax = (options) -> fire(document, 'ajaxStop') # to be compatible with jQuery.ajax prepareOptions = (options) -> + options.url = options.url or location.href options.type = options.type.toUpperCase() # append data to url if it's a GET request if options.type is 'GET' and options.data @@ -63,10 +64,10 @@ processResponse = (response, type) -> if typeof response is 'string' and typeof type is 'string' if type.match(/\bjson\b/) try response = JSON.parse(response) - else if type.match(/\bjavascript\b/) + else if type.match(/\b(?:java|ecma)script\b/) script = document.createElement('script') - script.innerHTML = response - document.body.appendChild(script) + script.text = response + document.head.appendChild(script).parentNode.removeChild(script) else if type.match(/\b(xml|html|svg)\b/) parser = new DOMParser() type = type.replace(/;.+/, '') # remove something like ';charset=utf-8' diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index ba189e23fe..5ddf1ceb66 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -62,7 +62,7 @@ module ActionView node end else - unless name.include?('#') # Dynamic template partial names can never be tracked + unless name.include?("#") # Dynamic template partial names can never be tracked logger.error " Couldn't find template for digesting: #{name}" end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 26a625e4fe..3eafe0028e 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -201,9 +201,9 @@ module ActionView # <%= f.submit %> # <% end %> # - # This also works for the methods in FormOptionHelper and DateHelper that + # This also works for the methods in FormOptionsHelper and DateHelper that # are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === #form_for with a model object # @@ -416,13 +416,13 @@ module ActionView # # To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter # - # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| + # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %> # ... # <% end %> # # If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>: # - # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| + # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| %> # ... # <% end %> def form_for(record, options = {}, &block) @@ -474,6 +474,8 @@ module ActionView end private :apply_form_for_options! + mattr_accessor(:form_with_generates_remote_forms) { true } + # Creates a form tag based on mixing URLs, scopes, or models. # # # Using just a URL: @@ -632,9 +634,9 @@ module ActionView # <%= form.submit %> # <% end %> # - # Same goes for the methods in FormOptionHelper and DateHelper designed + # Same goes for the methods in FormOptionsHelper and DateHelper designed # to work with an object as a base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === Setting the method # @@ -791,9 +793,9 @@ module ActionView # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is # of class +Permission+, the field will still be named <tt>permission[admin]</tt>. # - # Note: This also works for the methods in FormOptionHelper and + # Note: This also works for the methods in FormOptionsHelper and # DateHelper that are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === Nested Attributes Examples # @@ -1033,9 +1035,9 @@ module ActionView # <%= check_box_tag "comment[all_caps]", "1", @comment.commenter.hulk_mode? %> # <% end %> # - # Same goes for the methods in FormOptionHelper and DateHelper designed + # Same goes for the methods in FormOptionsHelper and DateHelper designed # to work with an object as a base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. def fields(scope = nil, model: nil, **options, &block) options[:allow_method_names_outside_object] = true options[:skip_default_ids] = true @@ -1503,7 +1505,7 @@ module ActionView end private - def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: false, + def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms, skip_enforcing_utf8: false, **options) html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html) html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? @@ -1517,12 +1519,14 @@ module ActionView html_options[:"accept-charset"] = "UTF-8" html_options[:"data-remote"] = true unless local - if !local && !embed_authenticity_token_in_remote_forms && - html_options[:authenticity_token].blank? - # The authenticity token is taken from the meta tag in this case - html_options[:authenticity_token] = false - elsif html_options[:authenticity_token] == true - # Include the default authenticity_token, which is only generated when its set to nil, + html_options[:authenticity_token] = options.delete(:authenticity_token) + + if !local && html_options[:authenticity_token].blank? + html_options[:authenticity_token] = embed_authenticity_token_in_remote_forms + end + + if html_options[:authenticity_token] == true + # Include the default authenticity_token, which is only generated when it's set to nil, # but we needed the true value to override the default of no authenticity_token on data-remote. html_options[:authenticity_token] = nil end @@ -1724,9 +1728,9 @@ module ActionView # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is # of class +Permission+, the field will still be named <tt>permission[admin]</tt>. # - # Note: This also works for the methods in FormOptionHelper and + # Note: This also works for the methods in FormOptionsHelper and # DateHelper that are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === Nested Attributes Examples # diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index ffc64e7118..9fc08b3837 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -18,7 +18,7 @@ module ActionView include TextHelper mattr_accessor :embed_authenticity_token_in_remote_forms - self.embed_authenticity_token_in_remote_forms = false + self.embed_authenticity_token_in_remote_forms = nil # Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like # ActionController::Base#url_for. The method for the form defaults to POST. diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 304db38060..70fc57c35f 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -621,11 +621,6 @@ module ActionView # # => [{name: 'country[name]', value: 'Denmark'}] def to_form_params(attribute, namespace = nil) attribute = if attribute.respond_to?(:permitted?) - unless attribute.permitted? - raise ArgumentError, "Attempting to generate a button from non-sanitized request parameters!" \ - " Whitelist and sanitize passed parameters to be secure." - end - attribute.to_h else attribute diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index d344d98f4b..61678933e9 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -5,7 +5,7 @@ module ActionView # = Action View Railtie class Railtie < Rails::Engine # :nodoc: config.action_view = ActiveSupport::OrderedOptions.new - config.action_view.embed_authenticity_token_in_remote_forms = false + config.action_view.embed_authenticity_token_in_remote_forms = nil config.action_view.debug_missing_translation = true config.eager_load_namespaces << ActionView @@ -17,6 +17,15 @@ module ActionView end end + initializer "action_view.form_with_generates_remote_forms" do |app| + ActiveSupport.on_load(:action_view) do + form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms) + unless form_with_generates_remote_forms.nil? + ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms + end + end + end + initializer "action_view.logger" do ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger } end diff --git a/actionview/test/fixtures/actionpack/layouts/builder.builder b/actionview/test/fixtures/actionpack/layouts/builder.builder index 7c7d4b2dd1..c55488edd0 100644 --- a/actionview/test/fixtures/actionpack/layouts/builder.builder +++ b/actionview/test/fixtures/actionpack/layouts/builder.builder @@ -1,3 +1,3 @@ xml.wrapper do xml << yield -end
\ No newline at end of file +end diff --git a/actionview/test/fixtures/actionpack/test/_hello.builder b/actionview/test/fixtures/actionpack/test/_hello.builder index ef52f632d1..fc72df16d0 100644 --- a/actionview/test/fixtures/actionpack/test/_hello.builder +++ b/actionview/test/fixtures/actionpack/test/_hello.builder @@ -1 +1 @@ -xm.hello
\ No newline at end of file +xm.hello diff --git a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder index 14fd3549fb..f98aaa34a5 100644 --- a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder +++ b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder @@ -1 +1 @@ -xml.test 'failed'
\ No newline at end of file +xml.test "failed" diff --git a/actionview/test/fixtures/actionpack/test/hello.builder b/actionview/test/fixtures/actionpack/test/hello.builder index a471553941..b8ab17ad5b 100644 --- a/actionview/test/fixtures/actionpack/test/hello.builder +++ b/actionview/test/fixtures/actionpack/test/hello.builder @@ -1,4 +1,4 @@ xml.html do xml.p "Hello #{@name}" - xml << render(:file => "test/greeting") -end
\ No newline at end of file + xml << render(file: "test/greeting") +end diff --git a/actionview/test/fixtures/actionpack/test/hello_world_container.builder b/actionview/test/fixtures/actionpack/test/hello_world_container.builder index e48d75c405..24032ab5e0 100644 --- a/actionview/test/fixtures/actionpack/test/hello_world_container.builder +++ b/actionview/test/fixtures/actionpack/test/hello_world_container.builder @@ -1,3 +1,3 @@ xml.test do - render :partial => 'hello', :locals => { :xm => xml } -end
\ No newline at end of file + render partial: "hello", locals: { xm: xml } +end diff --git a/actionview/test/fixtures/actionpack/test/hello_xml_world.builder b/actionview/test/fixtures/actionpack/test/hello_xml_world.builder index e7081b89fe..d16bb6b5cb 100644 --- a/actionview/test/fixtures/actionpack/test/hello_xml_world.builder +++ b/actionview/test/fixtures/actionpack/test/hello_xml_world.builder @@ -8,4 +8,4 @@ xml.html do xml.p "monks" xml.p "wiseguys" end -end
\ No newline at end of file +end diff --git a/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder index d539a425a4..cd65da751b 100644 --- a/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder +++ b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder @@ -1,4 +1,4 @@ content_for :title do - 'Putting stuff in the title!' + "Putting stuff in the title!" end xml << "Great stuff!" diff --git a/actionview/test/fixtures/comments/empty.html.builder b/actionview/test/fixtures/comments/empty.html.builder index 2b0c7207a3..12d6fdd9a5 100644 --- a/actionview/test/fixtures/comments/empty.html.builder +++ b/actionview/test/fixtures/comments/empty.html.builder @@ -1 +1 @@ -xml.h1 'No Comment'
\ No newline at end of file +xml.h1 "No Comment" diff --git a/actionview/test/fixtures/test/hello.builder b/actionview/test/fixtures/test/hello.builder index a471553941..b8ab17ad5b 100644 --- a/actionview/test/fixtures/test/hello.builder +++ b/actionview/test/fixtures/test/hello.builder @@ -1,4 +1,4 @@ xml.html do xml.p "Hello #{@name}" - xml << render(:file => "test/greeting") -end
\ No newline at end of file + xml << render(file: "test/greeting") +end diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index a4a2966ff9..a09dd468dd 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -729,6 +729,28 @@ class FormWithActsLikeFormForTest < FormWithTest assert_dom_equal expected, output_buffer end + def test_form_is_not_remote_by_default_if_form_with_generates_remote_forms_is_false + old_value = ActionView::Helpers::FormHelper.form_with_generates_remote_forms + ActionView::Helpers::FormHelper.form_with_generates_remote_forms = false + + form_with(model: @post, url: "/", id: "create-post", method: :patch) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post", method: "patch", local: true) do + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + end + + assert_dom_equal expected, output_buffer + ensure + ActionView::Helpers::FormHelper.form_with_generates_remote_forms = old_value + end + def test_form_with_skip_enforcing_utf8_true form_with(scope: :post, skip_enforcing_utf8: true) do |f| concat f.text_field(:title) diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 09454b32cc..a6444a1686 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -231,7 +231,11 @@ class UrlHelperTest < ActiveSupport::TestCase end def to_h - { foo: :bar, baz: "quux" } + if permitted? + { foo: :bar, baz: "quux" } + else + raise ArgumentError + end end end diff --git a/actionview/test/ujs/public/test/call-remote.js b/actionview/test/ujs/public/test/call-remote.js index dbeb8ad832..5932195363 100644 --- a/actionview/test/ujs/public/test/call-remote.js +++ b/actionview/test/ujs/public/test/call-remote.js @@ -100,6 +100,34 @@ asyncTest('JS code should be executed', 1, function() { submit() }) +asyncTest('ecmascript code should be executed', 1, function() { + buildForm({ method: 'post', 'data-type': 'script' }) + + $('form').append('<input type="text" name="content_type" value="application/ecmascript">') + $('form').append('<input type="text" name="content" value="ok(true, \'remote code should be run\')">') + + submit() +}) + +asyncTest('execution of JS code does not modify current DOM', 1, function() { + var docLength, newDocLength + function getDocLength() { + return document.documentElement.outerHTML.length + } + + buildForm({ method: 'post', 'data-type': 'script' }) + + $('form').append('<input type="text" name="content_type" value="text/javascript">') + $('form').append('<input type="text" name="content" value="\'remote code should be run\'">') + + docLength = getDocLength() + + submit(function() { + newDocLength = getDocLength() + ok(docLength === newDocLength, 'executed JS should not present in the document') + }) +}) + asyncTest('XML document should be parsed', 1, function() { buildForm({ method: 'post', 'data-type': 'html' }) diff --git a/actionview/test/ujs/public/test/data-remote.js b/actionview/test/ujs/public/test/data-remote.js index b756add24e..161a92ac11 100644 --- a/actionview/test/ujs/public/test/data-remote.js +++ b/actionview/test/ujs/public/test/data-remote.js @@ -1,3 +1,17 @@ +(function() { + +function buildSelect(attrs) { + attrs = $.extend({ + 'name': 'user_data', 'data-remote': 'true', 'data-url': '/echo', 'data-params': 'data1=value1' + }, attrs) + + $('#qunit-fixture').append( + $('<select />', attrs) + .append($('<option />', {value: 'optionValue1', text: 'option1'})) + .append($('<option />', {value: 'optionValue2', text: 'option2'})) + ) +} + module('data-remote', { setup: function() { $('#qunit-fixture') @@ -135,17 +149,7 @@ asyncTest('clicking on a button with data-remote attribute', 5, function() { }) asyncTest('changing a select option with data-remote attribute', 5, function() { - $('form') - .append( - $('<select />', { - 'name': 'user_data', - 'data-remote': 'true', - 'data-params': 'data1=value1', - 'data-url': '/echo' - }) - .append($('<option />', {value: 'optionValue1', text: 'option1'})) - .append($('<option />', {value: 'optionValue2', text: 'option2'})) - ) + buildSelect() $('select[data-remote]') .bindNative('ajax:success', function(e, data, status, xhr) { @@ -350,17 +354,7 @@ asyncTest('submitting a form with falsy "data-remote" attribute', 0, function() }) asyncTest('changing a select option with falsy "data-remote" attribute', 0, function() { - $('form') - .append( - $('<select />', { - 'name': 'user_data', - 'data-remote': 'false', - 'data-params': 'data1=value1', - 'data-url': '/echo' - }) - .append($('<option />', {value: 'optionValue1', text: 'option1'})) - .append($('<option />', {value: 'optionValue2', text: 'option2'})) - ) + buildSelect({'data-remote': 'false'}) $('select[data-remote=false]:first') .bindNative('ajax:beforeSend', function() { @@ -413,3 +407,32 @@ asyncTest('form buttons should only be serialized when clicked', 4, function() { }) .find('[name=submit2]').triggerNative('click') }) + +asyncTest('changing a select option without "data-url" attribute still fires ajax request to current location', 1, function() { + var currentLocation, ajaxLocation + + buildSelect({'data-url': ''}); + + $('select[data-remote]') + .bindNative('ajax:beforeSend', function(e, xhr, settings) { + // Get current location (the same way jQuery does) + try { + currentLocation = location.href + } catch(err) { + currentLocation = document.createElement( 'a' ) + currentLocation.href = '' + currentLocation = currentLocation.href + } + + ajaxLocation = settings.url.replace(settings.data, '').replace(/&$/, '').replace(/\?$/, '') + equal(ajaxLocation, currentLocation, 'URL should be current page by default') + + return false + }) + .val('optionValue2') + .triggerNative('change') + + setTimeout(function() { start() }, 20) +}) + +})() diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 6b4f93df8b..ddfb926e02 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1 +1,7 @@ +* Change logging instrumentation to log errors when a job raises an exception. + + Fixes #26848. + + *Steven Bull* + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activejob/CHANGELOG.md) for previous changes. diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index d7e2cd03e3..f46d5c68a8 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -74,9 +74,16 @@ module ActiveJob end def perform(event) - info do - job = event.payload[:job] - "Performed #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms" + job = event.payload[:job] + ex = event.payload[:exception_object] + if ex + error do + "Error performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message}):\n" + Array(ex.backtrace).join("\n") + end + else + info do + "Performed #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms" + end end end diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index b37736f859..d5ca0f385c 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -5,6 +5,7 @@ require "jobs/hello_job" require "jobs/logging_job" require "jobs/overridden_logging_job" require "jobs/nested_job" +require "jobs/rescue_job" require "models/person" class LoggingTest < ActiveSupport::TestCase @@ -124,4 +125,11 @@ class LoggingTest < ActiveSupport::TestCase set_logger ::Logger.new(nil) OverriddenLoggingJob.perform_later "Dummy" end + + def test_job_error_logging + RescueJob.perform_later "other" + rescue RescueJob::OtherError + assert_match(/Performing RescueJob \(Job ID: .*?\) from .*? with arguments:.*other/, @logger.messages) + assert_match(/Error performing RescueJob \(Job ID: .*?\) from .*? in .*ms: RescueJob::OtherError \(Bad hair\):\n.*\brescue_job\.rb:\d+:in `perform'/, @logger.messages) + end end diff --git a/activejob/test/jobs/rescue_job.rb b/activejob/test/jobs/rescue_job.rb index ef8f777437..62add4271a 100644 --- a/activejob/test/jobs/rescue_job.rb +++ b/activejob/test/jobs/rescue_job.rb @@ -19,7 +19,7 @@ class RescueJob < ActiveJob::Base when "david" raise ArgumentError, "Hair too good" when "other" - raise OtherError + raise OtherError, "Bad hair" else JobBuffer.add("performed beautifully") end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 166c6ac21f..b5c0b43b61 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -472,5 +472,9 @@ module ActiveModel def missing_attribute(attr_name, stack) raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack end + + def _read_attribute(attr) + __send__(attr) + end end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 6e0af99ad7..dc81f74779 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -179,13 +179,13 @@ module ActiveModel # Handles <tt>*_changed?</tt> for +method_missing+. def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: !!changes_include?(attr) && - (to == OPTION_NOT_GIVEN || to == __send__(attr)) && + (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) && (from == OPTION_NOT_GIVEN || from == changed_attributes[attr]) end # Handles <tt>*_was</tt> for +method_missing+. def attribute_was(attr) # :nodoc: - attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) + attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) end # Handles <tt>*_previously_changed?</tt> for +method_missing+. @@ -226,7 +226,7 @@ module ActiveModel # Handles <tt>*_change</tt> for +method_missing+. def attribute_change(attr) - [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) + [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr) end # Handles <tt>*_previous_change</tt> for +method_missing+. @@ -239,7 +239,7 @@ module ActiveModel return if attribute_changed?(attr) begin - value = __send__(attr) + value = _read_attribute(attr) value = value.duplicable? ? value.clone : value rescue TypeError, NoMethodError end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 5d3472802b..942b4fa9bb 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -172,8 +172,8 @@ module ActiveModel # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]} # person.errors.values # => [["cannot be nil", "must be specified"]] def values - messages.reject do |key, value| - value.empty? + messages.select do |key, value| + !value.empty? end.values end @@ -182,8 +182,8 @@ module ActiveModel # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]} # person.errors.keys # => [:name] def keys - messages.reject do |key, value| - value.empty? + messages.select do |key, value| + !value.empty? end.keys end diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb index c7e0208a5a..850cab962b 100644 --- a/activemodel/lib/active_model/type/string.rb +++ b/activemodel/lib/active_model/type/string.rb @@ -12,7 +12,12 @@ module ActiveModel private def cast_value(value) - ::String.new(super) + case value + when ::String then ::String.new(value) + when true then "t".freeze + when false then "f".freeze + else value.to_s + end end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index b4b8d9f33c..98b3f6a8e5 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -1,4 +1,3 @@ - module ActiveModel module Validations class FormatValidator < EachValidator # :nodoc: diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 995b331245..b82c85ddf4 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -104,7 +104,7 @@ module ActiveModel module HelperMethods # Validates whether the value of the specified attribute is numeric by # trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt> - # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\Z/</tt> + # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\z/</tt> # (if <tt>only_integer</tt> is set to +true+). # # class Person < ActiveRecord::Base diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 6e8a434bbe..ce4106a5e1 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -1,4 +1,3 @@ - module ActiveModel module Validations class PresenceValidator < EachValidator # :nodoc: diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb index 222083817e..47d412e27e 100644 --- a/activemodel/test/cases/type/string_test.rb +++ b/activemodel/test/cases/type/string_test.rb @@ -12,16 +12,25 @@ module ActiveModel end test "cast strings are mutable" do - s = "foo" type = Type::String.new + + s = "foo" assert_equal false, type.cast(s).frozen? + assert_equal false, s.frozen? + + f = "foo".freeze + assert_equal false, type.cast(f).frozen? + assert_equal true, f.frozen? end test "values are duped coming out" do - s = "foo" type = Type::String.new + + s = "foo" assert_not_same s, type.cast(s) + assert_equal s, type.cast(s) assert_not_same s, type.deserialize(s) + assert_equal s, type.deserialize(s) end end end diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb index 9b74d83b37..f733c45cf0 100644 --- a/activemodel/test/validators/email_validator.rb +++ b/activemodel/test/validators/email_validator.rb @@ -1,4 +1,3 @@ - class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) record.errors[attribute] << (options[:message] || "is not an email") unless diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 30d580b9e3..a229b4eaa2 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1 +1,31 @@ +* Quote database name in db:create grant statement (when database_user does not have access to create the database). + + *Rune Philosof* + +* Raise error `UnknownMigrationVersionError` on the movement of migrations + when the current migration does not exist. + + *bogdanvlviv* + +* Fix `bin/rails db:forward` first migration. + + *bogdanvlviv* + +* Support Descending Indexes for MySQL. + + MySQL 8.0.1 and higher supports descending indexes: `DESC` in an index definition is no longer ignored. + See https://dev.mysql.com/doc/refman/8.0/en/descending-indexes.html. + + *Ryuta Kamizono* + +* Fix inconsistency with changed attributes when overriding AR attribute reader. + + *bogdanvlviv* + +* When calling the dynamic fixture accessor method with no arguments it now returns all fixtures of this type. + Previously this method always returned an empty array. + + *Kevin McPhillips* + + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6efa448d49..e52c2004f3 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1649,7 +1649,7 @@ module ActiveRecord # you don't want to have association presence validated, use <tt>optional: true</tt>. # [:default] # Provide a callable (i.e. proc or lambda) to specify that the association should - # be initialized with a particular record before validation. + # be initialized with a particular record before validation. # # Option examples: # belongs_to :firm, foreign_key: "client_of" diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 77282e6463..62c944fce3 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -280,35 +280,6 @@ module ActiveRecord replace_on_target(record, index, skip_callbacks, &block) end - def replace_on_target(record, index, skip_callbacks) - callback(:before_add, record) unless skip_callbacks - - begin - if index - record_was = target[index] - target[index] = record - else - target << record - end - - set_inverse_instance(record) - - yield(record) if block_given? - rescue - if index - target[index] = record_was - else - target.delete(record) - end - - raise - end - - callback(:after_add, record) unless skip_callbacks - - record - end - def scope scope = super scope.none! if null_scope? @@ -385,15 +356,19 @@ module ActiveRecord transaction do add_to_target(build_record(attributes)) do |record| yield(record) if block_given? - insert_record(record, true, raise) + insert_record(record, true, raise) { @_was_loaded = loaded? } end end end end # Do the relevant stuff to insert the given record into the association collection. - def insert_record(record, validate = true, raise = false) - raise NotImplementedError + def insert_record(record, validate = true, raise = false, &block) + if raise + record.save!(validate: validate, &block) + else + record.save(validate: validate, &block) + end end def create_scope @@ -448,19 +423,41 @@ module ActiveRecord end end - def concat_records(records, should_raise = false) + def concat_records(records, raise = false) result = true records.each do |record| raise_on_type_mismatch!(record) - add_to_target(record) do |rec| - result &&= insert_record(rec, true, should_raise) unless owner.new_record? + add_to_target(record) do + result &&= insert_record(record, true, raise) { @_was_loaded = loaded? } unless owner.new_record? end end result && records end + def replace_on_target(record, index, skip_callbacks) + callback(:before_add, record) unless skip_callbacks + + set_inverse_instance(record) + + @_was_loaded = true + + yield(record) if block_given? + + if index + target[index] = record + elsif @_was_loaded || !loaded? + target << record + end + + callback(:after_add, record) unless skip_callbacks + + record + ensure + @_was_loaded = nil + end + def callback(method, record) callbacks_for(method).each do |callback| callback.call(method, owner, record) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index b413eb3f9c..10ca0e47ff 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -31,12 +31,7 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) set_owner_attributes(record) - - if raise - record.save!(validate: validate) - else - record.save(validate: validate) - end + super end def empty? diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index c4a7fe4432..53ffb3b68d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -39,11 +39,7 @@ module ActiveRecord ensure_not_nested if record.new_record? || record.has_changes_to_save? - if raise - record.save!(validate: validate) - else - return unless record.save(validate: validate) - end + return unless super end save_through_record(record) diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 6bccbc06cd..607c54e481 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -181,6 +181,7 @@ module ActiveRecord if reflection.collection? before_save :before_save_collection_association + after_save :after_save_collection_association define_non_cyclic_method(save_method) { save_collection_association(reflection) } # Doesn't use after_save as that would save associations added in after_create/after_update twice @@ -371,6 +372,10 @@ module ActiveRecord true end + def after_save_collection_association + @new_record_before_save = false + end + # Saves any new associated records, or all loaded autosave associations if # <tt>:autosave</tt> is enabled on the association. # diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 43784b70e3..1e1de1863a 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -7,17 +7,27 @@ module ActiveRecord if collection.loaded? size = collection.size if size > 0 - timestamp = collection.max_by(×tamp_column).public_send(timestamp_column) + timestamp = collection.max_by(×tamp_column)._read_attribute(timestamp_column) end else column_type = type_for_attribute(timestamp_column.to_s) column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}" + select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" - query = collection - .unscope(:select) - .select("COUNT(*) AS #{connection.quote_column_name("size")}", "MAX(#{column}) AS timestamp") - .unscope(:order) - result = connection.select_one(query) + if collection.limit_value || collection.offset_value + query = collection.spawn + query.select_values = [column] + subquery_alias = "subquery_for_cache_key" + subquery_column = "#{subquery_alias}.#{timestamp_column}" + subquery = query.arel.as(subquery_alias) + arel = Arel::SelectManager.new(query.engine).project(select_values % subquery_column).from(subquery) + else + query = collection.unscope(:order) + query.select_values = [select_values % column] + arel = query.arel + end + + result = connection.select_one(arel, nil, query.bound_attributes) if result.blank? size = 0 diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 53dbbd8c21..177b253645 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -857,9 +857,9 @@ module ActiveRecord # All Active Record models use this handler to determine the connection pool that they # should use. # - # The ConnectionHandler class is not coupled with the Active models, as it has no knowlodge + # The ConnectionHandler class is not coupled with the Active models, as it has no knowledge # about the model. The model needs to pass a specification name to the handler, - # in order to lookup the correct connection pool. + # in order to look up the correct connection pool. class ConnectionHandler def initialize # These caches are keyed by spec.name (ConnectionSpecification#name). diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 4682afc188..46d7f84efd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -2,8 +2,33 @@ module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database - # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - IndexDefinition = Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc: + # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes + class IndexDefinition # :nodoc: + attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment + + def initialize( + table, name, + unique = false, + columns = [], + lengths: {}, + orders: {}, + where: nil, + type: nil, + using: nil, + comment: nil + ) + @table = table + @name = name + @unique = unique + @columns = columns + @lengths = lengths + @orders = orders + @where = where + @type = type + @using = using + @comment = comment + end + end # Abstract representation of a column definition. Instances of this type # are typically created by methods in TableDefinition, and added to the diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 6bb072dd73..19b7821494 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -149,57 +149,67 @@ module ActiveRecord end def begin_transaction(options = {}) - run_commit_callbacks = !current_transaction.joinable? - transaction = - if @stack.empty? - RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) - else - SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, - run_commit_callbacks: run_commit_callbacks) - end + @connection.lock.synchronize do + run_commit_callbacks = !current_transaction.joinable? + transaction = + if @stack.empty? + RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) + else + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, + run_commit_callbacks: run_commit_callbacks) + end - @stack.push(transaction) - transaction + @stack.push(transaction) + transaction + end end def commit_transaction - transaction = @stack.last + @connection.lock.synchronize do + transaction = @stack.last - begin - transaction.before_commit_records - ensure - @stack.pop - end + begin + transaction.before_commit_records + ensure + @stack.pop + end - transaction.commit - transaction.commit_records + transaction.commit + transaction.commit_records + end end def rollback_transaction(transaction = nil) - transaction ||= @stack.pop - transaction.rollback - transaction.rollback_records + @connection.lock.synchronize do + transaction ||= @stack.pop + transaction.rollback + transaction.rollback_records + end end def within_new_transaction(options = {}) - transaction = begin_transaction options - yield - rescue Exception => error - if transaction - rollback_transaction - after_failure_actions(transaction, error) - end - raise - ensure - unless error - if Thread.current.status == "aborting" - rollback_transaction if transaction - else - begin - commit_transaction - rescue Exception - rollback_transaction(transaction) unless transaction.state.completed? - raise + @connection.lock.synchronize do + begin + transaction = begin_transaction options + yield + rescue Exception => error + if transaction + rollback_transaction + after_failure_actions(transaction, error) + end + raise + ensure + unless error + if Thread.current.status == "aborting" + rollback_transaction if transaction + else + begin + commit_transaction + rescue Exception + rollback_transaction(transaction) unless transaction.state.completed? + raise + end + end 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 96083e6519..85d6fbe8b3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -74,7 +74,7 @@ module ActiveRecord SIMPLE_INT = /\A\d+\z/ attr_accessor :visitor, :pool - attr_reader :schema_cache, :owner, :logger, :prepared_statements + attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock alias :in_use? :owner def self.type_cast_config_to_integer(config) 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 f118e086bb..c742799aab 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -48,9 +48,6 @@ module ActiveRecord json: { name: "json" }, } - INDEX_TYPES = [:fulltext, :spatial] - INDEX_USINGS = [:btree, :hash] - class StatementPool < ConnectionAdapters::StatementPool private def dealloc(stmt) stmt[:stmt].close @@ -67,14 +64,6 @@ module ActiveRecord end end - CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] - - def internal_string_options_for_primary_key # :nodoc: - super.tap { |options| - options[:collation] = collation.sub(/\A[^_]+/, "utf8") if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) - } - end - def version #:nodoc: @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) end @@ -93,10 +82,8 @@ module ActiveRecord true end - # Technically MySQL allows to create indexes with the sort order syntax - # but at the moment (5.5) it doesn't yet implement them def supports_index_sort_order? - true + !mariadb? && version >= "8.0.1" end def supports_transaction_isolation? @@ -304,36 +291,6 @@ module ActiveRecord execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing name to #indexes is deprecated without replacement. - MSG - end - - indexes = [] - current_index = nil - execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| - each_hash(result) do |row| - if current_index != row[:Key_name] - next if row[:Key_name] == "PRIMARY" # skip the primary key - current_index = row[:Key_name] - - mysql_index_type = row[:Index_type].downcase.to_sym - index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil - index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], {}, nil, nil, index_type, index_using, row[:Index_comment].presence) - end - - indexes.last.columns << row[:Column_name] - indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] - end - end - - indexes - end - def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name) diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index 3e0afd9761..e2ba0ba1a0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -55,13 +55,14 @@ module ActiveRecord def extract_expression_for_virtual_column(column) if mariadb? create_table_info = create_table_info(column.table_name) - if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)} AS \((?<expression>.+?)\) #{column.extra}/m =~ create_table_info + if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info $~[:expression].inspect end else + scope = quoted_scope(column.table_name) sql = "SELECT generation_expression FROM information_schema.columns" \ - " WHERE table_schema = #{quote(@config[:database])}" \ - " AND table_name = #{quote(column.table_name)}" \ + " WHERE table_schema = #{scope[:schema]}" \ + " AND table_name = #{scope[:name]}" \ " AND column_name = #{quote(column.name)}" select_value(sql, "SCHEMA").inspect end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index 9e2d0fb5e7..f9e1e046ea 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -2,7 +2,60 @@ module ActiveRecord module ConnectionAdapters module MySQL module SchemaStatements # :nodoc: + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + + indexes = [] + current_index = nil + execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| + each_hash(result) do |row| + if current_index != row[:Key_name] + next if row[:Key_name] == "PRIMARY" # skip the primary key + current_index = row[:Key_name] + + mysql_index_type = row[:Index_type].downcase.to_sym + case mysql_index_type + when :fulltext, :spatial + index_type = mysql_index_type + when :btree, :hash + index_using = mysql_index_type + end + + indexes << IndexDefinition.new( + row[:Table], + row[:Key_name], + row[:Non_unique].to_i == 0, + type: index_type, + using: index_using, + comment: row[:Index_comment].presence + ) + end + + indexes.last.columns << row[:Column_name] + indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] + indexes.last.orders.merge!(row[:Column_name] => :desc) if row[:Collation] == "D" + end + end + + indexes + end + + def internal_string_options_for_primary_key + super.tap do |options| + if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) && (mariadb? || version < "8.0.0") + options[:collation] = collation.sub(/\A[^_]+/, "utf8") + end + end + end + private + CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] + def schema_creation MySQL::SchemaCreation.new(self) 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 1d439acb07..5b483ad4ab 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -140,8 +140,17 @@ module ActiveRecord ] end - IndexDefinition.new(table_name, index_name, unique, columns, [], orders, where, nil, using.to_sym, comment.presence) - end.compact + IndexDefinition.new( + table_name, + index_name, + unique, + columns, + orders: orders, + where: where, + using: using.to_sym, + comment: comment.presence + ) + end end def table_options(table_name) # :nodoc: @@ -344,13 +353,17 @@ module ActiveRecord def primary_keys(table_name) # :nodoc: select_values(<<-SQL.strip_heredoc, "SCHEMA") - SELECT a.attname FROM pg_index i - CROSS JOIN generate_subscripts(i.indkey, 1) k - JOIN pg_attribute a - ON a.attrelid = i.indrelid - AND a.attnum = i.indkey[k] - WHERE i.indrelid = #{quote(quote_table_name(table_name))}::regclass - AND i.indisprimary + SELECT a.attname + FROM ( + SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx + FROM pg_index + WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass + AND indisprimary + ) i + JOIN pg_attribute a + ON a.attrelid = i.indrelid + AND a.attnum = i.indkey[i.idx] + ORDER BY i.idx SQL end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 0ad114165e..7ae9bd9a5f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -239,7 +239,9 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.query "SELECT 1" + @lock.synchronize do + @connection.query "SELECT 1" + end true rescue PG::Error false @@ -247,26 +249,32 @@ module ActiveRecord # Close then reopen the connection. def reconnect! - super - @connection.reset - configure_connection + @lock.synchronize do + super + @connection.reset + configure_connection + end end def reset! - clear_cache! - reset_transaction - unless @connection.transaction_status == ::PG::PQTRANS_IDLE - @connection.query "ROLLBACK" + @lock.synchronize do + clear_cache! + reset_transaction + unless @connection.transaction_status == ::PG::PQTRANS_IDLE + @connection.query "ROLLBACK" + end + @connection.query "DISCARD ALL" + configure_connection end - @connection.query "DISCARD ALL" - configure_connection end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - super - @connection.close rescue nil + @lock.synchronize do + super + @connection.close rescue nil + end end def native_database_types #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb index 8066a05c5e..e02491edb6 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -2,6 +2,41 @@ module ActiveRecord module ConnectionAdapters module SQLite3 module SchemaStatements # :nodoc: + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| + index_sql = select_value(<<-SQL, "SCHEMA") + SELECT sql + FROM sqlite_master + WHERE name = #{quote(row['name'])} AND type = 'index' + UNION ALL + SELECT sql + FROM sqlite_temp_master + WHERE name = #{quote(row['name'])} AND type = 'index' + SQL + + /\sWHERE\s+(?<where>.+)$/i =~ index_sql + + columns = exec_query("PRAGMA index_info(#{quote(row['name'])})", "SCHEMA").map do |col| + col["name"] + end + + IndexDefinition.new( + table_name, + row["name"], + row["unique"] != 0, + columns, + where: where + ) + end + end + private def schema_creation SQLite3::SchemaCreation.new(self) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index c54b88f7d1..e2c05ccc4e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -259,37 +259,6 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing name to #indexes is deprecated without replacement. - MSG - end - - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| - sql = <<-SQL - SELECT sql - FROM sqlite_master - WHERE name=#{quote(row['name'])} AND type='index' - UNION ALL - SELECT sql - FROM sqlite_temp_master - WHERE name=#{quote(row['name'])} AND type='index' - SQL - index_sql = exec_query(sql).first["sql"] - match = /\sWHERE\s+(.+)$/i.match(index_sql) - where = match[1] if match - IndexDefinition.new( - table_name, - row["name"], - row["unique"] != 0, - exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| - col["name"] - }, nil, nil, where) - end - end - def primary_keys(table_name) # :nodoc: pks = table_structure(table_name).select { |f| f["pk"] > 0 } pks.sort_by { |f| f["pk"] }.map { |f| f["name"] } diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 08d42f3dd4..d9912cfb22 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,4 +1,3 @@ - module ActiveRecord module DynamicMatchers #:nodoc: def respond_to_missing?(name, include_private = false) diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index e79167d568..c19216702c 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -70,13 +70,32 @@ module ActiveRecord # test. To ensure consistent data, the environment deletes the fixtures before running the load. # # In addition to being available in the database, the fixture's data may also be accessed by - # using a special dynamic method, which has the same name as the model, and accepts the - # name of the fixture to instantiate: + # using a special dynamic method, which has the same name as the model. # - # test "find" do + # Passing in a fixture name to this dynamic method returns the fixture matching this name: + # + # test "find one" do # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name # end # + # Passing in multiple fixture names returns all fixtures matching these names: + # + # test "find all by name" do + # assert_equal 2, web_sites(:rubyonrails, :google).length + # end + # + # Passing in no arguments returns all fixtures: + # + # test "find all" do + # assert_equal 2, web_sites.length + # end + # + # Passing in any fixture name that does not exist will raise <tt>StandardError</tt>: + # + # test "find by name that does not exist" do + # assert_raise(StandardError) { web_sites(:reddit) } + # end + # # Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the # following tests: # @@ -909,6 +928,8 @@ module ActiveRecord define_method(accessor_name) do |*fixture_names| force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload + return_single_record = fixture_names.size == 1 + fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty? @fixture_cache[fs_name] ||= {} @@ -923,7 +944,7 @@ module ActiveRecord end end - instances.size == 1 ? instances.first : instances + return_single_record ? instances.first : instances end private accessor_name end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 4e1df1432c..51c82f4ced 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1104,13 +1104,21 @@ module ActiveRecord def move(direction, migrations_paths, steps) migrator = new(direction, migrations(migrations_paths)) - start_index = migrator.migrations.index(migrator.current_migration) - if start_index - finish = migrator.migrations[start_index + steps] - version = finish ? finish.version : 0 - send(direction, migrations_paths, version) + if current_version != 0 && !migrator.current_migration + raise UnknownMigrationVersionError.new(current_version) end + + start_index = + if current_version == 0 + 0 + else + migrator.migrations.index(migrator.current_migration) + end + + finish = migrator.migrations[start_index + steps] + version = finish ? finish.version : 0 + send(direction, migrations_paths, version) end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 7ceb7d1a55..f652c7c3a1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -100,6 +100,10 @@ module ActiveRecord !(@new_record || @destroyed) end + ## + # :call-seq: + # save(*args) + # # Saves the model. # # If the model is new, a record gets created in the database, otherwise @@ -121,12 +125,16 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save(*args) - create_or_update(*args) + def save(*args, &block) + create_or_update(*args, &block) rescue ActiveRecord::RecordInvalid false end + ## + # :call-seq: + # save!(*args) + # # Saves the model. # # If the model is new, a record gets created in the database, otherwise @@ -150,8 +158,8 @@ module ActiveRecord # being updated. # # Unless an error is raised, returns true. - def save!(*args) - create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self)) + def save!(*args, &block) + create_or_update(*args, &block) || raise(RecordNotSaved.new("Failed to save the record", self)) end # Deletes the record in the database and freezes this instance to @@ -550,9 +558,9 @@ module ActiveRecord self.class.unscoped.where(self.class.primary_key => id) end - def create_or_update(*args) + def create_or_update(*args, &block) _raise_readonly_record_error if readonly? - result = new_record? ? _create_record : _update_record(*args) + result = new_record? ? _create_record(&block) : _update_record(*args, &block) result != false end @@ -567,6 +575,9 @@ module ActiveRecord rows_affected = self.class.unscoped._update_record attributes_values, id, id_in_database @_trigger_update_callback = rows_affected > 0 end + + yield(self) if block_given? + rows_affected end @@ -579,6 +590,9 @@ module ActiveRecord self.id ||= new_id if self.class.primary_key @new_record = false + + yield(self) if block_given? + id end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 5d24f5f5ca..a1459c87c6 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -312,16 +312,7 @@ module ActiveRecord relation = apply_join_dependency(self, construct_join_dependency(eager_loading: false)) return false if ActiveRecord::NullRelation === relation - relation = relation.except(:select, :distinct).select(ONE_AS_ONE).limit(1) - - case conditions - when Array, Hash - relation = relation.where(conditions) - else - unless conditions == :none - relation = relation.where(primary_key => conditions) - end - end + relation = construct_relation_for_exists(relation, conditions) connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false rescue ::RangeError @@ -391,6 +382,19 @@ module ActiveRecord end end + def construct_relation_for_exists(relation, conditions) + relation = relation.except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) + + case conditions + when Array, Hash + relation.where!(conditions) + else + relation.where!(primary_key => conditions) unless conditions == :none + end + + relation + end + def construct_join_dependency(joins = [], eager_loading: true) including = eager_load_values + includes_values ActiveRecord::Associations::JoinDependency.new(@klass, including, joins, eager_loading: eager_loading) @@ -401,8 +405,7 @@ module ActiveRecord end def apply_join_dependency(relation, join_dependency) - relation = relation.except(:includes, :eager_load, :preload) - relation = relation.joins join_dependency + relation = relation.except(:includes, :eager_load, :preload).joins!(join_dependency) if using_limitable_reflections?(join_dependency.reflections) relation diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index fca914aedd..183fe91c05 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,13 +1,14 @@ +require "active_record/relation/predicate_builder/array_handler" +require "active_record/relation/predicate_builder/base_handler" +require "active_record/relation/predicate_builder/basic_object_handler" +require "active_record/relation/predicate_builder/range_handler" +require "active_record/relation/predicate_builder/relation_handler" + +require "active_record/relation/predicate_builder/association_query_value" +require "active_record/relation/predicate_builder/polymorphic_array_value" + module ActiveRecord class PredicateBuilder # :nodoc: - require "active_record/relation/predicate_builder/array_handler" - require "active_record/relation/predicate_builder/association_query_handler" - require "active_record/relation/predicate_builder/base_handler" - require "active_record/relation/predicate_builder/basic_object_handler" - require "active_record/relation/predicate_builder/polymorphic_array_handler" - require "active_record/relation/predicate_builder/range_handler" - require "active_record/relation/predicate_builder/relation_handler" - delegate :resolve_column_aliases, to: :table def initialize(table) @@ -20,8 +21,6 @@ module ActiveRecord register_handler(RangeHandler::RangeWithBinds, RangeHandler.new) register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) - register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) - register_handler(PolymorphicArrayValue, PolymorphicArrayHandler.new(self)) end def build_from_hash(attributes) @@ -87,7 +86,6 @@ module ActiveRecord binds = [] attributes.each do |column_name, value| - binds.concat(value.bound_attributes) if value.is_a?(Relation) case when value.is_a?(Hash) && !table.has_column?(column_name) attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value) @@ -99,7 +97,21 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - result[column_name] = AssociationQueryHandler.value_for(table, column_name, value) + associated_table = table.associated_table(column_name) + if associated_table.polymorphic_association? + case value.is_a?(Array) ? value.first : value + when Base, Relation + value = [value] unless value.is_a?(Array) + klass = PolymorphicArrayValue + end + end + + klass ||= AssociationQueryValue + result[column_name] = klass.new(associated_table, value).queries.map do |query| + attrs, bvs = create_binds_for_hash(query) + binds.concat(bvs) + attrs + end when value.is_a?(Range) && !table.type(column_name).respond_to?(:subtype) first = value.begin last = value.end @@ -117,6 +129,8 @@ module ActiveRecord if can_be_bound?(column_name, value) result[column_name] = Arel::Nodes::BindParam.new binds << build_bind_param(column_name, value) + elsif value.is_a?(Relation) + binds.concat(value.bound_attributes) end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 88b6c37d43..1068e700e2 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -6,11 +6,11 @@ module ActiveRecord end def call(attribute, value) + return attribute.in([]) if value.empty? + return queries_predicates(value) if value.all? { |v| v.is_a?(Hash) } + values = value.map { |x| x.is_a?(Base) ? x.id : x } nils, values = values.partition(&:nil?) - - return attribute.in([]) if values.empty? && nils.empty? - ranges, values = values.partition { |v| v.is_a?(Range) } values_predicate = @@ -26,7 +26,7 @@ module ActiveRecord array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) } array_predicates.unshift(values_predicate) - array_predicates.inject { |composite, predicate| composite.or(predicate) } + array_predicates.inject(&:or) end # TODO Change this to private once we've dropped Ruby 2.2 support. @@ -40,6 +40,17 @@ module ActiveRecord other end end + + private + def queries_predicates(queries) + if queries.size > 1 + queries.map do |query| + Arel::Nodes::And.new(predicate_builder.build_from_hash(query)) + end.inject(&:or) + else + predicate_builder.build_from_hash(queries.first) + end + end end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb deleted file mode 100644 index 29860ec677..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ /dev/null @@ -1,88 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class AssociationQueryHandler # :nodoc: - def self.value_for(table, column, value) - associated_table = table.associated_table(column) - klass = if associated_table.polymorphic_association? && ::Array === value && value.first.is_a?(Base) - PolymorphicArrayValue - else - AssociationQueryValue - end - - klass.new(associated_table, value) - end - - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - queries = {} - - table = value.associated_table - if value.base_class - queries[table.association_foreign_type.to_s] = value.base_class.name - end - - queries[table.association_foreign_key.to_s] = value.ids - predicate_builder.build_from_hash(queries) - end - - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :predicate_builder - end - - class AssociationQueryValue # :nodoc: - attr_reader :associated_table, :value - - def initialize(associated_table, value) - @associated_table = associated_table - @value = value - end - - def ids - case value - when Relation - value.select(primary_key) - when Array - value.map { |v| convert_to_id(v) } - else - convert_to_id(value) - end - end - - def base_class - if associated_table.polymorphic_association? - @base_class ||= polymorphic_base_class_from_value - end - end - - private - - def primary_key - associated_table.association_primary_key(base_class) - end - - def polymorphic_base_class_from_value - case value - when Relation - value.klass.base_class - when Base - value.class.base_class - end - end - - def convert_to_id(value) - case value - when Base - value._read_attribute(primary_key) - else - value - end - end - end - end -end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb new file mode 100644 index 0000000000..2fe0f81cab --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -0,0 +1,41 @@ +module ActiveRecord + class PredicateBuilder + class AssociationQueryValue # :nodoc: + attr_reader :associated_table, :value + + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def queries + [associated_table.association_foreign_key.to_s => ids] + end + + private + def ids + case value + when Relation + value.select_values.empty? ? value.select(primary_key) : value + when Array + value.map { |v| convert_to_id(v) } + else + convert_to_id(value) + end + end + + def primary_key + associated_table.association_primary_key + end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key) + else + value + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb deleted file mode 100644 index 335124c952..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb +++ /dev/null @@ -1,59 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class PolymorphicArrayHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - table = value.associated_table - queries = value.type_to_ids_mapping.map do |type, ids| - { table.association_foreign_type.to_s => type, table.association_foreign_key.to_s => ids } - end - - predicates = queries.map { |query| predicate_builder.build_from_hash(query) } - - if predicates.size > 1 - type_and_ids_predicates = predicates.map { |type_predicate, id_predicate| Arel::Nodes::Grouping.new(type_predicate.and(id_predicate)) } - type_and_ids_predicates.inject(&:or) - else - predicates.first - end - end - - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :predicate_builder - end - - class PolymorphicArrayValue # :nodoc: - attr_reader :associated_table, :values - - def initialize(associated_table, values) - @associated_table = associated_table - @values = values - end - - def type_to_ids_mapping - default_hash = Hash.new { |hsh, key| hsh[key] = [] } - values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } - end - - private - - def primary_key(value) - associated_table.association_primary_key(base_class(value)) - end - - def base_class(value) - value.class.base_class - end - - def convert_to_id(value) - value._read_attribute(primary_key(value)) - end - end - end -end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb new file mode 100644 index 0000000000..9bb2f8c8dc --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -0,0 +1,49 @@ +module ActiveRecord + class PredicateBuilder + class PolymorphicArrayValue # :nodoc: + attr_reader :associated_table, :values + + def initialize(associated_table, values) + @associated_table = associated_table + @values = values + end + + def queries + type_to_ids_mapping.map do |type, ids| + { + associated_table.association_foreign_type.to_s => type, + associated_table.association_foreign_key.to_s => ids.size > 1 ? ids : ids.first + } + end + end + + private + def type_to_ids_mapping + default_hash = Hash.new { |hsh, key| hsh[key] = [] } + values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } + end + + def primary_key(value) + associated_table.association_primary_key(base_class(value)) + end + + def base_class(value) + case value + when Base + value.class.base_class + when Relation + value.klass.base_class + end + end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key(value)) + when Relation + value.select(primary_key(value)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 2bbfd01698..94d63765c9 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -47,9 +47,18 @@ module ActiveRecord @options = options end - def header(stream) - define_params = @version ? "version: #{@version}" : "" + # turns 20170404131909 into "2017_04_04_131909" + def formatted_version + stringified = @version.to_s + return stringified unless stringified.length == 14 + stringified.insert(4, "_").insert(7, "_").insert(10, "_") + end + def define_params + @version ? "version: #{formatted_version}" : "" + end + + def header(stream) stream.puts <<HEADER # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 920830b9cf..c05f0a8fbb 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -104,7 +104,7 @@ module ActiveRecord def grant_statement <<-SQL -GRANT ALL PRIVILEGES ON #{configuration['database']}.* +GRANT ALL PRIVILEGES ON `#{configuration['database']}`.* TO '#{configuration['username']}'@'localhost' IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; SQL diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 2fa39282fb..58698d59db 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -38,7 +38,7 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase assert_equal :string, string_column.type end - test "test type casting with emulated booleans" do + test "type casting with emulated booleans" do emulate_booleans true boolean = BooleanType.create!(archived: true, published: true) @@ -55,7 +55,7 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase assert_equal 0, @connection.type_cast(false) end - test "test type casting without emulated booleans" do + test "type casting without emulated booleans" do emulate_booleans false boolean = BooleanType.create!(archived: true, published: true) diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 605baa9905..251a50e41e 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -18,7 +18,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase ActiveRecord::SchemaMigration.create_table - assert connection.column_exists?(table_name, :version, :string, collation: "utf8_general_ci") + assert connection.column_exists?(table_name, :version, :string) end end @@ -29,7 +29,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase ActiveRecord::InternalMetadata.create_table - assert connection.column_exists?(table_name, :key, :string, collation: "utf8_general_ci") + assert connection.column_exists?(table_name, :key, :string) end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 6aa6a79705..52e4a38cae 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -13,6 +13,10 @@ module PostgresqlUUIDHelper def uuid_function connection.supports_pgcrypto_uuid? ? "gen_random_uuid()" : "uuid_generate_v4()" end + + def uuid_default + connection.supports_pgcrypto_uuid? ? {} : { default: uuid_function } + end end class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase @@ -178,7 +182,7 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase t.uuid "other_uuid_2", default: "my_uuid_generator()" end - connection.create_table("pg_uuids_3", id: :uuid) do |t| + connection.create_table("pg_uuids_3", id: :uuid, **uuid_default) do |t| t.string "name" end end @@ -320,10 +324,10 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase setup do connection.transaction do - connection.create_table("pg_uuid_posts", id: :uuid) do |t| + connection.create_table("pg_uuid_posts", id: :uuid, **uuid_default) do |t| t.string "title" end - connection.create_table("pg_uuid_comments", id: :uuid) do |t| + connection.create_table("pg_uuid_comments", id: :uuid, **uuid_default) do |t| t.references :uuid_post, type: :uuid t.string "content" end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index d6b595d7e7..8060790594 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -111,6 +111,21 @@ class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base association_foreign_key: "developer_id" end +class Kitchen < ActiveRecord::Base + has_one :sink +end + +class Sink < ActiveRecord::Base + has_and_belongs_to_many :sources, join_table: :edges + belongs_to :kitchen + accepts_nested_attributes_for :kitchen +end + +class Source < ActiveRecord::Base + self.table_name = "men" + has_and_belongs_to_many :sinks, join_table: :edges +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers @@ -1021,4 +1036,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase ActiveRecord::Base.partial_writes = original_partial_writes end end + + def test_has_and_belongs_to_many_with_belongs_to + sink = Sink.create! kitchen: Kitchen.new, sources: [Source.new] + assert_equal 1, sink.sources.count + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index e2f044c139..25133fc580 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -2447,15 +2447,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [first_bulb, second_bulb], car.bulbs end - test "double insertion of new object to association when same association used in the after create callback of a new object" do + test "prevent double insertion of new object when the parent association loaded in the after save callback" do reset_callbacks(:save, Bulb) do Bulb.after_save { |record| record.car.bulbs.load } + car = Car.create! car.bulbs << Bulb.new + assert_equal 1, car.bulbs.size end end + test "prevent double firing the before save callback of new object when the parent association saved in the callback" do + reset_callbacks(:save, Bulb) do + count = 0 + Bulb.before_save { |record| record.car.save && count += 1 } + + car = Car.create! + car.bulbs.create! + + assert_equal 1, count + end + end + class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base self.table_name = "authors" has_many :posts_with_error_destroying, @@ -2496,7 +2510,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_loading_association_in_validate_callback_doesnt_affect_persistence reset_callbacks(:validation, Bulb) do - Bulb.after_validation { |m| m.car.bulbs.load } + Bulb.after_validation { |record| record.car.bulbs.load } car = Car.create!(name: "Car") bulb = car.bulbs.create! diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index ee1be09358..15c253890b 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -703,6 +703,17 @@ class BasicsTest < ActiveRecord::TestCase assert_nil topic.bonus_time end + def test_attributes + category = Category.new(name: "Ruby") + + expected_attributes = category.attribute_names.map do |attribute_name| + [attribute_name, category.public_send(attribute_name)] + end.to_h + + assert_instance_of Hash, category.attributes + assert_equal expected_attributes, category.attributes + end + def test_boolean b_nil = Boolean.create("value" => nil) nil_id = b_nil.id diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index bb2829b3c1..2c6a38ec35 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -15,7 +15,7 @@ module ActiveRecord @connection.drop_table :cache_mes, if_exists: true end - test "test_cache_key_format_is_not_too_precise" do + test "cache_key format is not too precise" do record = CacheMe.create key = record.cache_key diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb index 59ef389326..a26a72712d 100644 --- a/activerecord/test/cases/coders/yaml_column_test.rb +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -1,4 +1,3 @@ - require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index 381a78a8e2..92620761e4 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -15,8 +15,34 @@ module ActiveRecord end test "cache_key for relation" do - developers = Developer.where(name: "David") - last_developer_timestamp = developers.order(updated_at: :desc).first.updated_at + developers = Developer.where(salary: 100000).order(updated_at: :desc) + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key + + assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + + test "cache_key for relation with limit" do + developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5) + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key + + assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + + test "cache_key for loaded relation" do + developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load + last_developer_timestamp = developers.first.updated_at assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb index 63f67a9a16..c23be52a6c 100644 --- a/activerecord/test/cases/comment_test.rb +++ b/activerecord/test/cases/comment_test.rb @@ -72,9 +72,11 @@ if ActiveRecord::Base.connection.supports_comments? end def test_add_index_with_comment_later - @connection.add_index :commenteds, :obvious, name: "idx_obvious", comment: "We need to see obvious comments" - index = @connection.indexes("commenteds").find { |idef| idef.name == "idx_obvious" } - assert_equal "We need to see obvious comments", index.comment + unless current_adapter?(:OracleAdapter) + @connection.add_index :commenteds, :obvious, name: "idx_obvious", comment: "We need to see obvious comments" + index = @connection.indexes("commenteds").find { |idef| idef.name == "idx_obvious" } + assert_equal "We need to see obvious comments", index.comment + end end def test_add_comment_to_column @@ -112,8 +114,10 @@ if ActiveRecord::Base.connection.supports_comments? assert_match %r[t\.string\s+"obvious"\n], output assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output - assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output - assert_match %r[t\.index\s+.+\s+name: "idx_obvious",\s+comment: "We need to see obvious comments"], output + unless current_adapter?(:OracleAdapter) + assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output + assert_match %r[t\.index\s+.+\s+name: "idx_obvious",\s+comment: "We need to see obvious comments"], output + end end def test_schema_dump_omits_blank_comments diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index f9eccfbda1..721861975a 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -671,6 +671,47 @@ class DirtyTest < ActiveRecord::TestCase assert binary.changed? end + test "changes is correct for subclass" do + foo = Class.new(Pirate) do + def catchphrase + super.upcase + end + end + + pirate = foo.create!(catchphrase: "arrrr") + + new_catchphrase = "arrrr matey!" + + pirate.catchphrase = new_catchphrase + assert pirate.catchphrase_changed? + + expected_changes = { + "catchphrase" => ["arrrr", new_catchphrase] + } + + assert_equal new_catchphrase.upcase, pirate.catchphrase + assert_equal expected_changes, pirate.changes + end + + test "changes is correct if override attribute reader" do + pirate = Pirate.create!(catchphrase: "arrrr") + def pirate.catchphrase + super.upcase + end + + new_catchphrase = "arrrr matey!" + + pirate.catchphrase = new_catchphrase + assert pirate.catchphrase_changed? + + expected_changes = { + "catchphrase" => ["arrrr", new_catchphrase] + } + + assert_equal new_catchphrase.upcase, pirate.catchphrase + assert_equal expected_changes, pirate.changes + end + test "attribute_changed? doesn't compute in-place changes for unrelated attributes" do test_type_class = Class.new(ActiveRecord::Type::Value) do define_method(:changed_in_place?) do |*| diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 89d8a8bdca..a7b6333010 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -202,11 +202,29 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.first.replies.exists? end - # ensures +exists?+ runs valid SQL by excluding order value - def test_exists_with_order + # Ensure +exists?+ runs without an error by excluding distinct value. + # See https://github.com/rails/rails/pull/26981. + def test_exists_with_order_and_distinct assert_equal true, Topic.order(:id).distinct.exists? end + # Ensure +exists?+ runs without an error by excluding order value. + def test_exists_with_order + assert_equal true, Topic.order("invalid sql here").exists? + end + + def test_exists_with_joins + assert_equal true, Topic.joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? + end + + def test_exists_with_left_joins + assert_equal true, Topic.left_joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? + end + + def test_exists_with_eager_load + assert_equal true, Topic.eager_load(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? + end + def test_exists_with_includes_limit_and_empty_result assert_equal false, Topic.includes(:replies).limit(0).exists? assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? @@ -236,9 +254,9 @@ class FinderTest < ActiveRecord::TestCase def test_exists_with_aggregate_having_three_mappings_with_one_difference existing_address = customers(:david).address - assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) - assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) - assert_equal false, Customer.exists?(address: Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) + assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) + assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) + assert_equal false, Customer.exists?(address: Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) end def test_exists_does_not_instantiate_records diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 3720b0cc1a..a0a6d3c7ef 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -95,6 +95,24 @@ class FixturesTest < ActiveRecord::TestCase assert_nil(topics["second"]["author_email_address"]) end + def test_no_args_returns_all + all_topics = topics + assert_equal 5, all_topics.length + assert_equal "The First Topic", all_topics.first["title"] + assert_equal 5, all_topics.last.id + end + + def test_no_args_record_returns_all_without_array + all_binaries = binaries + assert_kind_of(Array, all_binaries) + assert_equal 1, binaries.length + end + + def test_nil_raises + assert_raise(StandardError) { topics(nil) } + assert_raise(StandardError) { topics([nil]) } + end + def test_inserts create_fixtures("topics") first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'David'") diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index d7aa091623..0678bb714f 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -1,4 +1,3 @@ - require "cases/helper" require "models/company" require "models/developer" diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 19588d28a2..7bcabd0cc6 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -80,7 +80,7 @@ module ActiveRecord end def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences - connection.create_table :cats, id: :uuid + connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" assert_nothing_raised { rename_table :cats, :felines } assert connection.table_exists? :felines ensure diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 4f92f71a09..11ef0d8743 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -143,7 +143,7 @@ module ActiveRecord assert_equal({ foo: "bar" }, relation.create_with_value) end - test "test_merge!" do + test "merge!" do assert relation.merge!(select: :foo).equal?(relation) assert_equal [:foo], relation.select_values end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index c7b2ac90fb..cbc466d6b8 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -289,6 +289,11 @@ module ActiveRecord assert_equal essays(:david_modest_proposal), essay end + def test_where_on_association_with_select_relation + essay = Essay.where(author: Author.where(name: "David").select(:name)).take + assert_equal essays(:david_modest_proposal), essay + end + def test_where_with_strong_parameters protected_params = Class.new do attr_reader :permitted diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 856469c710..fcf68b0f2a 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1902,7 +1902,7 @@ class RelationTest < ActiveRecord::TestCase end test "relations don't load all records in #inspect" do - assert_sql(/LIMIT/) do + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do Post.all.inspect end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index fccba4738f..cb8d449ba9 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -182,7 +182,11 @@ class SchemaDumperTest < ActiveRecord::TestCase if current_adapter?(:PostgreSQLAdapter) assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition elsif current_adapter?(:Mysql2Adapter) - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition + if ActiveRecord::Base.connection.supports_index_sort_order? + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, order: { rating: :desc }', index_definition + else + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition + end else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index f30e0958c3..b85d303a91 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -167,7 +167,7 @@ if current_adapter?(:Mysql2Adapter) def assert_permissions_granted_for(db_user) db_name = @configuration["database"] db_password = @configuration["password"] - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON `#{db_name}`.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") end end diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 07288568e8..1d21a2454f 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -154,7 +154,9 @@ if ActiveRecord::Base.connection.supports_views? end # sqlite dose not support CREATE, INSERT, and DELETE for VIEW - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + if current_adapter?(:Mysql2Adapter, :SQLServerAdapter) || + current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.postgresql_version >= 90300 + class UpdateableViewTest < ActiveRecord::TestCase self.use_transactional_tests = false fixtures :books diff --git a/activerecord/test/models/essay.rb b/activerecord/test/models/essay.rb index 13267fbc21..1f9772870e 100644 --- a/activerecord/test/models/essay.rb +++ b/activerecord/test/models/essay.rb @@ -1,4 +1,5 @@ class Essay < ActiveRecord::Base + belongs_to :author belongs_to :writer, primary_key: :name, polymorphic: true belongs_to :category, primary_key: :name has_one :owner, primary_key: :name diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 860c63b27c..e56e8fa36a 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -3,11 +3,13 @@ ActiveRecord::Schema.define do enable_extension!("uuid-ossp", ActiveRecord::Base.connection) enable_extension!("pgcrypto", ActiveRecord::Base.connection) if ActiveRecord::Base.connection.supports_pgcrypto_uuid? - create_table :uuid_parents, id: :uuid, force: true do |t| + uuid_default = connection.supports_pgcrypto_uuid? ? {} : { default: "uuid_generate_v4()" } + + create_table :uuid_parents, id: :uuid, force: true, **uuid_default do |t| t.string :name end - create_table :uuid_children, id: :uuid, force: true do |t| + create_table :uuid_children, id: :uuid, force: true, **uuid_default do |t| t.string :name t.uuid :uuid_parent_id end @@ -102,7 +104,7 @@ _SQL end create_table :uuid_items, force: true, id: false do |t| - t.uuid :uuid, primary_key: true + t.uuid :uuid, primary_key: true, **uuid_default t.string :title end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 08bef08abc..50f1d9bfe7 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -413,6 +413,9 @@ ActiveRecord::Schema.define do t.string :name end + create_table :kitchens, force: true do |t| + end + create_table :legacy_things, force: true do |t| t.integer :tps_report_number t.integer :version, null: false, default: 0 @@ -783,6 +786,10 @@ ActiveRecord::Schema.define do t.belongs_to :ship end + create_table :sinks, force: true do |t| + t.references :kitchen + end + create_table :shop_accounts, force: true do |t| t.references :customer t.references :customer_carrier diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 6f99155199..a4fc1e34eb 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1 +1,21 @@ +* Pass gem name and deprecation horizon to deprecation notifications. + + *Willem van Bergen* + +* Add support for `:offset` and `:zone` to `ActiveSupport::TimeWithZone#change` + + *Andrew White* + +* Add support for `:offset` to `Time#change` + + Fixes #28723. + + *Andrew White* + +* Add `fetch_values` for `HashWithIndifferentAccess` + + The method was originally added to `Hash` in Ruby 2.3.0. + + *Josh Pencheon* + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md) for previous changes. diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index ea71569ca8..d771cab68b 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -512,7 +512,6 @@ module ActiveSupport end end - # An Array with a compile method. class CallbackChain #:nodoc:# include Enumerable diff --git a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb index 16a6789f8d..88a34128c9 100644 --- a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb +++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb @@ -1,7 +1,7 @@ class Array # The human way of thinking about adding stuff to the end of a list is with append. - alias_method :append, :<< + alias_method :append, :push unless [].respond_to?(:append) # The human way of thinking about adding stuff to the beginning of a list is with prepend. - alias_method :prepend, :unshift + alias_method :prepend, :unshift unless [].respond_to?(:prepend) end diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index d553406dff..0f59c754fe 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -79,6 +79,9 @@ class Date # date.to_time(:local) # => 2007-11-10 00:00:00 0800 # # date.to_time(:utc) # => 2007-11-10 00:00:00 UTC + # + # NOTE: The :local timezone is Ruby's *process* timezone, i.e. ENV['TZ']. + # If the *application's* timezone is needed, then use +in_time_zone+ instead. def to_time(form = :local) raise ArgumentError, "Expected :local or :utc, got #{form.inspect}." unless [:local, :utc].include?(form) ::Time.send(form, year, month, day) diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 90d7d2947f..3a4ae6cb8b 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -1,28 +1,50 @@ module Enumerable - # Calculates a sum from the elements. + # Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements + # when we omit an identity. # - # payments.sum { |p| p.price * p.tax_rate } - # payments.sum(&:price) - # - # The latter is a shortcut for: - # - # payments.inject(0) { |sum, p| sum + p.price } - # - # It can also calculate the sum without the use of a block. - # - # [5, 15, 10].sum # => 30 - # ['foo', 'bar'].sum # => "foobar" - # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5] - # - # The default sum of an empty list is zero. You can override this default: - # - # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0) - def sum(identity = nil, &block) - if block_given? - map(&block).sum(identity) - else - sum = identity ? inject(identity, :+) : inject(:+) - sum || identity || 0 + # We tried shimming it to attempt the fast native method, rescue TypeError, + # and fall back to the compatible implementation, but that's much slower than + # just calling the compat method in the first place. + if Enumerable.instance_methods(false).include?(:sum) && !((?a..?b).sum rescue false) + # We can't use Refinements here because Refinements with Module which will be prepended + # doesn't work well https://bugs.ruby-lang.org/issues/13446 + alias :_original_sum_with_required_identity :sum + private :_original_sum_with_required_identity + # Calculates a sum from the elements. + # + # payments.sum { |p| p.price * p.tax_rate } + # payments.sum(&:price) + # + # The latter is a shortcut for: + # + # payments.inject(0) { |sum, p| sum + p.price } + # + # It can also calculate the sum without the use of a block. + # + # [5, 15, 10].sum # => 30 + # ['foo', 'bar'].sum # => "foobar" + # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5] + # + # The default sum of an empty list is zero. You can override this default: + # + # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0) + def sum(identity = nil, &block) + if identity + _original_sum_with_required_identity(identity, &block) + elsif block_given? + map(&block).sum(identity) + else + inject(:+) || 0 + end + end + else + def sum(identity = nil, &block) + if block_given? + map(&block).sum(identity) + else + sum = identity ? inject(identity, :+) : inject(:+) + sum || identity || 0 + end end end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index cdf27f49ad..85ab739095 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -267,7 +267,10 @@ class Module module_eval <<-RUBY, __FILE__, __LINE__ + 1 def respond_to_missing?(name, include_private = false) - #{target}.respond_to?(name, include_private) + # It may look like an oversight, but we deliberately do not pass + # +include_private+, because they do not get delegated. + + #{target}.respond_to?(name) || super end def method_missing(method, *args, &block) diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 7b7aeef25a..d3f23f4663 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -107,21 +107,22 @@ class Time # to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>, # <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only # the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour - # and minute is passed, then sec, usec and nsec is set to 0. The +options+ - # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, - # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt> - # <tt>:nsec</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both. + # and minute is passed, then sec, usec and nsec is set to 0. The +options+ parameter + # takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, + # <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>, + # <tt>:offset</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both. # # Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0) # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0) # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => Time.new(1981, 8, 29, 0, 0, 0) def change(options) - new_year = options.fetch(:year, year) - new_month = options.fetch(:month, month) - new_day = options.fetch(:day, day) - new_hour = options.fetch(:hour, hour) - new_min = options.fetch(:min, options[:hour] ? 0 : min) - new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_year = options.fetch(:year, year) + new_month = options.fetch(:month, month) + new_day = options.fetch(:day, day) + new_hour = options.fetch(:hour, hour) + new_min = options.fetch(:min, options[:hour] ? 0 : min) + new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_offset = options.fetch(:offset, nil) if new_nsec = options[:nsec] raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec] @@ -130,13 +131,18 @@ class Time new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) end - if utc? - ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) + raise ArgumentError, "argument out of range" if new_usec >= 1000000 + + new_sec += Rational(new_usec, 1000000) + + if new_offset + ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, new_offset) + elsif utc? + ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec) elsif zone - ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) + ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec) else - raise ArgumentError, "argument out of range" if new_usec >= 1000000 - ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset) + ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, utc_offset) end end diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index 72c74e966a..35cddcfde6 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -30,7 +30,7 @@ module ActiveSupport attr_accessor :deprecation_horizon # It accepts two parameters on initialization. The first is a version of library - # and the second is a library name + # and the second is a library name. # # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') def initialize(deprecation_horizon = "5.3", gem_name = "Rails") diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 1d1354c23e..a9a182f212 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -9,18 +9,18 @@ module ActiveSupport class Deprecation # Default warning behaviors per Rails.env. DEFAULT_BEHAVIORS = { - raise: ->(message, callstack) { + raise: ->(message, callstack, deprecation_horizon, gem_name) { e = DeprecationException.new(message) e.set_backtrace(callstack.map(&:to_s)) raise e }, - stderr: ->(message, callstack) { + stderr: ->(message, callstack, deprecation_horizon, gem_name) { $stderr.puts(message) $stderr.puts callstack.join("\n ") if debug }, - log: ->(message, callstack) { + log: ->(message, callstack, deprecation_horizon, gem_name) { logger = if defined?(Rails.logger) && Rails.logger Rails.logger @@ -32,12 +32,16 @@ module ActiveSupport logger.debug callstack.join("\n ") if debug }, - notify: ->(message, callstack) { - ActiveSupport::Notifications.instrument("deprecation.rails", - message: message, callstack: callstack) + notify: ->(message, callstack, deprecation_horizon, gem_name) { + notification_name = "deprecation.#{gem_name.underscore.tr('/', '_')}" + ActiveSupport::Notifications.instrument(notification_name, + message: message, + callstack: callstack, + gem_name: gem_name, + deprecation_horizon: deprecation_horizon) }, - silence: ->(message, callstack) {}, + silence: ->(message, callstack, deprecation_horizon, gem_name) {}, } # Behavior module allows to determine how to display deprecation messages. @@ -83,8 +87,17 @@ module ActiveSupport # # custom stuff # } def behavior=(behavior) - @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || b } + @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) } end + + private + def arity_coerce(behavior) + if behavior.arity == 4 || behavior.arity == -1 + behavior + else + -> message, callstack, _, _ { behavior.call(message, callstack) } + end + end end end end diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index 58c5c50e30..851d8eeda1 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -18,7 +18,7 @@ module ActiveSupport callstack ||= caller_locations(2) deprecation_message(callstack, message).tap do |m| - behavior.each { |b| b.call(m, callstack) } + behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) } end end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 1e1d6d3aac..3b185dd86b 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -195,6 +195,19 @@ module ActiveSupport indices.collect { |key| self[convert_key(key)] } end + # Returns an array of the values at the specified indices, but also + # raises an exception when one of the keys can't be found. + # + # hash = ActiveSupport::HashWithIndifferentAccess.new + # hash[:a] = 'x' + # hash[:b] = 'y' + # hash.fetch_values('a', 'b') # => ["x", "y"] + # hash.fetch_values('a', 'c') { |key| 'z' } # => ["x", "z"] + # hash.fetch_values('a', 'c') # => KeyError: key not found: "c" + def fetch_values(*indices, &block) + indices.collect { |key| fetch(key, &block) } + end if Hash.method_defined?(:fetch_values) + # Returns a shallow copy of the hash. # # hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } }) diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 2df819e554..37dfdc0422 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -64,6 +64,8 @@ module ActiveSupport # If an exception happens during that particular instrumentation the payload will # have a key <tt>:exception</tt> with an array of two elements as value: a string with # the name of the exception class, and the exception message. + # The <tt>:exception_object</tt> key of the payload will have the exception + # itself as the value. # # As the previous example depicts, the class <tt>ActiveSupport::Notifications::Event</tt> # is able to take the arguments as they come and provide an object-oriented diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index 07c9be0604..3d9ff99729 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -1,4 +1,5 @@ require "active_support/core_ext/string/strip" # for strip_heredoc +require "active_support/core_ext/time/calculations" require "concurrent/map" module ActiveSupport diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index b0dd6b7e8c..ecb9fb6785 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -330,6 +330,42 @@ module ActiveSupport since(-other) end + # Returns a new +ActiveSupport::TimeWithZone+ where one or more of the elements have + # been changed according to the +options+ parameter. The time options (<tt>:hour</tt>, + # <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, + # so if only the hour is passed, then minute, sec, usec and nsec is set to 0. If the + # hour and minute is passed, then sec, usec and nsec is set to 0. The +options+ + # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, + # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, + # <tt>:nsec</tt>, <tt>:offset</tt>, <tt>:zone</tt>. Pass either <tt>:usec</tt> + # or <tt>:nsec</tt>, not both. Similarly, pass either <tt>:zone</tt> or + # <tt>:offset</tt>, not both. + # + # t = Time.zone.now # => Fri, 14 Apr 2017 11:45:15 EST -05:00 + # t.change(year: 2020) # => Tue, 14 Apr 2020 11:45:15 EST -05:00 + # t.change(hour: 12) # => Fri, 14 Apr 2017 12:00:00 EST -05:00 + # t.change(min: 30) # => Fri, 14 Apr 2017 11:30:00 EST -05:00 + # t.change(offset: "-10:00") # => Fri, 14 Apr 2017 11:45:15 HST -10:00 + # t.change(zone: "Hawaii") # => Fri, 14 Apr 2017 11:45:15 HST -10:00 + def change(options) + if options[:zone] && options[:offset] + raise ArgumentError, "Can't change both :offset and :zone at the same time: #{options.inspect}" + end + + new_time = time.change(options) + + if options[:zone] + new_zone = ::Time.find_zone(options[:zone]) + elsif options[:offset] + new_zone = ::Time.find_zone(new_time.utc_offset) + end + + new_zone ||= time_zone + periods = new_zone.periods_for_local(new_time) + + self.class.new(nil, new_zone, new_time, periods.include?(period) ? period : nil) + end + # Uses Date to provide precise Time calculations for years, months, and days # according to the proleptic Gregorian calendar. The result is returned as a # new TimeWithZone object. diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index ce5207546d..96a541a4ef 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -6,7 +6,7 @@ module ActiveSupport # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. # It allows us to do the following: # - # * Limit the set of zones provided by TZInfo to a meaningful subset of 146 + # * Limit the set of zones provided by TZInfo to a meaningful subset of 134 # zones. # * Retrieve and display zones with a friendlier name # (e.g., "Eastern Time (US & Canada)" instead of "America/New_York"). @@ -59,6 +59,7 @@ module ActiveSupport "Buenos Aires" => "America/Argentina/Buenos_Aires", "Montevideo" => "America/Montevideo", "Georgetown" => "America/Guyana", + "Puerto Rico" => "America/Puerto_Rico", "Greenland" => "America/Godthab", "Mid-Atlantic" => "Atlantic/South_Georgia", "Azores" => "Atlantic/Azores", diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index cde2967132..d849cdfa6b 100644 --- a/activesupport/lib/active_support/xml_mini/libxml.rb +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -14,11 +14,9 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) LibXML::XML::Parser.io(data).parse.to_hash end end diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb index 8a43b05b17..f3d485da2f 100644 --- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb +++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb @@ -65,12 +65,9 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) - LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER) parser = LibXML::XML::SaxParser.io(data) document = document_class.new diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb index 4c2be3f3ec..63466c08b2 100644 --- a/activesupport/lib/active_support/xml_mini/nokogiri.rb +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -19,11 +19,9 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) doc = Nokogiri::XML(data) raise doc.errors.first if doc.errors.length > 0 doc.to_hash diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb index 7388bea5d8..54e15e6a5f 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -71,11 +71,9 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) document = document_class.new parser = Nokogiri::XML::SAX::Parser.new(document) parser.parse(data) diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 36f0ee22b8..be7c14e9b4 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -166,6 +166,9 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase 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 non-zero offset + assert_equal DateTime.civil(2005, 2, 22, 15, 15, 10, Rational(-5, 24)), DateTime.civil(2005, 2, 22, 15, 15, 10, 0).change(offset: Rational(-5, 24)) + # 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) assert_equal DateTime.civil(2005, 1, 2, 11, 22, Rational(33000008, 1000000)), DateTime.civil(2005, 1, 2, 11, 22, 33).change(usec: 8) diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index a17438bf4d..085fd6592d 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -70,7 +70,23 @@ Product = Struct.new(:name) do end end +module ExtraMissing + def method_missing(sym, *args) + if sym == :extra_missing + 42 + else + super + end + end + + def respond_to_missing?(sym, priv = false) + sym == :extra_missing || super + end +end + DecoratedTester = Struct.new(:client) do + include ExtraMissing + delegate_missing_to :client end @@ -356,6 +372,22 @@ class ModuleTest < ActiveSupport::TestCase assert_match(/undefined method `my_fake_method' for/, e.message) end + def test_delegate_to_missing_affects_respond_to + assert DecoratedTester.new(@david).respond_to?(:name) + assert_not DecoratedTester.new(@david).respond_to?(:private_name) + assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method) + + assert DecoratedTester.new(@david).respond_to?(:name, true) + assert_not DecoratedTester.new(@david).respond_to?(:private_name, true) + assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method, true) + end + + def test_delegate_to_missing_respects_superclass_missing + assert_equal 42, DecoratedTester.new(@david).extra_missing + + assert_respond_to DecoratedTester.new(@david), :extra_missing + end + def test_delegate_with_case event = Event.new(Tester.new) assert_equal 1, event.foo diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index bd644c8457..625a5bffb8 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -433,6 +433,13 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(nsec: 1000000000) } end + def test_change_offset + assert_equal Time.new(2006, 2, 22, 15, 15, 10, "-08:00"), Time.new(2006, 2, 22, 15, 15, 10, "+01:00").change(offset: "-08:00") + assert_equal Time.new(2006, 2, 22, 15, 15, 10, -28800), Time.new(2006, 2, 22, 15, 15, 10, 3600).change(offset: -28800) + assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "+01:00").change(usec: 1000000, offset: "-08:00") } + assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "+01:00").change(nsec: 1000000000, offset: -28800) } + end + def test_advance assert_equal Time.local(2006, 2, 28, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 1) assert_equal Time.local(2005, 6, 28, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(months: 4) diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index c3afe68378..70ae793cda 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -625,6 +625,12 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Fri, 31 Dec 1999 06:00:00 EST -05:00", @twz.change(hour: 6).inspect assert_equal "Fri, 31 Dec 1999 19:15:00 EST -05:00", @twz.change(min: 15).inspect assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.change(sec: 30).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(offset: "-10:00").inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(offset: -36000).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: "Hawaii").inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: -10).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: -36000).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: "Pacific/Honolulu").inspect end def test_change_at_dst_boundary diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 36d1ef0849..257cb50fb2 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -100,16 +100,18 @@ class DeprecationTest < ActiveSupport::TestCase end def test_several_behaviors - @a, @b = nil, nil + @a, @b, @c = nil, nil, nil ActiveSupport::Deprecation.behavior = [ - Proc.new { |msg, callstack| @a = msg }, - Proc.new { |msg, callstack| @b = msg } + lambda { |msg, callstack, horizon, gem| @a = msg }, + lambda { |msg, callstack| @b = msg }, + lambda { |*args| @c = args }, ] @dtc.partially assert_match(/foo=nil/, @a) assert_match(/foo=nil/, @b) + assert_equal 4, @c.size end def test_raise_behaviour @@ -119,7 +121,7 @@ class DeprecationTest < ActiveSupport::TestCase callstack = caller_locations e = assert_raise ActiveSupport::DeprecationException do - ActiveSupport::Deprecation.behavior.first.call(message, callstack) + ActiveSupport::Deprecation.behavior.first.call(message, callstack, "horizon", "gem") end assert_equal message, e.message assert_equal callstack.map(&:to_s), e.backtrace.map(&:to_s) @@ -130,7 +132,7 @@ class DeprecationTest < ActiveSupport::TestCase behavior = ActiveSupport::Deprecation.behavior.first content = capture(:stderr) { - assert_nil behavior.call("Some error!", ["call stack!"]) + assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem") } assert_match(/Some error!/, content) assert_match(/call stack!/, content) @@ -152,11 +154,32 @@ class DeprecationTest < ActiveSupport::TestCase behavior = ActiveSupport::Deprecation.behavior.first stderr_output = capture(:stderr) { - assert_nil behavior.call("Some error!", ["call stack!"]) + assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem") } assert stderr_output.empty? end + def test_default_notify_behavior + ActiveSupport::Deprecation.behavior = :notify + behavior = ActiveSupport::Deprecation.behavior.first + + begin + events = [] + ActiveSupport::Notifications.subscribe("deprecation.my_gem_custom") { |_, **args| + events << args + } + + assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "MyGem::Custom") + assert_equal 1, events.size + assert_equal "Some error!", events.first[:message] + assert_equal ["call stack!"], events.first[:callstack] + assert_equal "horizon", events.first[:deprecation_horizon] + assert_equal "MyGem::Custom", events.first[:gem_name] + ensure + ActiveSupport::Notifications.unsubscribe("deprecation.my_gem_custom") + end + end + def test_deprecated_instance_variable_proxy assert_not_deprecated { @dtc.request.size } diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index ee862320e8..d68add46cd 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -166,6 +166,18 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal [1, 2], @mixed.values_at(:a, :b) end + def test_indifferent_fetch_values + skip unless Hash.method_defined?(:fetch_values) + + @mixed = @mixed.with_indifferent_access + + assert_equal [1, 2], @mixed.fetch_values("a", "b") + assert_equal [1, 2], @mixed.fetch_values(:a, :b) + assert_equal [1, 2], @mixed.fetch_values(:a, "b") + assert_equal [1, "c"], @mixed.fetch_values(:a, :c) { |key| key } + assert_raise(KeyError) { @mixed.fetch_values(:a, :c) } + end + def test_indifferent_reading hash = HashWithIndifferentAccess.new hash["a"] = 1 @@ -675,7 +687,6 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_includes yaml_output, "hash" end end - HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess def test_should_use_default_proc_for_unknown_key hash_wia = HashWithIndifferentAccess.new { 1 + 2 } diff --git a/activesupport/test/xml_mini/xml_mini_engine_test.rb b/activesupport/test/xml_mini/xml_mini_engine_test.rb index 5be9084c9d..244e0b0d3a 100644 --- a/activesupport/test/xml_mini/xml_mini_engine_test.rb +++ b/activesupport/test/xml_mini/xml_mini_engine_test.rb @@ -75,6 +75,11 @@ class XMLMiniEngineTest < ActiveSupport::TestCase assert_equal({}, ActiveSupport::XmlMini.parse("")) end + def test_parse_from_frozen_string + xml_string = "<root/>".freeze + assert_equal({ "root" => {} }, ActiveSupport::XmlMini.parse(xml_string)) + end + def test_array_type_makes_an_array assert_equal_rexml(<<-eoxml) <blog> diff --git a/guides/Rakefile b/guides/Rakefile index 0a591558e1..3a6f10040f 100644 --- a/guides/Rakefile +++ b/guides/Rakefile @@ -17,13 +17,13 @@ namespace :guides do namespace :generate do desc "Generate HTML guides" - task :html => :encoding do + task html: :encoding do ENV["WARNINGS"] = "1" # authors can't disable this ruby "rails_guides.rb" end desc "Generate .mobi file. The kindlegen executable must be in your PATH. You can get it for free from http://www.amazon.com/gp/feature.html?docId=1000765211" - task :kindle => :encoding do + task kindle: :encoding do require "kindlerb" unless Kindlerb.kindlegen_available? abort "Please run `setupkindlerb` to install kindlegen" @@ -38,7 +38,7 @@ namespace :guides do # Validate guides ------------------------------------------------------------------------- desc 'Validate guides, use ONLY=foo to process just "foo.html"' - task :validate => :encoding do + task validate: :encoding do ruby "w3c_validator.rb" end diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css index a1e4851833..b27776745a 100644 --- a/guides/assets/stylesheets/main.css +++ b/guides/assets/stylesheets/main.css @@ -294,6 +294,7 @@ a, a:link, a:visited { #mainCol a, #subCol a, #feature a {color: #980905;} #mainCol a code, #subCol a code, #feature a code {color: #980905;} +#mainCol a.anchorlink, #mainCol a.anchorlink code {color: #333;} #mainCol a.anchorlink { text-decoration: none; } #mainCol a.anchorlink:hover { text-decoration: underline; } diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb index 16aaa7d1eb..02d58601c4 100644 --- a/guides/rails_guides/markdown.rb +++ b/guides/rails_guides/markdown.rb @@ -106,7 +106,7 @@ module RailsGuides end end - doc.css('h3, h4, h5, h6').each do |node| + doc.css("h3, h4, h5, h6").each do |node| node.inner_html = "<a class='anchorlink' href='##{node[:id]}'>#{node.inner_html}</a>" end end.to_html diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index 20cbd568c9..9d43c10be6 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -94,15 +94,15 @@ HTML tree = version || edge root = file_path[%r{(.+)/}, 1] - path = case root - when "abstract_controller", "action_controller", "action_dispatch" - "actionpack/lib/#{file_path}" - when /\A(action|active)_/ - "#{root.sub("_", "")}/lib/#{file_path}" - else - file_path - end - + path = \ + case root + when "abstract_controller", "action_controller", "action_dispatch" + "actionpack/lib/#{file_path}" + when /\A(action|active)_/ + "#{root.sub("_", "")}/lib/#{file_path}" + else + file_path + end "https://github.com/rails/rails/tree/#{tree}/#{path}" end diff --git a/guides/source/5_1_release_notes.md b/guides/source/5_1_release_notes.md index 5d4885d55c..ffa18f1a2b 100644 --- a/guides/source/5_1_release_notes.md +++ b/guides/source/5_1_release_notes.md @@ -24,7 +24,14 @@ repository on GitHub. Upgrading to Rails 5.1 ---------------------- -ToDo +If you're upgrading an existing application, it's a great idea to have good test +coverage before going in. You should also first upgrade to Rails 5.0 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 5.1. A list of things to watch out for when upgrading is +available in the +[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-5-0-to-rails-5-1) +guide. + Major Features -------------- @@ -94,8 +101,8 @@ Secrets will be decrypted in production, using a key stored either in the [Pull Request](https://github.com/rails/rails/pull/27825) -Allows specifying common params used for all methods in a mailer class -to share instance variables, headers and other common setup. +Allows specifying common parameters used for all methods in a mailer class in +order to share instance variables, headers and other common setup. ``` ruby class InvitationsMailer < ApplicationMailer @@ -223,41 +230,228 @@ Railties Please refer to the [Changelog][railties] for detailed changes. +### Removals + +* Remove deprecated `config.static_cache_control`. + ([commit](https://github.com/rails/rails/commit/c861decd44198f8d7d774ee6a74194d1ac1a5a13) + +* Remove deprecated `config.serve_static_files`. + ([commit](https://github.com/rails/rails/commit/0129ca2eeb6d5b2ea8c6e6be38eeb770fe45f1fa)) + +* Remove deprecated file `rails/rack/debugger`. + ([commit](https://github.com/rails/rails/commit/7563bf7b46e6f04e160d664e284a33052f9804b8)) + +* Remove deprecated tasks: `rails:update`, `rails:template`, `rails:template:copy`, + `rails:update:configs` and `rails:update:bin`. + ([commit](https://github.com/rails/rails/commit/f7782812f7e727178e4a743aa2874c078b722eef)) + +* Remove deprecated `CONTROLLER` environment variable for `routes` task. + ([commit](https://github.com/rails/rails/commit/f9ed83321ac1d1902578a0aacdfe55d3db754219)) + +* Remove -j (--javascript) option from `rails new` command. + ([Pull Request](https://github.com/rails/rails/pull/28546)) + +### Notable changes + +* Added a shared section to `config/secrets.yml` that will be loaded for all + environments. + ([commit](https://github.com/rails/rails/commit/e530534265d2c32b5c5f772e81cb9002dcf5e9cf)) + +* The config file `config/secrets.yml` is now loaded in with all keys as symbols. + ([Pull Request](https://github.com/rails/rails/pull/26929)) + +* Removed jquery-rails from default stack. rails-ujs, which is shipped + with Action View, is included as default UJS adapter. + ([Pull Request](https://github.com/rails/rails/pull/27113)) + +* Add Yarn support in new apps with a yarn binstub and package.json. + ([Pull Request](https://github.com/rails/rails/pull/26836)) + +* Add Webpack support in new apps via the --webpack option, which will delegate + to the rails/webpacker gem. + ([Pull Request](https://github.com/rails/rails/pull/27288)) + +* Initialize Git repo when generating new app, if option `--skip-git` is not + provided. + ([Pull Request](https://github.com/rails/rails/pull/27632)) + +* Add encrypted secrets in `config/secrets.yml.enc`. + ([Pull Request](https://github.com/rails/rails/pull/28038)) + +Action Cable +----------- + +Please refer to the [Changelog][action-cable] for detailed changes. + +### Notable changes + +* Added support for `channel_prefix` to Redis and evented Redis adapters + in `cable.yml` to avoid name collisions when using the same Redis server + with multiple applications. + ([Pull Request](https://github.com/rails/rails/pull/27425)) + +* Permit same-origin connections by default. + ([commit](https://github.com/rails/rails/commit/dae404473409fcab0e07976aec626df670e52282)) + +* Add `ActiveSupport::Notifications` hook for broadcasing data. + ([Pull Request](https://github.com/rails/rails/pull/24988)) + Action Pack ----------- Please refer to the [Changelog][action-pack] for detailed changes. +### Removals + +### Deprecations + +### Notable changes + Action View ------------- Please refer to the [Changelog][action-view] for detailed changes. +### Removals + +* Removed deprecated `#original_exception` in `ActionView::Template::Error`. + ([commit](https://github.com/rails/rails/commit/b9ba263e5aaa151808df058f5babfed016a1879f)) + +* Remove the option `encode_special_chars` misnomer from `strip_tags`. + ([Pull Request](https://github.com/rails/rails/pull/28061)) + +### Deprecations + +* Deprecated Erubis ERB handler in favor of Erubi. + ([Pull Request](https://github.com/rails/rails/pull/27757)) + +### Notable changes + +* Raw template handler (the default template handler in Rails 5) now outputs + HTML-safe strings. + ([commit](https://github.com/rails/rails/commit/1de0df86695f8fa2eeae6b8b46f9b53decfa6ec8)) + +* Change `datetime_field` and `datetime_field_tag` to generate `datetime-local` + fields. + ([Pull Request](https://github.com/rails/rails/pull/28061)) + +* New Builder-style syntax for HTML tags (`tag.div`, `tag.br`, etc.) + ([Pull Request](https://github.com/rails/rails/pull/25543)) + +* Add `form_with` to unify `form_tag` and `form_for` usage. + ([Pull Request](https://github.com/rails/rails/pull/26976)) + Action Mailer ------------- Please refer to the [Changelog][action-mailer] for detailed changes. +### Notable changes + +* Exception handling: use `rescue_from` to handle exceptions raised by + mailer actions, by message delivery, and by deferred delivery jobs. + ([commit](https://github.com/rails/rails/commit/e35b98e6f5c54330245645f2ed40d56c74538902)) + +* Allowed setting custom content type when attachments are included + and body is set inline. + ([Pull Request](https://github.com/rails/rails/pull/27227)) + +* Allowed passing lambdas as values to the `default` method. + ([Commit](https://github.com/rails/rails/commit/1cec84ad2ddd843484ed40b1eb7492063ce71baf)) + +* Added support for parameterized invocation of mailers to share before filters and defaults + between different mailer actions. + ([Commit](https://github.com/rails/rails/commit/1cec84ad2ddd843484ed40b1eb7492063ce71baf)) + +* Passed the incoming arguments to the mailer action to `process.action_mailer` event under + an `args` key. + ([Pull Request](https://github.com/rails/rails/pull/27900)) + Active Record ------------- Please refer to the [Changelog][active-record] for detailed changes. +### Removals + +### Deprecations + +### Notable changes + +* Skipped comments in the output of `mysqldump` command by default. + ([Pull Request](https://github.com/rails/rails/pull/23301)) + +* Fixed `ActiveRecord::Relation#count` to use Ruby's `Enumerable#count` for counting + records when a block is passed as argument instead of silently ignoring the + passed block. + ([Pull Request](https://github.com/rails/rails/pull/24203)) + +* Pass `"-v ON_ERROR_STOP=1"` flag with `psql` command to not suppress SQL errors. + ([Pull Request](https://github.com/rails/rails/pull/24773)) + Active Model ------------ Please refer to the [Changelog][active-model] for detailed changes. +### Removals + +* Removed deprecated methods in `ActiveModel::Errors`. + ([commit](https://github.com/rails/rails/commit/9de6457ab0767ebab7f2c8bc583420fda072e2bd)) + +* Removed deprecated `:tokenizer` option in the length validator. + ([commit](https://github.com/rails/rails/commit/6a78e0ecd6122a6b1be9a95e6c4e21e10e429513)) + +* Remove deprecated behavior that halts callbacks when the return value is false. + ([commit](https://github.com/rails/rails/commit/3a25cdca3e0d29ee2040931d0cb6c275d612dffe)) + +### Notable changes + +* The original string assigned to a model attribute is no longer incorrectly + frozen. + ([Pull Request](https://github.com/rails/rails/pull/28729)) + Active Job ----------- Please refer to the [Changelog][active-job] for detailed changes. +### Removals + +* Removed deprecated support to passing the adapter class to `.queue_adapter`. + ([commit](https://github.com/rails/rails/commit/d1fc0a5eb286600abf8505516897b96c2f1ef3f6)) + +* Removed deprecated `#original_exception` in `ActiveJob::DeserializationError`. + ([commit](https://github.com/rails/rails/commit/d861a1fcf8401a173876489d8cee1ede1cecde3b)) + +### Notable changes + +* Added declarative exception handling via `ActiveJob::Base.retry_on` and `ActiveJob::Base.discard_on`. + ([Pull Request](https://github.com/rails/rails/pull/25991)) + +* Yield the job instance so you have access to things like `job.arguments` on + the custom logic after retries fail. + ([commit](https://github.com/rails/rails/commit/a1e4c197cb12fef66530a2edfaeda75566088d1f)) + Active Support -------------- Please refer to the [Changelog][active-support] for detailed changes. +### Removals + +### Deprecations + +### Notable changes + +* Added `Module#delegate_missing_to` to delegate method calls not + defined for the current object to a proxy object. + ([Pull Request](https://github.com/rails/rails/pull/23930)) + +* Added `Date#all_day` which returns a range representing the whole day + of the current date & time. + ([Pull Request](https://github.com/rails/rails/pull/24930)) + Credits ------- diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index e26805d22c..b8f076a27b 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -469,7 +469,7 @@ In order to make this work, the model must have an accessor named `password_dige The `has_secure_password` will add the following validations on the `password` accessor: 1. Password should be present. -2. Password should be equal to its confirmation (provided +password_confirmation+ is passed along). +2. Password should be equal to its confirmation (provided `password_confirmation` is passed along). 3. The maximum length of a password is 72 (required by `bcrypt` on which ActiveModel::SecurePassword depends) #### Examples diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 77bd3c97e8..b1705855d0 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -117,6 +117,10 @@ Here is a list with all the available Active Record callbacks, listed in the sam WARNING. `after_save` runs both on create and update, but always _after_ the more specific callbacks `after_create` and `after_update`, no matter the order in which the macro calls were executed. +NOTE: `before_destroy` callbacks should be placed before `dependent: :destroy` +associations (or use the `prepend: true` option), to ensure they execute before +the records are deleted by `dependent: :destroy`. + ### `after_initialize` and `after_find` The `after_initialize` callback will be called whenever an Active Record object is instantiated, either by directly using `new` or when a record is loaded from the database. It can be useful to avoid the need to directly override your Active Record `initialize` method. @@ -254,7 +258,11 @@ Halting Execution As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed. -The whole callback chain is wrapped in a transaction. If any _before_ callback method returns exactly `false` or raises an exception, the execution chain gets halted and a ROLLBACK is issued; _after_ callbacks can only accomplish that by raising an exception. +The whole callback chain is wrapped in a transaction. If any callback raises an exception, the execution chain gets halted and a ROLLBACK is issued. To intentionally stop a chain use: + +```ruby +throw :abort +``` WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception. diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index 6e7e29ed60..7fdb5901f3 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -972,11 +972,11 @@ on. Because this is database-independent, it could be loaded into any database that Active Record supports. This could be very useful if you were to distribute an application that is able to run against multiple databases. -There is however a trade-off: `db/schema.rb` cannot express database specific -items such as triggers, stored procedures or check constraints. While in a -migration you can execute custom SQL statements, the schema dumper cannot -reconstitute those statements from the database. If you are using features like -this, then you should set the schema format to `:sql`. +NOTE: `db/schema.rb` cannot express database specific items such as triggers, +sequences, stored procedures or check constraints, etc. Please note that while +custom SQL statements can be run in migrations, these statements cannot be reconstituted +by the schema dumper. If you are using features like this, then you +should set the schema format to `:sql`. Instead of using Active Record's schema dumper, the database's structure will be dumped using a tool specific to the database (via the `db:structure:dump` diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 34b9c0d2ca..3c61754982 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -333,10 +333,6 @@ As a contributor, it's important to think about whether this API is meant for en A class or module is marked with `:nodoc:` to indicate that all methods are internal API and should never be used directly. -If you come across an existing `:nodoc:` you should tread lightly. Consider asking someone from the core team or author of the code before removing it. This should almost always happen through a pull request instead of the docrails project. - -A `:nodoc:` should never be added simply because a method or class is missing documentation. There may be an instance where an internal public method wasn't given a `:nodoc:` by mistake, for example when switching a method from private to public visibility. When this happens it should be discussed over a PR on a case-by-case basis and never committed directly to docrails. - To summarize, the Rails team uses `:nodoc:` to mark publicly visible methods and classes for internal use; changes to the visibility of API should be considered carefully and discussed over a pull request first. Regarding the Rails Stack diff --git a/guides/source/configuring.md b/guides/source/configuring.md index ae70b06996..bf9456a482 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -543,6 +543,8 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `config.action_view.debug_missing_translation` determines whether to wrap the missing translations key in a `<span>` tag or not. This defaults to `true`. +* `config.action_view.form_with_generates_remote_forms` determines whether `form_with` generates remote forms or not. This defaults to `true`. + ### Configuring Action Mailer There are a number of settings available on `config.action_mailer`: diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 3b19b0dff1..39f4272b3c 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -132,24 +132,12 @@ learn about Ruby on Rails, and the API, which serves as a reference. You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing them up to date with the latest edge Rails. -You can either open a pull request to [Rails](https://github.com/rails/rails) or -ask the [Rails core team](http://rubyonrails.org/community/#core) for commit access on -docrails if you contribute regularly. -Please do not open pull requests in docrails, if you'd like to get feedback on your -change, ask for it in [Rails](https://github.com/rails/rails) instead. - -Docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation. - -If you are unsure of the documentation changes, you can create an issue in the [Rails](https://github.com/rails/rails/issues) issues tracker on GitHub. +To do so, open a pull request to [Rails](https://github.com/rails/rails) on GitHub. When working with documentation, please take into account the [API Documentation Guidelines](api_documentation_guidelines.html) and the [Ruby on Rails Guides Guidelines](ruby_on_rails_guides_guidelines.html). -NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements. - NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes. -WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails. - Translating Rails Guides ------------------------ @@ -418,16 +406,6 @@ examples or multiple paragraphs. Otherwise, it's best to make a new paragraph. Some changes require the dependencies to be upgraded. In these cases make sure you run `bundle update` to get the right version of the dependency and commit the `Gemfile.lock` file within your changes. -### Sanity Check - -You should not be the only person who looks at the code before you submit it. -If you know someone else who uses Rails, try asking them if they'll check out -your work. If you don't know anyone else using Rails, try hopping into the IRC -room or posting about your idea to the rails-core mailing list. Doing this in -private before you push a patch out publicly is the "smoke test" for a patch: -if you can't convince one other developer of the beauty of your code, youโre -unlikely to convince the core team either. - ### Commit Your Changes When you're happy with the code on your computer, you need to commit the changes to Git: @@ -685,4 +663,4 @@ And then... think about your next contribution! Rails Contributors ------------------ -All contributions, either via master or docrails, get credit in [Rails Contributors](http://contributors.rubyonrails.org). +All contributions get credit in [Rails Contributors](http://contributors.rubyonrails.org). diff --git a/guides/source/engines.md b/guides/source/engines.md index 180a786237..2276f348a1 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -14,6 +14,7 @@ After reading this guide, you will know: * How to build features for the engine. * How to hook the engine into an application. * How to override engine functionality in the application. +* Avoid loading Rails frameworks with Load and Configuration Hooks -------------------------------------------------------------------------------- @@ -1410,3 +1411,114 @@ module MyEngine end end ``` + +Active Support On Load Hooks +---------------------------- + +Active Support is the Ruby on Rails component responsible for providing Ruby language extensions, utilities, and other transversal utilities. + +Rails code can often be referenced on load of an application. Rails is responsible for the load order of these frameworks, so when you load frameworks, such as `ActiveRecord::Base`, prematurely you are violating an implicit contract your application has with Rails. Moreover, by loading code such as `ActiveRecord::Base` on boot of your application you are loading entire frameworks which may slow down your boot time and could cause conflicts with load order and boot of your application. + +On Load hooks are the API that allow you to hook into this initialization process without violating the load contract with Rails. This will also mitigate boot performance degradation and avoid conflicts. + +## What are `on_load` hooks? + +Since Ruby is a dynamic language, some code will cause different Rails frameworks to load. Take this snippet for instance: + +```ruby +ActiveRecord::Base.include(MyActiveRecordHelper) +``` + +This snippet means that when this file is loaded, it will encounter `ActiveRecord::Base`. This encounter causes Ruby to look for the definition of that constant and will require it. This causes the entire Active Record framework to be loaded on boot. + +`ActiveSupport.on_load` is a mechanism that can be used to defer the loading of code until it is actually needed. The snippet above can be changed to: + +```ruby +ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } +``` + +This new snippet will only include `MyActiveRecordHelper` when `ActiveRecord::Base` is loaded. + +## How does it work? + +In the Rails framework these hooks are called when a specific library is loaded. For example, when `ActionController::Base` is loaded, the `:action_controller_base` hook is called. This means that all `ActiveSupport.on_load` calls with `:action_controller_base` hooks will be called in the context of `ActionController::Base` (that means `self` will be an `ActionController::Base`). + +## Modifying code to use `on_load` hooks + +Modifying code is generally straightforward. If you have a line of code that refers to a Rails framework such as `ActiveRecord::Base` you can wrap that code in an `on_load` hook. + +### Example 1 + +```ruby +ActiveRecord::Base.include(MyActiveRecordHelper) +``` + +becomes + +```ruby +ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } # self refers to ActiveRecord::Base here, so we can simply #include +``` + +### Example 2 + +```ruby +ActionController::Base.prepend(MyActionControllerHelper) +``` + +becomes + +```ruby +ActiveSupport.on_load(:action_controller_base) { prepend MyActionControllerHelper } # self refers to ActionController::Base here, so we can simply #prepend +``` + +### Example 3 + +```ruby +ActiveRecord::Base.include_root_in_json = true +``` + +becomes + +```ruby +ActiveSupport.on_load(:active_record) { self.include_root_in_json = true } # self refers to ActiveRecord::Base here +``` + +## Available Hooks + +These are the hooks you can use in your own code. + +To hook into the initialization process of one of the following classes use the available hook. + +| Class | Available Hooks | +| --------------------------------- | ------------------------------------ | +| `ActionCable` | `action_cable` | +| `ActionController::API` | `action_controller_api` | +| `ActionController::API` | `action_controller` | +| `ActionController::Base` | `action_controller_base` | +| `ActionController::Base` | `action_controller` | +| `ActionController::TestCase` | `action_controller_test_case` | +| `ActionDispatch::IntegrationTest` | `action_dispatch_integration_test` | +| `ActionMailer::Base` | `action_mailer` | +| `ActionMailer::TestCase` | `action_mailer_test_case` | +| `ActionView::Base` | `action_view` | +| `ActionView::TestCase` | `action_view_test_case` | +| `ActiveJob::Base` | `active_job` | +| `ActiveJob::TestCase` | `active_job_test_case` | +| `ActiveRecord::Base` | `active_record` | +| `ActiveSupport::TestCase` | `active_support_test_case` | +| `i18n` | `i18n` | + +## Configuration hooks + +These are the available configuration hooks. They do not hook into any particular framework, instead they run in context of the entire application. + +| Hook | Use Case | +| ---------------------- | ------------------------------------------------------------------------------------- | +| `before_configuration` | First configurable block to run. Called before any initializers are run. | +| `before_initialize` | Second configurable block to run. Called before frameworks initialize. | +| `before_eager_load` | Third configurable block to run. Does not run if `config.cache_classes` set to false. | +| `after_initialize` | Last configurable block to run. Called after frameworks initialize. | + +### Example + +`config.before_configuration { puts 'I am called before any initializers' }` diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 0508b0fb38..f46f1648b3 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -164,7 +164,7 @@ make it easier for users to click the inputs. Other form controls worth mentioning are textareas, password fields, hidden fields, search fields, telephone fields, date fields, time fields, -color fields, datetime fields, datetime-local fields, month fields, week fields, +color fields, datetime-local fields, month fields, week fields, URL fields, email fields, number fields and range fields: ```erb @@ -877,7 +877,7 @@ Active Record provides model level support via the `accepts_nested_attributes_fo ```ruby class Person < ApplicationRecord - has_many :addresses + has_many :addresses, inverse_of: :person accepts_nested_attributes_for :addresses end diff --git a/guides/source/generators.md b/guides/source/generators.md index d0b6cef3fd..a554e08204 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -426,7 +426,7 @@ Fallbacks allow your generators to have a single responsibility, increasing code Application Templates --------------------- -Now that you've seen how generators can be used _inside_ an application, did you know they can also be used to _generate_ applications too? This kind of generator is referred as a "template". This is a brief overview of the Templates API. For detailed documentation see the [Rails Application Templates guide](rails_application_templates.html). +Now that you've seen how generators can be used _inside_ an application, did you know they can also be used to _generate_ applications too? This kind of generator is referred to as a "template". This is a brief overview of the Templates API. For detailed documentation see the [Rails Application Templates guide](rails_application_templates.html). ```ruby gem "rspec-rails", group: "test" diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 068114898d..f3ae5a5b28 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -46,7 +46,7 @@ development with Rails. What is Rails? -------------- -Rails is a web application development framework written in the Ruby language. +Rails is a web application development framework written in the Ruby programming language. It is designed to make programming web applications easier by making assumptions about what every developer needs to get started. It allows you to write less code while accomplishing more than many other languages and frameworks. @@ -182,7 +182,7 @@ of the files and folders that Rails created by default: |test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html).| |tmp/|Temporary files (like cache and pid files).| |vendor/|A place for all third-party code. In a typical Rails application this includes vendored gems.| -|.gitignore|This file tells git which files (or patterns) it should ignore. See [Github - Ignoring files](https://help.github.com/articles/ignoring-files) for more info about ignoring files. +|.gitignore|This file tells git which files (or patterns) it should ignore. See [GitHub - Ignoring files](https://help.github.com/articles/ignoring-files) for more info about ignoring files. Hello, Rails! ------------- diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index 340933c7ee..f25b185fb5 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -20,9 +20,9 @@ Introduction to Rack Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call. -* [Rack API Documentation](http://rack.github.io/) - -Explaining Rack is not really in the scope of this guide. In case you are not familiar with Rack's basics, you should check out the [Resources](#resources) section below. +Explaining how Rack works is not really in the scope of this guide. In case you +are not familiar with Rack's basics, you should check out the [Resources](#resources) +section below. Rails on Rack ------------- @@ -74,7 +74,7 @@ And start the server: $ rackup config.ru ``` -To find out more about different `rackup` options: +To find out more about different `rackup` options, you can run: ```bash $ rackup --help @@ -89,7 +89,8 @@ Action Dispatcher Middleware Stack Many of Action Dispatcher's internal components are implemented as Rack middlewares. `Rails::Application` uses `ActionDispatch::MiddlewareStack` to combine various internal and external middlewares to form a complete Rails Rack application. -NOTE: `ActionDispatch::MiddlewareStack` is Rails equivalent of `Rack::Builder`, but built for better flexibility and more features to meet Rails' requirements. +NOTE: `ActionDispatch::MiddlewareStack` is Rails' equivalent of `Rack::Builder`, +but is built for better flexibility and more features to meet Rails' requirements. ### Inspecting Middleware Stack diff --git a/guides/source/routing.md b/guides/source/routing.md index 53735ce82e..f7dbbc510e 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -142,16 +142,17 @@ Sometimes, you have a resource that clients always look up without referencing a get 'profile', to: 'users#show' ``` -Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action but you must also specify the `controller:` to use: +Passing a `String` to `to:` will expect a `controller#action` format. When using a `Symbol`, the `to:` option should be replaced with `action:`. When using a `String` without a `#`, the `to:` option should be replaced with `controller:`: ```ruby -get 'profile', to: :show, controller: 'users' +get 'profile', action: :show, controller: 'users' ``` This resourceful route: ```ruby resource :geocoder +resolve('Geocoder') { [:geocoder] } ``` creates six different routes in your application, all mapping to the `Geocoders` controller: @@ -175,14 +176,6 @@ A singular resourceful route generates these helpers: As with plural resources, the same helpers ending in `_url` will also include the host, port and path prefix. -WARNING: A [long-standing bug](https://github.com/rails/rails/issues/1769) prevents `form_for` from working automatically with singular resources. As a workaround, specify the URL for the form directly, like so: - -```ruby -form_for @geocoder, url: geocoder_path do |f| - -# snippet for brevity -``` - ### Controller Namespaces and Routing You may wish to organize groups of controllers under a namespace. Most commonly, you might group a number of administrative controllers under an `Admin::` namespace. You would place these controllers under the `app/controllers/admin` directory, and you can group them together in your router: diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 3afc0e5309..ff747a95a0 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -73,16 +73,32 @@ For more information on changes made to Rails 5.1 please see the [release notes] ### Top-level `HashWithIndifferentAccess` is soft-deprecated If your application uses the the top-level `HashWithIndifferentAccess` class, you -should slowly move your code to use the `ActiveSupport::HashWithIndifferentAccess` -one. +should slowly move your code to instead use `ActiveSupport::HashWithIndifferentAccess`. It is only soft-deprecated, which means that your code will not break at the -moment and no deprecation warning will be displayed but this constant will be +moment and no deprecation warning will be displayed, but this constant will be removed in the future. Also, if you have pretty old YAML documents containing dumps of such objects, you may need to load and dump them again to make sure that they reference -the right constant and that loading them won't break in the future. +the right constant, and that loading them won't break in the future. + +### `config.secrets` now loaded with all keys as symbols + +If your application stores nested configuration in `config/secrets.yml`, all keys +are now loaded as symbols, so access using strings should be changed. + +From: + +```ruby +Rails.appplication.config.secrets[:smtp_settings]["address"] +``` + +To: + +```ruby +Rails.application.config.secrets[:smtp_settings][:address] +``` Upgrading from Rails 4.2 to Rails 5.0 ------------------------------------- diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index cbaf9100f7..2a6a87c232 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -338,7 +338,7 @@ this: end ``` -Notice the format.js in the `respond_to` block; that allows the controller to +Notice the `format.js` in the `respond_to` block: that allows the controller to respond to your Ajax request. You then have a corresponding `app/views/users/create.js.erb` view file that generates the actual JavaScript code that will be sent and executed on the client side. @@ -355,7 +355,7 @@ which uses Ajax to speed up page rendering in most applications. ### How Turbolinks Works -Turbolinks attaches a click handler to all `<a>` on the page. If your browser +Turbolinks attaches a click handler to all `<a>` tags on the page. If your browser supports [PushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState%28%29_method), Turbolinks will make an Ajax request for the page, parse the response, and @@ -385,7 +385,7 @@ $(document).ready -> ``` However, because Turbolinks overrides the normal page loading process, the -event that this relies on will not be fired. If you have code that looks like +event that this relies upon will not be fired. If you have code that looks like this, you must change your code to do this instead: ```coffeescript diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 6032d2e1a1..5463b05b60 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1 +1,8 @@ +* Namespace error pages' CSS selectors to stop the styles from bleeding into other pages + when using Turbolinks. + + *Jan Krutisch* + + + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/railties/CHANGELOG.md) for previous changes. diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 89f7b5991f..f8a923141d 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -386,7 +386,9 @@ module Rails def secrets @secrets ||= begin secrets = ActiveSupport::OrderedOptions.new - secrets.merge! Rails::Secrets.parse(config.paths["config/secrets"].existent, env: Rails.env) + files = config.paths["config/secrets"].existent + files = files.reject { |path| path.end_with?(".enc") } unless config.read_encrypted_secrets + secrets.merge! Rails::Secrets.parse(files, env: Rails.env) # Fallback to config.secret_key_base if secrets.secret_key_base isn't set secrets.secret_key_base ||= config.secret_key_base diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 4223c38146..dc0491035d 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -81,7 +81,6 @@ INFO initializer :set_secrets_root, group: :all do Rails::Secrets.root = root - Rails::Secrets.read_encrypted_secrets = config.read_encrypted_secrets end end end diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 9c4bd16aad..70dce268f1 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -7,7 +7,8 @@ class CodeStatistics #:nodoc: "Model tests", "Mailer tests", "Job tests", - "Integration tests"] + "Integration tests", + "System tests"] HEADERS = { lines: " Lines", code_lines: " LOC", classes: "Classes", methods: "Methods" } diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb index 588fb06b15..5bfbe58d97 100644 --- a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb +++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb @@ -140,7 +140,7 @@ module Rails class_option :mode, enum: %w( html list line column ), type: :string, desc: "Automatically put the sqlite3 database in the specified mode (html, list, line, column)." - class_option :header, type: :string + class_option :header, type: :boolean class_option :environment, aliases: "-e", type: :string, desc: "Specifies the environment to run this console under (test/development/production)." diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 9109be5e04..c715e5ac9f 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -304,7 +304,7 @@ module Rails return [] if options[:skip_sprockets] gems = [] - gems << GemfileEntry.github("sass-rails", "rails/sass-rails", nil, + gems << GemfileEntry.version("sass-rails", "~> 5.0", "Use SCSS for stylesheets") if !options[:skip_javascript] diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 46001f306a..aef66adb64 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -150,7 +150,7 @@ module Rails end def field_id(attribute_name) - [singular_table_name, attribute_name].join('_') + [singular_table_name, attribute_name].join("_") end def singular_table_name # :doc: diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 324843a5f5..f4717bb35b 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -403,9 +403,7 @@ module Rails end def delete_new_framework_defaults - # Sprockets owns the only new default for 5.1: if it's disabled, - # we don't want the file. - unless options[:update] && !options[:skip_sprockets] + unless options[:update] remove_file "config/initializers/new_framework_defaults_5_1.rb" end end diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css index 865300bef9..d05ea0f511 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css @@ -2,7 +2,7 @@ * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt index c6607dbb2b..52b3de5ee5 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt @@ -19,9 +19,9 @@ chdir APP_ROOT do <% unless options[:skip_yarn] %> # Install JavaScript dependencies if using Yarn # system('bin/yarn') - <% end %> <% unless options.skip_active_record -%> + # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') # cp 'config/database.yml.sample', 'config/database.yml' diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update.tt b/railties/lib/rails/generators/rails/app/templates/bin/update.tt index d23af018c7..d385b363c6 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/update.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/update.tt @@ -17,6 +17,7 @@ chdir APP_ROOT do system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') <% unless options.skip_active_record -%> + puts "\n== Updating database ==" system! 'bin/rails db:migrate' <% end -%> 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 d5d214052f..0b1d22228e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -34,6 +34,10 @@ module <%= app_const_base %> # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true +<%- elsif !depends_on_system_test? -%> + + # Don't generate system test files. + config.generators.system_tests = nil <%- end -%> end end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt index 5f5545c4c7..a0c7f44b60 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt @@ -5,6 +5,9 @@ # Once upgraded flip defaults one by one to migrate to the new default. # # Read the Guide for Upgrading Ruby on Rails for more info on each option. + +# Make `form_with` generate non-remote forms. +Rails.application.config.action_view.form_with_generates_remote_forms = false <%- unless options[:skip_sprockets] -%> # Unknown asset fallback will return the path passed in when the given diff --git a/railties/lib/rails/generators/rails/app/templates/public/404.html b/railties/lib/rails/generators/rails/app/templates/public/404.html index b612547fc2..2be3af26fc 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/404.html +++ b/railties/lib/rails/generators/rails/app/templates/public/404.html @@ -4,7 +4,7 @@ <title>The page you were looking for doesn't exist (404)</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> - body { + .rails-default-error-page { background-color: #EFEFEF; color: #2E2F30; text-align: center; @@ -12,13 +12,13 @@ margin: 0; } - div.dialog { + .rails-default-error-page div.dialog { width: 95%; max-width: 33em; margin: 4em auto 0; } - div.dialog > div { + .rails-default-error-page div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -31,13 +31,13 @@ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } - h1 { + .rails-default-error-page h1 { font-size: 100%; color: #730E15; line-height: 1.5em; } - div.dialog > p { + .rails-default-error-page div.dialog > p { margin: 0 0 1em; padding: 1em; background-color: #F7F7F7; @@ -54,7 +54,7 @@ </style> </head> -<body> +<body class="rails-default-error-page"> <!-- This file lives in public/404.html --> <div class="dialog"> <div> diff --git a/railties/lib/rails/generators/rails/app/templates/public/422.html b/railties/lib/rails/generators/rails/app/templates/public/422.html index a21f82b3bd..c08eac0d1d 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/422.html +++ b/railties/lib/rails/generators/rails/app/templates/public/422.html @@ -4,7 +4,7 @@ <title>The change you wanted was rejected (422)</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> - body { + .rails-default-error-page { background-color: #EFEFEF; color: #2E2F30; text-align: center; @@ -12,13 +12,13 @@ margin: 0; } - div.dialog { + .rails-default-error-page div.dialog { width: 95%; max-width: 33em; margin: 4em auto 0; } - div.dialog > div { + .rails-default-error-page div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -31,13 +31,13 @@ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } - h1 { + .rails-default-error-page h1 { font-size: 100%; color: #730E15; line-height: 1.5em; } - div.dialog > p { + .rails-default-error-page div.dialog > p { margin: 0 0 1em; padding: 1em; background-color: #F7F7F7; @@ -54,7 +54,7 @@ </style> </head> -<body> +<body class="rails-default-error-page"> <!-- This file lives in public/422.html --> <div class="dialog"> <div> diff --git a/railties/lib/rails/generators/rails/app/templates/public/500.html b/railties/lib/rails/generators/rails/app/templates/public/500.html index 061abc587d..78a030af22 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/500.html +++ b/railties/lib/rails/generators/rails/app/templates/public/500.html @@ -4,7 +4,7 @@ <title>We're sorry, but something went wrong (500)</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> - body { + .rails-default-error-page { background-color: #EFEFEF; color: #2E2F30; text-align: center; @@ -12,13 +12,13 @@ margin: 0; } - div.dialog { + .rails-default-error-page div.dialog { width: 95%; max-width: 33em; margin: 4em auto 0; } - div.dialog > div { + .rails-default-error-page div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -31,13 +31,13 @@ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } - h1 { + .rails-default-error-page h1 { font-size: 100%; color: #730E15; line-height: 1.5em; } - div.dialog > p { + .rails-default-error-page div.dialog > p { margin: 0 0 1em; padding: 1em; background-color: #F7F7F7; @@ -54,7 +54,7 @@ </style> </head> -<body> +<body class="rails-default-error-page"> <!-- This file lives in public/500.html --> <div class="dialog"> <div> diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb index 2a95712cd9..8b644f212c 100644 --- a/railties/lib/rails/secrets.rb +++ b/railties/lib/rails/secrets.rb @@ -14,12 +14,10 @@ module Rails end @cipher = "aes-128-gcm" - @read_encrypted_secrets = false @root = File # Wonky, but ensures `join` uses the current directory. class << self - attr_writer :root - attr_accessor :read_encrypted_secrets + attr_writer :root def parse(paths, env:) paths.each_with_object(Hash.new) do |path, all_secrets| @@ -88,11 +86,7 @@ module Rails def preprocess(path) if path.end_with?(".enc") - if @read_encrypted_secrets - decrypt(IO.binread(path)) - else - "" - end + decrypt(IO.binread(path)) else IO.read(path) end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 14433fbba0..07c6afed7f 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -693,6 +693,66 @@ module ApplicationTests assert_match(/label/, last_response.body) end + test "form_with can be configured with form_with_generates_remote_forms" do + app_file "config/initializers/form_builder.rb", <<-RUBY + Rails.configuration.action_view.form_with_generates_remote_forms = false + RUBY + + app_file "app/models/post.rb", <<-RUBY + class Post + include ActiveModel::Model + attr_accessor :name + end + RUBY + + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ApplicationController + def index + render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>" + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + RUBY + + app "development" + + get "/posts" + assert_no_match(/data-remote/, last_response.body) + end + + test "form_with generates remote forms by default" do + app_file "app/models/post.rb", <<-RUBY + class Post + include ActiveModel::Model + attr_accessor :name + end + RUBY + + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ApplicationController + def index + render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>" + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + RUBY + + app "development" + + get "/posts" + assert_match(/data-remote/, last_response.body) + end + test "default method for update can be changed" do app_file "app/models/post.rb", <<-RUBY class Post diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index 00f6620188..2c9770e147 100644 --- a/railties/test/application/rake/migrations_test.rb +++ b/railties/test/application/rake/migrations_test.rb @@ -77,7 +77,7 @@ module ApplicationTests assert_equal "Schema migrations table does not exist yet.\n", output end - test "test migration status" do + test "migration status" do Dir.chdir(app_path) do `bin/rails generate model user username:string password:string; bin/rails generate migration add_email_to_users email:string; @@ -117,7 +117,7 @@ module ApplicationTests end end - test "test migration status after rollback and redo" do + test "migration status after rollback and redo" do Dir.chdir(app_path) do `bin/rails generate model user username:string password:string; bin/rails generate migration add_email_to_users email:string; @@ -142,6 +142,62 @@ module ApplicationTests end end + test "migration status after rollback and forward" do + Dir.chdir(app_path) do + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rails db:migrate` + + output = `bin/rails db:migrate:status` + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + + `bin/rails db:rollback STEP=2` + output = `bin/rails db:migrate:status` + + assert_match(/down\s+\d{14}\s+Create users/, output) + assert_match(/down\s+\d{14}\s+Add email to users/, output) + + `bin/rails db:forward STEP=2` + output = `bin/rails db:migrate:status` + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + end + end + + test "raise error on any move when current migration does not exist" do + Dir.chdir(app_path) do + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rails db:migrate + rm db/migrate/*email*.rb` + + output = `bin/rails db:migrate:status` + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) + + output = `bin/rails db:rollback 2>&1` + assert_match(/rails aborted!/, output) + assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output) + assert_match(/No migration with version number\s\d{14}\./, output) + + output = `bin/rails db:migrate:status` + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) + + output = `bin/rails db:forward 2>&1` + assert_match(/rails aborted!/, output) + assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output) + assert_match(/No migration with version number\s\d{14}\./, output) + + output = `bin/rails db:migrate:status` + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) + end + end + test "migration status after rollback and redo without timestamps" do add_to_config("config.active_record.timestamped_migrations = false") @@ -224,7 +280,7 @@ module ApplicationTests end end - test "test migration status migrated file is deleted" do + test "migration status migrated file is deleted" do Dir.chdir(app_path) do `bin/rails generate model user username:string password:string; bin/rails generate migration add_email_to_users email:string; @@ -232,7 +288,6 @@ module ApplicationTests rm db/migrate/*email*.rb` output = `bin/rails db:migrate:status` - File.write("test.txt", output) assert_match(/up\s+\d{14}\s+Create users/, output) assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index a8e3a7ec5b..23b259b503 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -323,7 +323,7 @@ module ApplicationTests assert true end - test "test line filter does not run this" do + test "line filter does not run this" do assert true end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 7965bd68d0..8a51beb380 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -410,7 +410,6 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_match(/config\.assets\.js_compressor = :uglifier/, content) assert_no_match(/config\.assets\.css_compressor = :sass/, content) end - assert_no_file "config/initializers/new_framework_defaults_5_1.rb" end def test_generator_if_skip_yarn_is_given @@ -452,6 +451,17 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_does_not_generate_system_test_files_if_skip_system_test_is_given + run_generator [destination_root, "--skip_system_test"] + + Dir.chdir(destination_root) do + quietly { `./bin/rails g scaffold User` } + + assert_no_file("test/application_system_test_case.rb") + assert_no_file("test/system/users_test.rb") + end + end + def test_generator_if_api_is_given run_generator [destination_root, "--api"] assert_file "Gemfile" do |content| diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb index 953408f0b4..36c8ef1fd9 100644 --- a/railties/test/secrets_test.rb +++ b/railties/test/secrets_test.rb @@ -9,22 +9,22 @@ class Rails::SecretsTest < ActiveSupport::TestCase def setup build_app - - @old_read_encrypted_secrets, Rails::Secrets.read_encrypted_secrets = - Rails::Secrets.read_encrypted_secrets, true end def teardown - Rails::Secrets.read_encrypted_secrets = @old_read_encrypted_secrets - teardown_app end test "setting read to false skips parsing" do - Rails::Secrets.read_encrypted_secrets = false + run_secrets_generator do + Rails::Secrets.write(<<-end_of_secrets) + test: + yeah_yeah: lets-walk-in-the-cool-evening-light + end_of_secrets - Dir.chdir(app_path) do - assert_equal Hash.new, Rails::Secrets.parse(%w( config/secrets.yml.enc ), env: "production") + Rails.application.config.read_encrypted_secrets = false + Rails.application.instance_variable_set(:@secrets, nil) # Dance around caching ๐๐บ + assert_not Rails.application.secrets.yeah_yeah end end @@ -90,11 +90,27 @@ class Rails::SecretsTest < ActiveSupport::TestCase end_of_secrets Rails.application.config.root = app_path + Rails.application.config.read_encrypted_secrets = true Rails.application.instance_variable_set(:@secrets, nil) # Dance around caching ๐๐บ assert_equal "lets-walk-in-the-cool-evening-light", Rails.application.secrets.yeah_yeah end end + test "refer secrets inside env config" do + run_secrets_generator do + Rails::Secrets.write(<<-end_of_yaml) + production: + some_secret: yeah yeah + end_of_yaml + + add_to_env_config "production", <<-end_of_config + config.dereferenced_secret = Rails.application.secrets.some_secret + end_of_config + + assert_equal "yeah yeah\n", `bin/rails runner -e production "puts Rails.application.config.dereferenced_secret"` + end + end + private def run_secrets_generator Dir.chdir(app_path) do |