diff options
183 files changed, 2327 insertions, 1010 deletions
diff --git a/.travis.yml b/.travis.yml index 5b8cfa9dbf..19fc899a8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,6 @@ services: addons: postgresql: "9.4" - apt: - packages: - - postgresql-9.4 bundler_args: --without test --jobs 3 --retry 3 before_install: @@ -42,7 +42,7 @@ gem "json", ">= 2.0.0" gem "rubocop", ">= 0.47", require: false group :doc do - gem "sdoc", "1.0.0.rc1" + gem "sdoc", "> 1.0.0.rc1", "< 2.0" gem "redcarpet", "~> 3.2.3", platforms: :ruby gem "w3c_validators" gem "kindlerb", "~> 1.2.0" diff --git a/Gemfile.lock b/Gemfile.lock index 4feb8e2968..445bab1ca3 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) @@ -264,7 +264,7 @@ GEM rainbow (2.2.1) rake (12.0.0) rb-fsevent (0.9.8) - rdoc (5.0.0) + rdoc (5.1.0) redcarpet (3.2.3) redis (3.3.2) redis-namespace (1.5.2) @@ -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) @@ -298,8 +298,8 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sdoc (1.0.0.rc1) - rdoc (= 5.0.0) + sdoc (1.0.0.rc2) + rdoc (~> 5.0) selenium-webdriver (3.0.5) childprocess (~> 0.5) rubyzip (~> 1.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) @@ -417,7 +417,7 @@ DEPENDENCIES resque-scheduler rubocop (>= 0.47) sass-rails - sdoc (= 1.0.0.rc1) + sdoc (> 1.0.0.rc1, < 2.0) sequel sidekiq sneakers diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index d5bd58cfdb..2bf11da463 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1 +1,8 @@ +* 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/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index b0152aff03..87ba743f3d 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -52,6 +52,12 @@ module ActionMailer class Preview extend ActiveSupport::DescendantsTracker + attr_reader :params + + def initialize(params = {}) + @params = params + end + class << self # Returns all mailer preview classes. def all @@ -62,8 +68,8 @@ module ActionMailer # Returns the mail object for the given email name. The registered preview # interceptors will be informed so that they can transform the message # as they would if the mail was actually being delivered. - def call(email) - preview = new + def call(email, params = {}) + preview = new(params) message = preview.public_send(email) inform_preview_interceptors(message) message diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 61960d411d..ebb97078e6 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -968,3 +968,19 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase end end end + +class BasePreviewTest < ActiveSupport::TestCase + class BaseMailerPreview < ActionMailer::Preview + def welcome + BaseMailer.welcome(params) + end + end + + test "has access to params" do + params = { name: "World" } + + assert_called_with(BaseMailer, :welcome, [params]) do + BaseMailerPreview.call(:welcome, params) + end + end +end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 51cec82801..cc908dcc43 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -10,4 +10,5 @@ *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/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/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 0cf010f59e..74a3afab44 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -326,11 +326,11 @@ module Mime def ref; end - def respond_to_missing?(method, include_private = false) - method.to_s.ends_with? "?" - end - private + def respond_to_missing?(method, _) + method.to_s.ends_with? "?" + end + def method_missing(method, *args) false if method.to_s.ends_with? "?" end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 79a2ef965b..7c585dbe68 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -85,7 +85,7 @@ module ActionDispatch def set_binary_encoding(params) action = params[:action] - if controller_class.binary_params_for?(action) + if binary_params_for?(action) ActionDispatch::Request::Utils.each_param_value(params) do |param| param.force_encoding ::Encoding::ASCII_8BIT end @@ -93,6 +93,12 @@ module ActionDispatch params end + def binary_params_for?(action) + controller_class.binary_params_for?(action) + rescue NameError + false + end + def parse_formatted_parameters(parsers) return yield if content_length.zero? || content_mime_type.nil? diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index ffcd875b4d..e624b4c80d 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -59,11 +59,11 @@ module ActionDispatch end private - def escape(component, pattern) # :doc: + def escape(component, pattern) component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII) end - def percent_encode(unsafe) # :doc: + def percent_encode(unsafe) safe = EMPTY.dup unsafe.each_byte { |b| safe << DEC2HEX[b] } safe @@ -84,6 +84,10 @@ module ActionDispatch ENCODER.escape_fragment(fragment.to_s) end + # Replaces any escaped sequences with their unescaped representations. + # + # uri = "/topics?title=Ruby%20on%20Rails" + # unescape_uri(uri) #=> "/topics?title=Ruby on Rails" def self.unescape_uri(uri) ENCODER.unescape_uri(uri) end 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/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb index ee847eaeed..c1423f770f 100644 --- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -19,7 +19,8 @@ module ActionDispatch end end - def respond_to_missing?(method, include_private = false) + private + def respond_to_missing?(method, _) super || @helpers.respond_to?(method) end @@ -32,7 +33,7 @@ module ActionDispatch @helpers.#{method}(*args) end RUBY - send(method, *args) + public_send(method, *args) else super 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/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 2e2db98ad6..2416c58817 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -385,14 +385,15 @@ module ActionDispatch integration_session.default_url_options = options end - def respond_to_missing?(method, include_private = false) - integration_session.respond_to?(method, include_private) || super + private + def respond_to_missing?(method, _) + integration_session.respond_to?(method) || super end # Delegate unhandled messages to the current session instance. - def method_missing(sym, *args, &block) - if integration_session.respond_to?(sym) - integration_session.__send__(sym, *args, &block).tap do + def method_missing(method, *args, &block) + if integration_session.respond_to?(method) + integration_session.public_send(method, *args, &block).tap do copy_session_variables! end else 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 794e057b85..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 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/CHANGELOG.md b/actionview/CHANGELOG.md index c514e757c8..d478f4c437 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1 +1,7 @@ +* Update `distance_of_time_in_words` helper to display better error messages + for bad input. + + *Jay Hayes* + + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionview/CHANGELOG.md) for previous changes. diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 09dc6ef6bd..3f43465aa4 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -95,8 +95,8 @@ module ActionView scope: :'datetime.distance_in_words' }.merge!(options) - from_time = from_time.to_time if from_time.respond_to?(:to_time) - to_time = to_time.to_time if to_time.respond_to?(:to_time) + from_time = normalize_distance_of_time_argument_to_time(from_time) + to_time = normalize_distance_of_time_argument_to_time(to_time) from_time, to_time = to_time, from_time if from_time > to_time distance_in_minutes = ((to_time - from_time) / 60.0).round distance_in_seconds = (to_time - from_time).round @@ -130,22 +130,18 @@ module ActionView # 60 days up to 365 days when 86400...525600 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round else - if from_time.acts_like?(:time) && to_time.acts_like?(:time) - fyear = from_time.year - fyear += 1 if from_time.month >= 3 - tyear = to_time.year - tyear -= 1 if to_time.month < 3 - leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count { |x| Date.leap?(x) } - minute_offset_for_leap_year = leap_years * 1440 - # Discount the leap year days when calculating year distance. - # e.g. if there are 20 leap year days between 2 dates having the same day - # and month then the based on 365 days calculation - # the distance in years will come out to over 80 years when in written - # English it would read better as about 80 years. - minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year - else - minutes_with_offset = distance_in_minutes - end + from_year = from_time.year + from_year += 1 if from_time.month >= 3 + to_year = to_time.year + to_year -= 1 if to_time.month < 3 + leap_years = (from_year > to_year) ? 0 : (from_year..to_year).count { |x| Date.leap?(x) } + minute_offset_for_leap_year = leap_years * 1440 + # Discount the leap year days when calculating year distance. + # e.g. if there are 20 leap year days between 2 dates having the same day + # and month then the based on 365 days calculation + # the distance in years will come out to over 80 years when in written + # English it would read better as about 80 years. + minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year remainder = (minutes_with_offset % MINUTES_IN_YEAR) distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR) if remainder < MINUTES_IN_QUARTER_YEAR @@ -687,6 +683,18 @@ module ActionView content_tag("time".freeze, content, options.reverse_merge(datetime: datetime), &block) end + + private + + def normalize_distance_of_time_argument_to_time(value) + if value.is_a?(Numeric) + Time.at(value) + elsif value.respond_to?(:to_time) + value.to_time + else + raise ArgumentError, "#{value.inspect} can't be converted to a Time value" + end + end end class DateTimeSelector #:nodoc: diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 1a419508e5..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 # @@ -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/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index 0895533a60..aa420c4b66 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -149,7 +149,7 @@ module ActionView end value = options.fetch(:selected) { value(object) } - select = content_tag("select", add_options(option_tags, options, value), html_options) + select = content_tag("select", add_options(option_tags, options, value), html_options.except!("skip_default_ids", "allow_method_names_outside_object")) if html_options["multiple"] && options.fetch(:include_hidden, true) tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 304db38060..a6857101b9 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -542,7 +542,7 @@ module ActionView return false unless request.get? || request.head? - check_parameters ||= !options.is_a?(String) && options.try(:delete, :check_parameters) + check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters) url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY) # We ignore any extra parameters in the request_uri if the @@ -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/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index d257147e1f..a259752d6a 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -128,6 +128,16 @@ class DateHelperTest < ActionView::TestCase assert_distance_of_time_in_words(from) end + def test_distance_in_words_with_nil_input + assert_raises(ArgumentError) { distance_of_time_in_words(nil) } + assert_raises(ArgumentError) { distance_of_time_in_words(0, nil) } + end + + def test_distance_in_words_with_mixed_argument_types + assert_equal "1 minute", distance_of_time_in_words(0, Time.at(60)) + assert_equal "10 minutes", distance_of_time_in_words(Time.at(600), 0) + end + def test_distance_in_words_with_mathn_required # test we avoid Integer#/ (redefined by mathn) silence_warnings { require "mathn" } diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index a4a2966ff9..bff0643fb0 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -302,6 +302,7 @@ class FormWithActsLikeFormForTest < FormWithTest concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) + concat f.select(:category, %w( animal economy sports )) concat f.submit("Create post") concat f.button("Create post") concat f.button { @@ -315,6 +316,7 @@ class FormWithActsLikeFormForTest < FormWithTest "<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' />" \ + "<select name='post[category]'><option value='animal'>animal</option>\n<option value='economy'>economy</option>\n<option value='sports'>sports</option></select>" \ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \ "<button name='button' type='submit'>Create post</button>" \ "<button name='button' type='submit'><span>Create post</span></button>" @@ -729,6 +731,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..6adfa95dd1 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 @@ -505,6 +509,12 @@ class UrlHelperTest < ActiveSupport::TestCase assert !current_page?("http://www.example.com/", check_parameters: true) end + def test_current_page_considering_params_when_options_does_not_respond_to_to_hash + @request = request_for_url("/?order=desc&page=1") + + assert !current_page?(:back, check_parameters: false) + end + def test_current_page_with_params_that_match @request = request_for_url("/?order=desc&page=1") @@ -672,13 +682,6 @@ class UrlHelperTest < ActiveSupport::TestCase def request_forgery_protection_token "form_token" end - - private - def sort_query_string_params(uri) - path, qs = uri.split("?") - qs = qs.split("&").sort.join("&") if qs - qs ? "#{path}?#{qs}" : path - end end class UrlHelperControllerTest < ActionController::TestCase @@ -876,6 +879,11 @@ class WorkshopsController < ActionController::Base @workshop = Workshop.new(params[:id]) render inline: "<%= url_for(@workshop) %>\n<%= link_to('Workshop', @workshop) %>" end + + def edit + @workshop = Workshop.new(params[:id]) + render inline: "<%= current_page?(@workshop) %>" + end end class SessionsController < ActionController::Base @@ -940,4 +948,11 @@ class PolymorphicControllerTest < ActionController::TestCase get :edit, params: { workshop_id: 1, id: 1, format: "json" } assert_equal %{/workshops/1/sessions/1.json\n<a href="/workshops/1/sessions/1.json">Session</a>}, @response.body end + + def test_current_page_when_options_does_not_respond_to_to_hash + @controller = WorkshopsController.new + + get :edit, params: { id: 1 } + assert_equal "false", @response.body + end end diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 6b4f93df8b..77dfdefc05 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1 +1,8 @@ +* 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/CHANGELOG.md b/activemodel/CHANGELOG.md index cdba0cee12..e707a65147 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -4,27 +4,27 @@ Change `#values` to only return the not empty values. - Example: - - # Before - person = Person.new - person.errors.keys # => [] - person.errors.values # => [] - person.errors.messages # => {} - person.errors[:name] # => [] - person.errors.messages # => {:name => []} - person.errors.keys # => [:name] - person.errors.values # => [[]] - - # After - person = Person.new - person.errors.keys # => [] - person.errors.values # => [] - person.errors.messages # => {} - person.errors[:name] # => [] - person.errors.messages # => {:name => []} - person.errors.keys # => [] - person.errors.values # => [] + Example: + + # Before + person = Person.new + person.errors.keys # => [] + person.errors.values # => [] + person.errors.messages # => {} + person.errors[:name] # => [] + person.errors.messages # => {:name => []} + person.errors.keys # => [:name] + person.errors.values # => [[]] + + # After + person = Person.new + person.errors.keys # => [] + person.errors.values # => [] + person.errors.messages # => {} + person.errors[:name] # => [] + person.errors.messages # => {:name => []} + person.errors.keys # => [] + person.errors.values # => [] *bogdanvlviv* 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/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/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index d7e6bf3707..f5b1ad721c 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -107,7 +107,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validates_format_of_with_lambda - Topic.validates_format_of :content, with: lambda { |topic| topic.title == "digit" ? /\A\d+\Z/ : /\A\S+\Z/ } + Topic.validates_format_of :content, with: lambda { |topic| topic.title == "digit" ? /\A\d+\z/ : /\A\S+\z/ } t = Topic.new t.title = "digit" @@ -119,7 +119,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validates_format_of_without_lambda - Topic.validates_format_of :content, without: lambda { |topic| topic.title == "characters" ? /\A\d+\Z/ : /\A\S+\Z/ } + Topic.validates_format_of :content, without: lambda { |topic| topic.title == "characters" ? /\A\d+\z/ : /\A\S+\z/ } t = Topic.new t.title = "characters" @@ -131,7 +131,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validates_format_of_for_ruby_class - Person.validates_format_of :karma, with: /\A\d+\Z/ + Person.validates_format_of :karma, with: /\A\d+\z/ p = Person.new p.karma = "Pixies" diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 9e3b686bbb..68f0638e32 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,27 @@ +* 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. diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index a2432e389a..0e61dbfb00 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -22,7 +22,7 @@ module ActiveRecord end def default(&block) - writer(instance_exec(&block)) if reader.nil? + writer(owner.instance_exec(&block)) if reader.nil? end def reset 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/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 5d6676f0df..74a4d515c2 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -1121,6 +1121,19 @@ module ActiveRecord delegate(*delegate_methods, to: :scope) + module DelegateExtending # :nodoc: + private + def method_missing(method, *args, &block) + extending_values = association_scope.extending_values + if extending_values.any? && (extending_values - self.class.included_modules).any? + self.class.include(*extending_values) + public_send(method, *args, &block) + else + super + end + end + end + private def find_nth_with_limit(index, limit) @@ -1141,21 +1154,16 @@ module ActiveRecord @association.find_from_target? end + def association_scope + @association.association_scope + end + def exec_queries load_target end def respond_to_missing?(method, _) - scope.respond_to?(method) || super - end - - def method_missing(method, *args, &block) - if scope.respond_to?(method) && scope.extending_values.any? - extend(*scope.extending_values) - public_send(method, *args, &block) - else - super - end + association_scope.respond_to?(method) || super end end end 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..61bf5477aa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -506,14 +506,16 @@ module ActiveRecord # +conn+: an AbstractAdapter object, which was obtained by earlier by # calling #checkout on this pool. def checkin(conn) - synchronize do - remove_connection_from_thread_cache conn + conn.lock.synchronize do + synchronize do + remove_connection_from_thread_cache conn - conn._run_checkin_callbacks do - conn.expire - end + conn._run_checkin_callbacks do + conn.expire + end - @available.add conn + @available.add conn + end end end @@ -857,9 +859,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_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/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 45e400b75b..af55cfe2f6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -10,8 +10,6 @@ module ActiveRecord # Establishes a connection to the database that's used by all Active Record objects. def mysql2_connection(config) config = config.symbolize_keys - - config[:username] = "root" if config[:username].nil? config[:flags] ||= 0 if config[:flags].kind_of? Array diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb index 730e7c7137..44a7338bf5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -6,50 +6,8 @@ module ActiveRecord true end - def disable_referential_integrity(&block) # :nodoc: + def disable_referential_integrity # :nodoc: if supports_disable_referential_integrity? - if supports_alter_constraint? - disable_referential_integrity_with_alter_constraint(&block) - else - disable_referential_integrity_with_disable_trigger(&block) - end - else - yield - end - end - - private - - def disable_referential_integrity_with_alter_constraint - tables_constraints = execute(<<-SQL).values - SELECT table_name, constraint_name - FROM information_schema.table_constraints - WHERE constraint_type = 'FOREIGN KEY' - AND is_deferrable = 'NO' - SQL - - execute( - tables_constraints.collect { |table, constraint| - "ALTER TABLE #{quote_table_name(table)} ALTER CONSTRAINT #{constraint} DEFERRABLE" - }.join(";") - ) - - begin - transaction do - execute("SET CONSTRAINTS ALL DEFERRED") - - yield - end - ensure - execute( - tables_constraints.collect { |table, constraint| - "ALTER TABLE #{quote_table_name(table)} ALTER CONSTRAINT #{constraint} NOT DEFERRABLE" - }.join(";") - ) - end - end - - def disable_referential_integrity_with_disable_trigger original_exception = nil begin @@ -81,7 +39,10 @@ Rails needs superuser privileges to disable referential integrity. end rescue ActiveRecord::ActiveRecordError end + else + yield end + end end end 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 02a6da2f71..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: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 7ae9bd9a5f..033d81916e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -314,12 +314,6 @@ module ActiveRecord postgresql_version >= 90400 end - def supports_alter_constraint? - # PostgreSQL 9.4 introduces ALTER TABLE ... ALTER CONSTRAINT but it has a bug and fixed in 9.4.2 - # https://www.postgresql.org/docs/9.4/static/release-9-4-2.html - postgresql_version >= 90402 - end - def get_advisory_lock(lock_id) # :nodoc: unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") @@ -561,7 +555,7 @@ module ActiveRecord end def has_default_function?(default_value, default) - !default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default) + !default_value && %r{\w+\(.*\)|\(.*\)::\w+|CURRENT_DATE|CURRENT_TIMESTAMP}.match?(default) end def load_additional_types(type_map, oids = nil) 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 d9912cfb22..3a9625092e 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,15 +1,14 @@ module ActiveRecord module DynamicMatchers #:nodoc: - def respond_to_missing?(name, include_private = false) - if self == Base - super - else - match = Method.match(self, name) - match && match.valid? || super - end - end - private + def respond_to_missing?(name, _) + if self == Base + super + else + match = Method.match(self, name) + match && match.valid? || super + end + end def method_missing(name, *arguments, &block) match = Method.match(self, name) 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/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 03103bba98..f9cf59b283 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -92,10 +92,6 @@ module ActiveRecord send(method, args, &block) end - def respond_to_missing?(*args) # :nodoc: - super || delegate.respond_to?(*args) - end - ReversibleAndIrreversibleMethods.each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -225,10 +221,14 @@ module ActiveRecord [:add_foreign_key, reversed_args] end + def respond_to_missing?(method, _) + super || delegate.respond_to?(method) + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) - if @delegate.respond_to?(method) - @delegate.send(method, *args, &block) + if delegate.respond_to?(method) + delegate.public_send(method, *args, &block) else super 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/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index f3e2df8786..711099e9e1 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -93,7 +93,7 @@ db_namespace = namespace :db do # desc 'Runs the "up" for a given migration VERSION.' task up: [:environment, :load_config] do - raise "VERSION is required" if ENV["VERSION"] && ENV["VERSION"].empty? + raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) @@ -102,7 +102,7 @@ db_namespace = namespace :db do # desc 'Runs the "down" for a given migration VERSION.' task down: [:environment, :load_config] do - raise "VERSION is required - To go down one migration, use db:rollback" if ENV["VERSION"] && ENV["VERSION"].empty? + raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty? version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace["_dump"].invoke diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 50378f9d99..257ae04ff4 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -25,6 +25,8 @@ module ActiveRecord def inherited(child_class) child_class.initialize_relation_delegate_cache + delegate = child_class.relation_delegate_class(ActiveRecord::Associations::CollectionProxy) + delegate.include ActiveRecord::Associations::CollectionProxy::DelegateExtending super end end @@ -109,12 +111,10 @@ module ActiveRecord end end - def respond_to_missing?(method, include_private = false) - super || @klass.respond_to?(method, include_private) || - arel.respond_to?(method, include_private) - end - private + def respond_to_missing?(method, _) + super || @klass.respond_to?(method) || arel.respond_to?(method) + end def method_missing(method, *args, &block) if @klass.respond_to?(method) 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 9141d9c537..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ /dev/null @@ -1,71 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class AssociationQueryHandler # :nodoc: - def self.value_for(table, column, value) - associated_table = table.associated_table(column) - 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 - klass.new(associated_table, value) - end - - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - predicate_builder.build_from_hash(value.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 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/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_value.rb index c2f136256b..9bb2f8c8dc 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -1,28 +1,5 @@ module ActiveRecord class PredicateBuilder - class PolymorphicArrayHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - predicates = value.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 @@ -35,7 +12,7 @@ module ActiveRecord type_to_ids_mapping.map do |type, ids| { associated_table.association_foreign_type.to_s => type, - associated_table.association_foreign_key.to_s => ids + associated_table.association_foreign_key.to_s => ids.size > 1 ? ids : ids.first } end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 46fa8a70a3..45110a79cd 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -164,7 +164,7 @@ module ActiveRecord def migrate raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? - verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true + verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose 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/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/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb index c5c540cebc..0ff04bfa27 100644 --- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -1,150 +1,111 @@ require "cases/helper" require "support/connection_helper" -if ActiveRecord::Base.connection.respond_to?(:supports_alter_constraint?) && - ActiveRecord::Base.connection.supports_alter_constraint? - class PostgreSQLReferentialIntegrityWithAlterConstraintTest < ActiveRecord::PostgreSQLTestCase - self.use_transactional_tests = false +class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false - include ConnectionHelper + include ConnectionHelper - IS_REFERENTIAL_INTEGRITY_SQL = lambda do |sql| - sql.match(/SET CONSTRAINTS ALL DEFERRED/) - end + IS_REFERENTIAL_INTEGRITY_SQL = lambda do |sql| + sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/) + end - module ProgrammerMistake - def execute(sql) - if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) - raise ArgumentError, "something is not right." - else - super - end + module MissingSuperuserPrivileges + def execute(sql) + if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) + super "BROKEN;" rescue nil # put transaction in broken state + raise ActiveRecord::StatementInvalid, "PG::InsufficientPrivilege" + else + super end end + end - def setup - @connection = ActiveRecord::Base.connection - end - - def teardown - reset_connection - end - - def test_errors_bubble_up - @connection.extend ProgrammerMistake - - assert_raises ArgumentError do - @connection.disable_referential_integrity {} + module ProgrammerMistake + def execute(sql) + if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) + raise ArgumentError, "something is not right." + else + super end end end -else - class PostgreSQLReferentialIntegrityWithDisableTriggerTest < ActiveRecord::PostgreSQLTestCase - self.use_transactional_tests = false - include ConnectionHelper + def setup + @connection = ActiveRecord::Base.connection + end - IS_REFERENTIAL_INTEGRITY_SQL = lambda do |sql| - sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/) + def teardown + reset_connection + if ActiveRecord::Base.connection.is_a?(MissingSuperuserPrivileges) + raise "MissingSuperuserPrivileges patch was not removed" end + end - module MissingSuperuserPrivileges - def execute(sql) - if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) - super "BROKEN;" rescue nil # put transaction in broken state - raise ActiveRecord::StatementInvalid, "PG::InsufficientPrivilege" - else - super - end - end - end + def test_should_reraise_invalid_foreign_key_exception_and_show_warning + @connection.extend MissingSuperuserPrivileges - module ProgrammerMistake - def execute(sql) - if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) - raise ArgumentError, "something is not right." - else - super + warning = capture(:stderr) do + e = assert_raises(ActiveRecord::InvalidForeignKey) do + @connection.disable_referential_integrity do + raise ActiveRecord::InvalidForeignKey, "Should be re-raised" end end + assert_equal "Should be re-raised", e.message end + assert_match (/WARNING: Rails was not able to disable referential integrity/), warning + assert_match (/cause: PG::InsufficientPrivilege/), warning + end - def setup - @connection = ActiveRecord::Base.connection - end - - def teardown - reset_connection - if ActiveRecord::Base.connection.is_a?(MissingSuperuserPrivileges) - raise "MissingSuperuserPrivileges patch was not removed" - end - end - - def test_should_reraise_invalid_foreign_key_exception_and_show_warning - @connection.extend MissingSuperuserPrivileges + def test_does_not_print_warning_if_no_invalid_foreign_key_exception_was_raised + @connection.extend MissingSuperuserPrivileges - warning = capture(:stderr) do - e = assert_raises(ActiveRecord::InvalidForeignKey) do - @connection.disable_referential_integrity do - raise ActiveRecord::InvalidForeignKey, "Should be re-raised" - end + warning = capture(:stderr) do + e = assert_raises(ActiveRecord::StatementInvalid) do + @connection.disable_referential_integrity do + raise ActiveRecord::StatementInvalid, "Should be re-raised" end - assert_equal "Should be re-raised", e.message end - assert_match (/WARNING: Rails was not able to disable referential integrity/), warning - assert_match (/cause: PG::InsufficientPrivilege/), warning + assert_equal "Should be re-raised", e.message end + assert warning.blank?, "expected no warnings but got:\n#{warning}" + end - def test_does_not_print_warning_if_no_invalid_foreign_key_exception_was_raised - @connection.extend MissingSuperuserPrivileges + def test_does_not_break_transactions + @connection.extend MissingSuperuserPrivileges - warning = capture(:stderr) do - e = assert_raises(ActiveRecord::StatementInvalid) do - @connection.disable_referential_integrity do - raise ActiveRecord::StatementInvalid, "Should be re-raised" - end - end - assert_equal "Should be re-raised", e.message + @connection.transaction do + @connection.disable_referential_integrity do + assert_transaction_is_not_broken end - assert warning.blank?, "expected no warnings but got:\n#{warning}" + assert_transaction_is_not_broken end + end - def test_does_not_break_transactions - @connection.extend MissingSuperuserPrivileges + def test_does_not_break_nested_transactions + @connection.extend MissingSuperuserPrivileges - @connection.transaction do + @connection.transaction do + @connection.transaction(requires_new: true) do @connection.disable_referential_integrity do assert_transaction_is_not_broken end - assert_transaction_is_not_broken end + assert_transaction_is_not_broken end + end - def test_does_not_break_nested_transactions - @connection.extend MissingSuperuserPrivileges + def test_only_catch_active_record_errors_others_bubble_up + @connection.extend ProgrammerMistake - @connection.transaction do - @connection.transaction(requires_new: true) do - @connection.disable_referential_integrity do - assert_transaction_is_not_broken - end - end - assert_transaction_is_not_broken - end + assert_raises ArgumentError do + @connection.disable_referential_integrity {} end + end - def test_only_catch_active_record_errors_others_bubble_up - @connection.extend ProgrammerMistake + private - assert_raises ArgumentError do - @connection.disable_referential_integrity {} - end + def assert_transaction_is_not_broken + assert_equal 1, @connection.select_value("SELECT 1") end - - private - - def assert_transaction_is_not_broken - assert_equal 1, @connection.select_value("SELECT 1") - end - end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 5b08ba1358..c8b26232b6 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -136,6 +136,24 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal david, ship.developer end + def test_default_with_lambda + model = Class.new(ActiveRecord::Base) do + self.table_name = "ships" + def self.name; "Temp"; end + belongs_to :developer, default: -> { default_developer } + + def default_developer + Developer.first + end + end + + ship = model.create! + assert_equal developers(:david), ship.developer + + ship = model.create!(developer: developers(:jamis)) + assert_equal developers(:jamis), ship.developer + end + def test_default_scope_on_relations_is_not_cached counter = 0 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..6a479a344c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -2037,12 +2037,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal client_association.new.attributes, client_association.send(:new).attributes end - def test_respond_to_private_class_methods - client_association = companies(:first_firm).clients - assert !client_association.respond_to?(:private_method) - assert client_association.respond_to?(:private_method, true) - end - def test_creating_using_primary_key firm = Firm.all.merge!(order: "id").first client = firm.clients_using_primary_key.create!(name: "test") @@ -2447,15 +2441,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 +2504,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/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb index 2aca3523c4..6d3757f467 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -7,7 +7,7 @@ require "models/categorization" require "models/person" class LeftOuterJoinAssociationTest < ActiveRecord::TestCase - fixtures :authors, :essays, :posts, :comments, :categorizations, :people, :author_addresses + fixtures :authors, :author_addresses, :essays, :posts, :comments, :categorizations, :people def test_construct_finder_sql_applies_aliases_tables_on_association_conditions result = Author.left_outer_joins(:thinking_posts, :welcome_posts).to_a 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/batches_test.rb b/activerecord/test/cases/batches_test.rb index f7e21faf0f..1a54c02fac 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -35,12 +35,10 @@ class EachTest < ActiveRecord::TestCase end end - if Enumerator.method_defined? :size - def test_each_should_return_a_sized_enumerator - assert_equal 11, Post.find_each(batch_size: 1).size - assert_equal 5, Post.find_each(batch_size: 2, start: 7).size - assert_equal 11, Post.find_each(batch_size: 10_000).size - end + def test_each_should_return_a_sized_enumerator + assert_equal 11, Post.find_each(batch_size: 1).size + assert_equal 5, Post.find_each(batch_size: 2, start: 7).size + assert_equal 11, Post.find_each(batch_size: 10_000).size end def test_each_enumerator_should_execute_one_query_per_batch @@ -515,14 +513,12 @@ class EachTest < ActiveRecord::TestCase assert_equal 2, person.reload.author_id # incremented only once end - if Enumerator.method_defined? :size - def test_find_in_batches_should_return_a_sized_enumerator - assert_equal 11, Post.find_in_batches(batch_size: 1).size - assert_equal 6, Post.find_in_batches(batch_size: 2).size - assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size - assert_equal 4, Post.find_in_batches(batch_size: 3).size - assert_equal 1, Post.find_in_batches(batch_size: 10_000).size - end + def test_find_in_batches_should_return_a_sized_enumerator + assert_equal 11, Post.find_in_batches(batch_size: 1).size + assert_equal 6, Post.find_in_batches(batch_size: 2).size + assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size + assert_equal 4, Post.find_in_batches(batch_size: 3).size + assert_equal 1, Post.find_in_batches(batch_size: 10_000).size end [true, false].each do |load| diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index 381a78a8e2..f344c77691 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -11,16 +11,42 @@ module ActiveRecord fixtures :developers, :projects, :developers_projects, :topics, :comments, :posts test "collection_cache_key on model" do - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, Developer.collection_cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, Developer.collection_cache_key) 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) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) - /\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 @@ -48,7 +74,7 @@ module ActiveRecord test "cache_key for empty relation" do developers = Developer.where(name: "Non Existent Developer") - assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-0\z/, developers.cache_key) end test "cache_key with custom timestamp column" do @@ -64,7 +90,7 @@ module ActiveRecord test "collection proxy provides a cache_key" do developers = projects(:active_record).developers - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) end test "cache_key for loaded collection with zero size" do @@ -72,18 +98,18 @@ module ActiveRecord posts = Post.includes(:comments) empty_loaded_collection = posts.first.comments - assert_match(/\Acomments\/query-(\h+)-0\Z/, empty_loaded_collection.cache_key) + assert_match(/\Acomments\/query-(\h+)-0\z/, empty_loaded_collection.cache_key) end test "cache_key for queries with offset which return 0 rows" do developers = Developer.offset(20) - assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-0\z/, developers.cache_key) end test "cache_key with a relation having selected columns" do developers = Developer.select(:salary) - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) end end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index a6297673c9..996d298689 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -87,9 +87,14 @@ if current_adapter?(:PostgreSQLAdapter) test "schema dump includes default expression" do output = dump_table_schema("defaults") - assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output + if ActiveRecord::Base.connection.postgresql_version >= 100000 + assert_match %r/t\.date\s+"modified_date",\s+default: -> { "CURRENT_DATE" }/, output + assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + else + assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output + assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "now\(\)" }/, output + end assert_match %r/t\.date\s+"modified_date_function",\s+default: -> { "now\(\)" }/, output - assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "now\(\)" }/, output assert_match %r/t\.datetime\s+"modified_time_function",\s+default: -> { "now\(\)" }/, output end end 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/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 90ad970e16..8c67aa8746 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -21,6 +21,7 @@ class LogSubscriberTest < ActiveRecord::TestCase TRANSACTION: REGEXP_CYAN, OTHER: REGEXP_MAGENTA } + Event = Struct.new(:duration, :payload) class TestDebugLogSubscriber < ActiveRecord::LogSubscriber attr_reader :debugs @@ -55,25 +56,22 @@ class LogSubscriberTest < ActiveRecord::TestCase end def test_schema_statements_are_ignored - event = Struct.new(:duration, :payload) - logger = TestDebugLogSubscriber.new assert_equal 0, logger.debugs.length - logger.sql(event.new(0, sql: "hi mom!")) + logger.sql(Event.new(0, sql: "hi mom!")) assert_equal 1, logger.debugs.length - logger.sql(event.new(0, sql: "hi mom!", name: "foo")) + logger.sql(Event.new(0, sql: "hi mom!", name: "foo")) assert_equal 2, logger.debugs.length - logger.sql(event.new(0, sql: "hi mom!", name: "SCHEMA")) + logger.sql(Event.new(0, sql: "hi mom!", name: "SCHEMA")) assert_equal 2, logger.debugs.length end def test_sql_statements_are_not_squeezed - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new - logger.sql(event.new(0, sql: "ruby rails")) + logger.sql(Event.new(0, sql: "ruby rails")) assert_match(/ruby rails/, logger.debugs.first) end @@ -86,56 +84,51 @@ class LogSubscriberTest < ActiveRecord::TestCase end def test_basic_query_logging_coloration - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, color_regex| - logger.sql(event.new(0, sql: verb.to_s)) + logger.sql(Event.new(0, sql: verb.to_s)) assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_basic_payload_name_logging_coloration_generic_sql - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, _| - logger.sql(event.new(0, sql: verb.to_s)) + logger.sql(Event.new(0, sql: verb.to_s)) assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(event.new(0, sql: verb.to_s, name: "SQL")) + logger.sql(Event.new(0, sql: verb.to_s, name: "SQL")) assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_basic_payload_name_logging_coloration_named_sql - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, _| - logger.sql(event.new(0, sql: verb.to_s, name: "Model Load")) + logger.sql(Event.new(0, sql: verb.to_s, name: "Model Load")) assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(event.new(0, sql: verb.to_s, name: "Model Exists")) + logger.sql(Event.new(0, sql: verb.to_s, name: "Model Exists")) assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(event.new(0, sql: verb.to_s, name: "ANY SPECIFIC NAME")) + logger.sql(Event.new(0, sql: verb.to_s, name: "ANY SPECIFIC NAME")) assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_query_logging_coloration_with_nested_select - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| - logger.sql(event.new(0, sql: "#{verb} WHERE ID IN SELECT")) + logger.sql(Event.new(0, sql: "#{verb} WHERE ID IN SELECT")) assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_query_logging_coloration_with_multi_line_nested_select - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| @@ -145,13 +138,12 @@ class LogSubscriberTest < ActiveRecord::TestCase SELECT ID FROM THINGS ) EOS - logger.sql(event.new(0, sql: sql)) + logger.sql(Event.new(0, sql: sql)) assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) end end def test_query_logging_coloration_with_lock - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true sql = <<-EOS @@ -159,13 +151,13 @@ class LogSubscriberTest < ActiveRecord::TestCase (SELECT * FROM mytable FOR UPDATE) ss WHERE col1 = 5; EOS - logger.sql(event.new(0, sql: sql)) + logger.sql(Event.new(0, sql: sql)) assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) sql = <<-EOS LOCK TABLE films IN SHARE MODE; EOS - logger.sql(event.new(0, sql: sql)) + logger.sql(Event.new(0, sql: sql)) assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index aadbc375af..2e4b454a86 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -299,6 +299,7 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_verbosity _, migrations = sensors(3) + ActiveRecord::Migration.verbose = true ActiveRecord::Migrator.new(:up, migrations, 1).migrate assert_not_equal 0, ActiveRecord::Migration.message_count @@ -311,7 +312,6 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_verbosity_off _, migrations = sensors(3) - ActiveRecord::Migration.message_count = 0 ActiveRecord::Migration.verbose = false ActiveRecord::Migrator.new(:up, migrations, 1).migrate assert_equal 0, ActiveRecord::Migration.message_count diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 5b7e2fd008..5895c51714 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -49,7 +49,7 @@ class PersistenceTest < ActiveRecord::TestCase assert !test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception else # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead - assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\Z/i) do + assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do test_update_with_order_succeeds.call("id DESC") end end diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 9e95149ede..64866eaf2d 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -21,7 +21,7 @@ class RelationMergingTest < ActiveRecord::TestCase def test_relation_to_sql post = Post.first sql = post.comments.to_sql - assert_match(/.?post_id.? = #{post.id}\Z/i, sql) + assert_match(/.?post_id.? = #{post.id}\z/i, sql) end def test_relation_merging_with_arel_equalities_keeps_last_equality 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 fcf68b0f2a..7a710f1004 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -594,7 +594,7 @@ class RelationTest < ActiveRecord::TestCase end end - def test_respond_to_delegates_to_relation + def test_respond_to_delegates_to_arel relation = Topic.all fake_arel = Struct.new(:responds) { def respond_to?(method, access = false) @@ -607,10 +607,6 @@ class RelationTest < ActiveRecord::TestCase relation.respond_to?(:matching_attributes) assert_equal [:matching_attributes, false], fake_arel.responds.first - - fake_arel.responds = [] - relation.respond_to?(:matching_attributes, true) - assert_equal [:matching_attributes, true], fake_arel.responds.first end def test_respond_to_dynamic_finders diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 949086fda0..1a0b7c6ca7 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -45,10 +45,8 @@ module ActiveRecord end end - if Enumerator.method_defined? :size - test "each without block returns a sized enumerator" do - assert_equal 3, result.each.size - end + test "each without block returns a sized enumerator" do + assert_equal 3, result.each.size end test "cast_values returns rows after type casting" do 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/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index baa41a3a47..c47c97e9d9 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -343,8 +343,23 @@ module ActiveRecord ENV["VERBOSE"] = "false" ENV["VERSION"] = "4" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", 4) + ActiveRecord::Migration.expects(:verbose=).with(false) + ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) + ActiveRecord::Tasks::DatabaseTasks.migrate + + ENV.delete("VERBOSE") + ENV.delete("VERSION") + ActiveRecord::Migrator.expects(:migrate).with("custom/path", nil) + ActiveRecord::Migration.expects(:verbose=).with(true) + ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) + ActiveRecord::Tasks::DatabaseTasks.migrate + + ENV["VERBOSE"] = "yes" + ENV["VERSION"] = "unknown" + ActiveRecord::Migrator.expects(:migrate).with("custom/path", 0) + ActiveRecord::Migration.expects(:verbose=).with(true) + ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) ActiveRecord::Tasks::DatabaseTasks.migrate ensure ENV["VERBOSE"], ENV["VERSION"] = verbose, version @@ -353,7 +368,8 @@ module ActiveRecord def test_migrate_raise_error_on_empty_version version = ENV["VERSION"] ENV["VERSION"] = "" - assert_raise(RuntimeError, "Empty VERSION provided") { ActiveRecord::Tasks::DatabaseTasks.migrate } + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_equal "Empty VERSION provided", e.message ensure ENV["VERSION"] = version 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/models/comment.rb b/activerecord/test/models/comment.rb index a4b81d56e0..76b484e616 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -19,6 +19,11 @@ class Comment < ActiveRecord::Base has_many :children, class_name: "Comment", foreign_key: :parent_id belongs_to :parent, class_name: "Comment", counter_cache: :children_count + # Should not be called if extending modules that having the method exists on an association. + def self.greeting + raise + end + def self.what_are_you "a comment..." end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 20e37710e7..d269a95e8c 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -170,14 +170,6 @@ class Client < Company def overwrite_to_raise end - - class << self - private - - def private_method - "darkness" - end - end end class ExclusivelyDependentFirm < Company 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/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 1adc473e1d..3ce4a0bbab 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,7 +1,22 @@ +* 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/date_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb index eb8b8b2c65..870391aeaa 100644 --- a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb @@ -1,4 +1,5 @@ require "active_support/core_ext/date_and_time/compatibility" +require "active_support/core_ext/module/remove_method" class DateTime include DateAndTime::Compatibility 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/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/duration.rb b/activesupport/lib/active_support/duration.rb index 99080e34a1..d4424ed792 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -305,10 +305,6 @@ module ActiveSupport to_i end - def respond_to_missing?(method, include_private = false) #:nodoc: - @value.respond_to?(method, include_private) - end - # Build ISO 8601 Duration string for this duration. # The +precision+ parameter can be used to limit seconds' precision of duration. def iso8601(precision: nil) @@ -335,8 +331,12 @@ module ActiveSupport end end + def respond_to_missing?(method, _) + value.respond_to?(method) + end + def method_missing(method, *args, &block) - value.send(method, *args, &block) + value.public_send(method, *args, &block) end def raise_type_error(other) 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/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 4f1ab993b8..0b345ecf0f 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -171,10 +171,8 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, payments.index_by(&:price)) assert_equal Enumerator, payments.index_by.class - if Enumerator.method_defined? :size - assert_nil payments.index_by.size - assert_equal 42, (1..42).index_by.size - end + assert_nil payments.index_by.size + assert_equal 42, (1..42).index_by.size assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, payments.index_by.each(&:price)) end 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/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/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 46fabca3e8..1d059cc2a5 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "rails", "5.1.0.rc1" + gem "rails", "5.1.0" end require "rack/test" diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb index 71fe356ea0..252b270a0c 100644 --- a/guides/bug_report_templates/active_job_gem.rb +++ b/guides/bug_report_templates/active_job_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activejob", "5.1.0.rc1" + gem "activejob", "5.1.0" end require "minitest/autorun" diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index a685c257ea..61d4e8d395 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activerecord", "5.1.0.rc1" + gem "activerecord", "5.1.0" gem "sqlite3" end diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb index b4e822dfe0..0c398e334a 100644 --- a/guides/bug_report_templates/active_record_migrations_gem.rb +++ b/guides/bug_report_templates/active_record_migrations_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activerecord", "5.1.0.rc1" + gem "activerecord", "5.1.0" gem "sqlite3" end diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb index e1b705bea4..4dcd04ea27 100644 --- a/guides/bug_report_templates/generic_gem.rb +++ b/guides/bug_report_templates/generic_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activesupport", "5.1.0.rc1" + gem "activesupport", "5.1.0" end require "active_support/core_ext/object/blank" 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 e995c50297..ecdb951870 100644 --- a/guides/source/5_1_release_notes.md +++ b/guides/source/5_1_release_notes.md @@ -40,7 +40,7 @@ Major Features [Pull Request](https://github.com/rails/rails/pull/26836) -Rails 5.1 will allow managing JavaScript dependencies +Rails 5.1 allows managing JavaScript dependencies from NPM via Yarn. This will make it easy to use libraries like React, VueJS or any other library from NPM world. The Yarn support is integrated with the asset pipeline so that all dependencies will work seamlessly with the @@ -70,14 +70,14 @@ offerings. It is no longer required, as the UJS has been rewritten to use plain, vanilla JavaScript. This code now ships inside of Action View as `rails-ujs`. -You can still use the jQuery version if needed, but it is no longer required by default. +You can still use jQuery if needed, but it is no longer required by default. ### System tests [Pull Request](https://github.com/rails/rails/pull/26703) Rails 5.1 has baked-in support for writing Capybara tests, in the form of -System tests. You need no longer worry about configuring Capybara and +System tests. You no longer need to worry about configuring Capybara and database cleaning strategies for such tests. Rails 5.1 provides a wrapper for running tests in Chrome with additional features such as failure screenshots. @@ -86,7 +86,7 @@ screenshots. [Pull Request](https://github.com/rails/rails/pull/28038) -Rails will now allow management of application secrets in a secure way, +Rails now allows management of application secrets in a secure way, building on top of the [sekrets](https://github.com/ahoward/sekrets) gem. Run `bin/rails secrets:setup` to setup a new encrypted secrets file. This will @@ -101,38 +101,29 @@ 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 - before_action { @inviter, @invitee = params[:inviter], params[:invitee] } before_action { @account = params[:inviter].account } def account_invitation mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})" end - - def project_invitation - @project = params[:project] - @summarizer = ProjectInvitationSummarizer.new(@project.bucket) - - mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})" - end end -InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later +InvitationsMailer.with(inviter: person_a, invitee: person_b) + .account_invitation.deliver_later ``` ### Direct & resolved routes [Pull Request](https://github.com/rails/rails/pull/23138) -Rails 5.1 has added two new methods, `resolve` and `direct`, to the routing -DSL. - -The `resolve` method allows customizing polymorphic mapping of models. +Rails 5.1 adds two new methods, `resolve` and `direct`, to the routing +DSL. The `resolve` method allows customizing polymorphic mapping of models. ``` ruby resource :basket @@ -181,50 +172,94 @@ Before Rails 5.1, there were two interfaces for handling HTML forms: Rails 5.1 combines both of these interfaces with `form_with`, and can generate form tags based on URLs, scopes or models. -``` erb -# Using just a URL: +Using just a URL: +``` erb <%= form_with url: posts_path do |form| %> <%= form.text_field :title %> <% end %> -# => +<%# Will generate %> + <form action="/posts" method="post" data-remote="true"> <input type="text" name="title"> </form> +``` -# Adding a scope prefixes the input field names: +Adding a scope prefixes the input field names: +``` erb <%= form_with scope: :post, url: posts_path do |form| %> <%= form.text_field :title %> <% end %> -# => + +<%# Will generate %> + <form action="/posts" method="post" data-remote="true"> <input type="text" name="post[title]"> </form> +``` -# Using a model infers both the URL and scope: +Using a model infers both the URL and scope: +``` erb <%= form_with model: Post.new do |form| %> <%= form.text_field :title %> <% end %> -# => + +<%# Will generate %> + <form action="/posts" method="post" data-remote="true"> <input type="text" name="post[title]"> </form> +``` -# An existing model makes an update form and fills out field values: +An existing model makes an update form and fills out field values: +``` erb <%= form_with model: Post.first do |form| %> <%= form.text_field :title %> <% end %> -# => + +<%# Will generate %> + <form action="/posts/1" method="post" data-remote="true"> <input type="hidden" name="_method" value="patch"> <input type="text" name="post[title]" value="<the title of the post>"> </form> ``` +Incompatibilities +----------------- + +The following changes may require immediate action upon upgrade. + +### Transactional tests with multiple connections + +Transactional tests now wrap all Active Record connections in database +transactions. + +When a test spawns additional threads, and those threads obtain database +connections, those connections are now handled specially: + +The threads will share a single connection, which is inside the managed +transaction. This ensures all threads see the database in the same +state, ignoring the outermost transaction. Previously, such additional +connections were unable to see the fixture rows, for example. + +When a thread enters a nested transaction, it will temporarily obtain +exclusive use of the connection, to maintain isolation. + +If your tests currently rely on obtaining a separate, +outside-of-transaction, connection in a spawned thread, you'll need to +switch to more explicit connection management. + +If your tests spawn threads and those threads interact while also using +explicit database transactions, this change may introduce a deadlock. + +The easy way to opt out of this new behavior is to disable transactional +tests on any test cases it affects. + Railties -------- @@ -232,10 +267,70 @@ Please refer to the [Changelog][railties] for detailed changes. ### Removals -### Deprecations +* 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)) + +* Display railtie class name in `rails initializers`. + ([Pull Request](https://github.com/rails/rails/pull/25257)) + +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)) + +* Add `ActiveSupport::Notifications` hook for broadcasting data. + ([Pull Request](https://github.com/rails/rails/pull/24988)) + Action Pack ----------- @@ -243,10 +338,34 @@ Please refer to the [Changelog][action-pack] for detailed changes. ### Removals +* Removed support for non-keyword arguments in `#process`, `#get`, `#post`, + `#patch`, `#put`, `#delete`, and `#head` for the `ActionDispatch::IntegrationTest` + and `ActionController::TestCase` classes. + ([Commit](https://github.com/rails/rails/commit/98b8309569a326910a723f521911e54994b112fb), + [Commit](https://github.com/rails/rails/commit/de9542acd56f60d281465a59eac11e15ca8b3323)) + +* Removed deprecated `ActionDispatch::Callbacks.to_prepare` and + `ActionDispatch::Callbacks.to_cleanup`. + ([Commit](https://github.com/rails/rails/commit/3f2b7d60a52ffb2ad2d4fcf889c06b631db1946b)) + +* Removed deprecated methods related to controller filters. + ([Commit](https://github.com/rails/rails/commit/d7be30e8babf5e37a891522869e7b0191b79b757)) + ### Deprecations +* Deprecated `config.action_controller.raise_on_unfiltered_parameters`. + It doesn't have any effect in Rails 5.1. + ([Commit](https://github.com/rails/rails/commit/c6640fb62b10db26004a998d2ece98baede509e5)) + ### Notable changes +* Added the `direct` and `resolve` methods to the routing DSL. + ([Pull Request](https://github.com/rails/rails/pull/23138)) + +* Added a new `ActionDispatch::SystemTestCase` class to write system tests in + your applications. + ([Pull Request](https://github.com/rails/rails/pull/26703)) + Action View ------------- @@ -254,20 +373,57 @@ 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)) + +* Add `check_parameters` option to `current_page?`. + ([Pull Request](https://github.com/rails/rails/pull/27549)) + Action Mailer ------------- Please refer to the [Changelog][action-mailer] for detailed changes. -### Removals +### Notable changes -### Deprecations +* Allowed setting custom content type when attachments are included + and body is set inline. + ([Pull Request](https://github.com/rails/rails/pull/27227)) -### Notable changes +* 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 ------------- @@ -276,10 +432,109 @@ Please refer to the [Changelog][active-record] for detailed changes. ### Removals +* Removed support for passing arguments and block at the same time to + `ActiveRecord::QueryMethods#select`. + ([Commit](https://github.com/rails/rails/commit/4fc3366d9d99a0eb19e45ad2bf38534efbf8c8ce)) + +* Removed deprecated `activerecord.errors.messages.restrict_dependent_destroy.one` and + `activerecord.errors.messages.restrict_dependent_destroy.many` i18n scopes. + ([Commit](https://github.com/rails/rails/commit/00e3973a311)) + +* Removed deprecated force reload argument in singular and collection association readers. + ([Commit](https://github.com/rails/rails/commit/09cac8c67af)) + +* Removed deprecated support for passing a column to `#quote`. + ([Commit](https://github.com/rails/rails/commit/e646bad5b7c)) + +* Removed deprecated `name` arguments from `#tables`. + ([Commit](https://github.com/rails/rails/commit/d5be101dd02214468a27b6839ffe338cfe8ef5f3)) + +* Removed deprecated behavior of `#tables` and `#table_exists?` to return tables and views + to return only tables and not views. + ([Commit](https://github.com/rails/rails/commit/5973a984c369a63720c2ac18b71012b8347479a8)) + +* Removed deprecated `original_exception` argument in `ActiveRecord::StatementInvalid#initialize` + and `ActiveRecord::StatementInvalid#original_exception`. + ([Commit](https://github.com/rails/rails/commit/bc6c5df4699d3f6b4a61dd12328f9e0f1bd6cf46)) + +* Removed deprecated support of passing a class as a value in a query. + ([Commit](https://github.com/rails/rails/commit/b4664864c972463c7437ad983832d2582186e886)) + +* Removed deprecated support to query using commas on LIMIT. + ([Commit](https://github.com/rails/rails/commit/fc3e67964753fb5166ccbd2030d7382e1976f393)) + +* Removed deprecated `conditions` parameter from `#destroy_all`. + ([Commit](https://github.com/rails/rails/commit/d31a6d1384cd740c8518d0bf695b550d2a3a4e9b)) + +* Removed deprecated `conditions` parameter from `#delete_all`. + ([Commit](https://github.com/rails/rails/pull/27503/commits/e7381d289e4f8751dcec9553dcb4d32153bd922b)) + +* Removed deprecated method `#load_schema_for` in favor of `#load_schema`. + ([Commit](https://github.com/rails/rails/commit/419e06b56c3b0229f0c72d3e4cdf59d34d8e5545)) + +* Removed deprecated `#raise_in_transactional_callbacks` configuration. + ([Commit](https://github.com/rails/rails/commit/8029f779b8a1dd9848fee0b7967c2e0849bf6e07)) + +* Removed deprecated `#use_transactional_fixtures` configuration. + ([Commit](https://github.com/rails/rails/commit/3955218dc163f61c932ee80af525e7cd440514b3)) + ### Deprecations +* Deprecated `error_on_ignored_order_or_limit` flag in favor of + `error_on_ignored_order`. + ([Commit](https://github.com/rails/rails/commit/451437c6f57e66cc7586ec966e530493927098c7)) + +* Deprecated `sanitize_conditions` in favor of `sanitize_sql`. + ([Pull Request](https://github.com/rails/rails/pull/25999)) + +* Deprecated `supports_migrations?` on connection adapters. + ([Pull Request](https://github.com/rails/rails/pull/28172)) + +* Deprecated `Migrator.schema_migrations_table_name`, use `SchemaMigration.table_name` instead. + ([Pull Request](https://github.com/rails/rails/pull/28351)) + +* Deprecated using `#quoted_id` in quoting and type casting. + ([Pull Request](https://github.com/rails/rails/pull/27962)) + +* Deprecated passing `default` argument to `#index_name_exists?`. + ([Pull Request](https://github.com/rails/rails/pull/26930)) + ### Notable changes +* Change Default Primary Keys to BIGINT. + ([Pull Request](https://github.com/rails/rails/pull/26266)) + +* Virtual/generated column support for MySQL 5.7.5+ and MariaDB 5.2.0+. + ([Commit](https://github.com/rails/rails/commit/65bf1c60053e727835e06392d27a2fb49665484c)) + +* Added support for limits in batch processing. + ([Commit](https://github.com/rails/rails/commit/451437c6f57e66cc7586ec966e530493927098c7)) + +* Transactional tests now wrap all Active Record connections in database + transactions. + ([Pull Request](https://github.com/rails/rails/pull/28726)) + +* 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)) + +* Add `ActiveRecord::Base.connection_pool.stat`. + ([Pull Request](https://github.com/rails/rails/pull/26988)) + +* Inheriting directly from `ActiveRecord::Migration` raises an error. + Specify the Rails version for which the migration was written for. + ([Commit](https://github.com/rails/rails/commit/249f71a22ab21c03915da5606a063d321f04d4d3)) + +* An error is raised when `through` association has ambiguous reflection name. + ([Commit](https://github.com/rails/rails/commit/0944182ad7ed70d99b078b22426cbf844edd3f61)) + Active Model ------------ @@ -287,10 +542,21 @@ Please refer to the [Changelog][active-model] for detailed changes. ### Removals -### Deprecations +* 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 ----------- @@ -298,10 +564,21 @@ Please refer to the [Changelog][active-job] for detailed changes. ### Removals -### Deprecations +* 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 -------------- @@ -309,10 +586,53 @@ Please refer to the [Changelog][active-support] for detailed changes. ### Removals +* Removed the `ActiveSupport::Concurrency::Latch` class. + ([Commit](https://github.com/rails/rails/commit/0d7bd2031b4054fbdeab0a00dd58b1b08fb7fea6)) + +* Removed `halt_callback_chains_on_return_false`. + ([Commit](https://github.com/rails/rails/commit/4e63ce53fc25c3bc15c5ebf54bab54fa847ee02a)) + +* Removed deprecated behavior that halts callbacks when the return is false. + ([Commit](https://github.com/rails/rails/commit/3a25cdca3e0d29ee2040931d0cb6c275d612dffe)) + ### Deprecations +* The top level `HashWithIndifferentAccess` class has been softly deprecated + in favor of the `ActiveSupport::HashWithIndifferentAccess` one. + ([Pull Request](https://github.com/rails/rails/pull/28157)) + +* Deprecated passing string to `:if` and `:unless` conditional options on `set_callback` and `skip_callback`. + ([Commit](https://github.com/rails/rails/commit/0952552) + ### Notable changes +* Fixed duration parsing and traveling to make it consistent across DST changes. + ([Commit](https://github.com/rails/rails/commit/8931916f4a1c1d8e70c06063ba63928c5c7eab1e), + [Pull Request](https://github.com/rails/rails/pull/26597)) + +* Updated Unicode to version 9.0.0. + ([Pull Request](https://github.com/rails/rails/pull/27822)) + +* Add Duration#before and #after as aliases for #ago and #since. + ([Pull Request](https://github.com/rails/rails/pull/27721)) + +* 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)) + +* Introduced the `assert_changes` and `assert_no_changes` methods for tests. + ([Pull Request](https://github.com/rails/rails/pull/25393)) + +* The `travel` and `travel_to` methods now raise on nested calls. + ([Pull Request](https://github.com/rails/rails/pull/24890)) + +* Update `DateTime#change` to support usec and nsec. + ([Pull Request](https://github.com/rails/rails/pull/28242)) + 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_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/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/documents.yaml b/guides/source/documents.yaml index 5fccdcccec..2afef57fc2 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -197,7 +197,6 @@ name: Ruby on Rails 5.1 Release Notes url: 5_1_release_notes.html description: Release notes for Rails 5.1. - work_in_progress: true - name: Ruby on Rails 5.0 Release Notes url: 5_0_release_notes.html 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/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 3afc0e5309..93864db141 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. + +### `application.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.application.secrets[:smtp_settings]["address"] +``` + +To: + +```ruby +Rails.application.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..047aeae37d 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -141,6 +141,8 @@ follow this pattern. Built-in Helpers ---------------------- +### Remote elements + Rails provides a bunch of view helper methods written in Ruby to assist you in generating HTML. Sometimes, you want to add a little Ajax to those elements, and Rails has got your back in those cases. @@ -153,14 +155,18 @@ Unless you have disabled the Asset Pipeline, provides the JavaScript half, and the regular Ruby view helpers add appropriate tags to your DOM. -### form_for +You can read below about the different events that are fired dealing with +remote elements inside your application. + +#### form_with -[`form_for`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for) -is a helper that assists with writing forms. `form_for` takes a `:remote` -option. It works like this: +[`form_with`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with) +is a helper that assists with writing forms. By default, `form_with` assumes that +your form will be using Ajax. You can opt out of this behavior by +passing the `:local` option `form_with`. ```erb -<%= form_for(@article, remote: true) do |f| %> +<%= form_with(model: @article) do |f| %> ... <% end %> ``` @@ -168,7 +174,7 @@ option. It works like this: This will generate the following HTML: ```html -<form accept-charset="UTF-8" action="/articles" class="new_article" data-remote="true" id="new_article" method="post"> +<form action="/articles" method="post" data-remote="true"> ... </form> ``` @@ -189,32 +195,9 @@ $(document).ready -> ``` Obviously, you'll want to be a bit more sophisticated than that, but it's a -start. You can see more about the events [in the jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki/ajax). - -### form_tag - -[`form_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-form_tag) -is very similar to `form_for`. It has a `:remote` option that you can use like -this: - -```erb -<%= form_tag('/articles', remote: true) do %> - ... -<% end %> -``` - -This will generate the following HTML: - -```html -<form accept-charset="UTF-8" action="/articles" data-remote="true" method="post"> - ... -</form> -``` - -Everything else is the same as `form_for`. See its documentation for full -details. +start. -### link_to +#### link_to [`link_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to) is a helper that assists with generating links. It has a `:remote` option you @@ -230,7 +213,7 @@ which generates <a href="/articles/1" data-remote="true">an article</a> ``` -You can bind to the same Ajax events as `form_for`. Here's an example. Let's +You can bind to the same Ajax events as `form_with`. Here's an example. Let's assume that we have a list of articles that can be deleted with just one click. We would generate some HTML like this: @@ -246,7 +229,7 @@ $ -> alert "The article was deleted." ``` -### button_to +#### button_to [`button_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to) is a helper that helps you create buttons. It has a `:remote` option that you can call like this: @@ -262,7 +245,136 @@ this generates </form> ``` -Since it's just a `<form>`, all of the information on `form_for` also applies. +Since it's just a `<form>`, all of the information on `form_with` also applies. + +### Customize remote elements + +It is possible to customize the behavior of elements with a `data-remote` +attribute without writing a line of JavaScript. Your can specify extra `data-` +attributes to accomplish this. + +#### `data-method` + +Activating hyperlinks always results in an HTTP GET request. However, if your +application is [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer), +some links are in fact actions that change data on the server, and must be +performed with non-GET requests. This attribute allows marking up such links +with an explicit method such as "post", "put" or "delete". + +The way it works is that, when the link is activated, it constructs a hidden form +in the document with the "action" attribute corresponding to "href" value of the +link, and the method corresponding to `data-method` value, and submits that form. + +NOTE: Because submitting forms with HTTP methods other than GET and POST isn't +widely supported across browsers, all other HTTP methods are actually sent over +POST with the intended method indicated in the `_method` parameter. Rails +automatically detects and compensates for this. + +#### `data-url` and `data-params` + +Certain elements of your page aren't actually referring to any URL, but you may want +them to trigger Ajax calls. Specifying the `data-url` attribute along with +the `data-remote` one will trigger an Ajax call to the given URL. You can also +specify extra parameters through the `data-params` attribute. + +This can be useful to trigger an action on check-boxes for instance: + +```html +<input type="checkbox" data-remote="true" + data-url="/update" data-params="id=10" data-method="put"> +``` + +#### `data-type` + +It is also possible to define the Ajax `dataType` explicitly while performing +requests for `data-remote` elements, by way of the `data-type` attribute. + +### Confirmations + +You can ask for an extra confirmation of the user by adding a `data-confirm` +attribute on links and forms. The user will be presented a JavaScript `confirm()` +dialog containing the attribute's text. If the user chooses to cancel, the action +doesn't take place. + +Adding this attribute on links will trigger the dialog on click, and adding it +on forms will trigger it on submit. For example: + +```erb +<%= link_to "Dangerous zone", dangerous_zone_path, + data: { confirm: 'Are you sure?' } %> +``` + +This generates: + +```html +<a href="..." data-confirm="Are you sure?">Dangerous zone</a> +``` + +The attribute is also allowed on form submit buttons. This allows you to customize +the warning message depending on the button which was activated. In this case, +you should **not** have `data-confirm` on the form itself. + +The default confirmation uses a JavaScript confirm dialog, but you can customize +this by listening to the `confirm` event, which is fired just before the confirmation +window appears to the user. To cancel this default confirmation, have the confirm +handler to return `false`. + +### Automatic disabling + +It is also possible to automatically disable an input while the form is submitting +by using the `data-disable-with` attribute. This is to prevent accidental +double-clicks from the user, which could result in duplicate HTTP requests that +the backend may not detect as such. The value of the attribute is the text that will +become the new value of the button in its disabled state. + +This also works for links with `data-method` attribute. + +For example: + +```erb +<%= form_with(model: @article.new) do |f| %> + <%= f.submit data: { "disable-with": "Saving..." } %> +<%= end %> +``` + +This generates a form with: + +```html +<input data-disable-with="Saving..." type="submit"> +``` + +Dealing with Ajax events +------------------------ + +Here are the different events that are fired when you deal with elements +that have a `data-remote` attribute: + +NOTE: All handlers bound to these events are always passed the event object as the +first argument. The table below describes the extra parameters passed after the +event argument. For exemple, if the extra parameters are listed as `xhr, settings`, +then to access them, you would define your handler with `function(event, xhr, settings)`. + +| Event name | Extra parameters | Fired | +|---------------------|------------------|-------------------------------------------------------------| +| `ajax:before` | | Before the whole ajax business, aborts if stopped. | +| `ajax:beforeSend` | xhr, options | Before the request is sent, aborts if stopped. | +| `ajax:send` | xhr | When the request is sent. | +| `ajax:success` | xhr, status, err | After completion, if the response was a success. | +| `ajax:error` | xhr, status, err | After completion, if the response was an error. | +| `ajax:complete` | xhr, status | After the request has been completed, no matter the outcome.| +| `ajax:aborted:file` | elements | If there are non-blank file inputs, aborts if stopped. | + +### Stoppable events + +If you stop `ajax:before` or `ajax:beforeSend` by returning false from the +handler method, the Ajax request will never take place. The `ajax:before` event +is also useful for manipulating form data before serialization. The +`ajax:beforeSend` event is also useful for adding custom request headers. + +If you stop the `ajax:aborted:file` event, the default behavior of allowing the +browser to submit the form via normal means (i.e. non-AJAX submission) will be +canceled and the form will not be submitted at all. This is useful for +implementing your own AJAX file upload workaround. Server-Side Concerns -------------------- @@ -297,7 +409,7 @@ The index view (`app/views/users/index.html.erb`) contains: <br> -<%= form_for(@user, remote: true) do |f| %> +<%= form_with(model: @user) do |f| %> <%= f.label :name %><br> <%= f.text_field :name %> <%= f.submit %> @@ -338,7 +450,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 +467,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 +497,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..e58086ce93 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1 +1,11 @@ +* Added a shared section to config/database.yml that will be loaded for all environments. + + *Pierre Schambacher* + +* 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/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 7c49fabba5..27c1572357 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -133,7 +133,14 @@ module Rails config = if yaml && yaml.exist? require "yaml" require "erb" - YAML.load(ERB.new(yaml.read).result) || {} + loaded_yaml = YAML.load(ERB.new(yaml.read).result) || {} + shared = loaded_yaml.delete("shared") + if shared + loaded_yaml.each do |_k, values| + values.reverse_merge!(shared) + end + end + Hash.new(shared).merge(loaded_yaml) elsif ENV["DATABASE_URL"] # Value from ENV['DATABASE_URL'] is set to default database connection # by Active Record. 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/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 324843a5f5..aa0e016424 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -317,10 +317,6 @@ module Rails def create_vendor_files build(:vendor) - - if options[:skip_yarn] - remove_file "package.json" - end end def delete_app_assets_if_api_option @@ -403,9 +399,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/bin/yarn b/railties/lib/rails/generators/rails/app/templates/bin/yarn index 4ae896a8d3..44f75c22a4 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/yarn +++ b/railties/lib/rails/generators/rails/app/templates/bin/yarn @@ -3,7 +3,8 @@ Dir.chdir(VENDOR_PATH) do begin exec "yarnpkg #{ARGV.join(" ")}" rescue Errno::ENOENT - puts "Yarn executable was not detected in the system." - puts "Download Yarn at https://yarnpkg.com/en/docs/install" + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 end 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/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 000ce40fbc..7ff55ef5bf 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -6,6 +6,8 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: before_action :require_local!, unless: :show_previews? before_action :find_preview, only: :preview + helper_method :part_query + def index @previews = ActionMailer::Preview.all @page_title = "Mailer Previews" @@ -19,7 +21,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: @email_action = File.basename(params[:path]) if @preview.email_exists?(@email_action) - @email = @preview.call(@email_action) + @email = @preview.call(@email_action, params) if params[:part] part_type = Mime::Type.lookup(params[:part]) @@ -76,4 +78,8 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: @email end end + + def part_query(mime_type) + request.query_parameters.merge(part: mime_type).to_query + end end diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 474118c0a9..3476bb0eb5 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -162,10 +162,6 @@ module Rails @instance ||= new end - def respond_to_missing?(*args) - instance.respond_to?(*args) || super - end - # Allows you to configure the railtie. This is the same method seen in # Railtie::Configurable, but this module is no longer required for all # subclasses of Railtie so we provide the class method here. @@ -178,6 +174,10 @@ module Rails ActiveSupport::Inflector.underscore(string).tr("/", "_") end + def respond_to_missing?(name, _) + instance.respond_to?(name) || super + end + # If the class method does not have a method, then send the method call # to the Railtie instance. def method_missing(name, *args, &block) 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/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb index c63781ed0c..89c1129f90 100644 --- a/railties/lib/rails/templates/rails/mailers/email.html.erb +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -98,8 +98,8 @@ <% if @email.multipart? %> <dd> <select onchange="formatChanged(this);"> - <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option> - <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option> + <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="?<%= part_query('text/html') %>">View as HTML email</option> + <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="?<%= part_query('text/plain') %>">View as plain-text email</option> </select> </dd> <% end %> @@ -107,7 +107,7 @@ </header> <% if @part && @part.mime_type %> - <iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> + <iframe seamless name="messageBody" src="?<%= part_query(@part.mime_type) %>"></iframe> <% else %> <p> You are trying to preview an email that does not have any content. diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 0f9bf98737..81537d813e 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -12,7 +12,12 @@ require "rails/generators/test_case" require "active_support/testing/autorun" if defined?(ActiveRecord::Base) - ActiveRecord::Migration.maintain_test_schema! + begin + ActiveRecord::Migration.maintain_test_schema! + rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 + end module ActiveSupport class TestCase diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 14433fbba0..06767167a9 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 @@ -1347,6 +1407,40 @@ module ApplicationTests assert_match "config/database", err.message end + test "loads database.yml using shared keys" do + app_file "config/database.yml", <<-YAML + shared: + username: bobby + adapter: sqlite3 + + development: + database: 'dev_db' + YAML + + app "development" + + ar_config = Rails.application.config.database_configuration + assert_equal "sqlite3", ar_config["development"]["adapter"] + assert_equal "bobby", ar_config["development"]["username"] + assert_equal "dev_db", ar_config["development"]["database"] + end + + test "loads database.yml using shared keys for undefined environments" do + app_file "config/database.yml", <<-YAML + shared: + username: bobby + adapter: sqlite3 + database: 'dev_db' + YAML + + app "development" + + ar_config = Rails.application.config.database_configuration + assert_equal "sqlite3", ar_config["development"]["adapter"] + assert_equal "bobby", ar_config["development"]["username"] + assert_equal "dev_db", ar_config["development"]["database"] + end + test "config.action_mailer.show_previews defaults to true in development" do app "development" diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index c3a360e5d4..f5c013dab6 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -485,6 +485,57 @@ module ApplicationTests assert_match '<li><a href="/my_app/rails/mailers/notifier/foo">foo</a></li>', last_response.body end + test "mailer preview receives query params" do + mailer "notifier", <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo(name) + @name = name + mail to: "to@example.org" + end + end + RUBY + + html_template "notifier/foo", <<-RUBY + <p>Hello, <%= @name %>!</p> + RUBY + + text_template "notifier/foo", <<-RUBY + Hello, <%= @name %>! + RUBY + + mailer_preview "notifier", <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo(params[:name] || "World") + end + end + RUBY + + app("development") + + get "/rails/mailers/notifier/foo.txt" + assert_equal 200, last_response.status + assert_match '<iframe seamless name="messageBody" src="?part=text%2Fplain">', last_response.body + assert_match '<option selected value="?part=text%2Fplain">', last_response.body + assert_match '<option value="?part=text%2Fhtml">', last_response.body + + get "/rails/mailers/notifier/foo?part=text%2Fplain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo.html?name=Ruby" + assert_equal 200, last_response.status + assert_match '<iframe seamless name="messageBody" src="?name=Ruby&part=text%2Fhtml">', last_response.body + assert_match '<option selected value="?name=Ruby&part=text%2Fhtml">', last_response.body + assert_match '<option value="?name=Ruby&part=text%2Fplain">', last_response.body + + get "/rails/mailers/notifier/foo?name=Ruby&part=text%2Fhtml" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, Ruby!</p>], last_response.body + end + test "plain text mailer preview with attachment" do image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb index cbb990f13b..fe07ad3cbe 100644 --- a/railties/test/application/middleware/exceptions_test.rb +++ b/railties/test/application/middleware/exceptions_test.rb @@ -100,6 +100,20 @@ module ApplicationTests end end + test "routing to an nonexistent controller when action_dispatch.show_exceptions and consider_all_requests_local are set shows diagnostics" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + resources :articles + end + RUBY + + app.config.action_dispatch.show_exceptions = true + app.config.consider_all_requests_local = true + + get "/articles" + assert_match "<title>Action Controller: Exception caught</title>", last_response.body + end + test "displays diagnostics message when exception raised in template that contains UTF-8" do controller :foo, <<-RUBY class FooController < ActionController::Base diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index 449d281967..51dfe2ef98 100644 --- a/railties/test/application/rake/migrations_test.rb +++ b/railties/test/application/rake/migrations_test.rb @@ -48,8 +48,14 @@ module ApplicationTests output = `bin/rails db:migrate:up VERSION= 2>&1` assert_match(/VERSION is required/, output) + output = `bin/rails db:migrate:up 2>&1` + assert_match(/VERSION is required/, output) + output = `bin/rails db:migrate:down VERSION= 2>&1` assert_match(/VERSION is required - To go down one migration, use db:rollback/, output) + + output = `bin/rails db:migrate:down 2>&1` + assert_match(/VERSION is required - To go down one migration, use db:rollback/, output) end end @@ -142,6 +148,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") @@ -232,7 +294,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/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/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 924503a522..2e742a005e 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -119,7 +119,7 @@ module TestHelpers end routes = File.read("#{app_path}/config/routes.rb") - if routes =~ /(\n\s*end\s*)\Z/ + if routes =~ /(\n\s*end\s*)\z/ File.open("#{app_path}/config/routes.rb", "w") do |f| f.puts $` + "\nActiveSupport::Deprecation.silence { match ':controller(/:action(/:id))(.:format)', via: :all }\n" + $1 end @@ -251,7 +251,7 @@ module TestHelpers def add_to_config(str) environment = File.read("#{app_path}/config/application.rb") - if environment =~ /(\n\s*end\s*end\s*)\Z/ + if environment =~ /(\n\s*end\s*end\s*)\z/ File.open("#{app_path}/config/application.rb", "w") do |f| f.puts $` + "\n#{str}\n" + $1 end @@ -260,7 +260,7 @@ module TestHelpers def add_to_env_config(env, str) environment = File.read("#{app_path}/config/environments/#{env}.rb") - if environment =~ /(\n\s*end\s*)\Z/ + if environment =~ /(\n\s*end\s*)\z/ File.open("#{app_path}/config/environments/#{env}.rb", "w") do |f| f.puts $` + "\n#{str}\n" + $1 end 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 |