diff options
79 files changed, 545 insertions, 136 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 4b081591c0..6a68c057ac 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,8 @@ +* Fix `ActionController::Parameters#fetch` overwriting `KeyError` returned by + default block. + + *Jonas Schuber Erlandsson*, *Roque Pinel* + * `ActionController::Parameters` no longer inherits from `HashWithIndifferentAccess` diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index ebd02bd9a1..784092867c 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -96,7 +96,7 @@ module AbstractController # ==== Returns # * <tt>String</tt> def controller_path - @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous? + @controller_path ||= name.sub(/Controller$/, ''.freeze).underscore unless anonymous? end # Refresh the cached action_methods when a new action_method is added. diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 109eff10eb..d84c238a62 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -181,7 +181,7 @@ module AbstractController end def default_helper_module! - module_name = name.sub(/Controller$/, '') + module_name = name.sub(/Controller$/, ''.freeze) module_path = module_name.underscore helper module_path rescue LoadError => e diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 87609d8aa7..4c9f14e409 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -25,7 +25,7 @@ module ActionController status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" - message << " (#{additions.join(" | ")})" unless additions.blank? + message << " (#{additions.join(" | ".freeze)})" unless additions.blank? message end end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index d920668184..e31d65aac2 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -55,10 +55,10 @@ module ActionController # You can pass any of the following options to affect the before_action callback # * <tt>only</tt> - The callback should be run only for this action # * <tt>except</tt> - The callback should be run for all actions except this action - # * <tt>if</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a true value. - # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a false value. + # * <tt>if</tt> - A symbol naming an instance method or a proc; the + # callback will be called only when it returns a true value. + # * <tt>unless</tt> - A symbol naming an instance method or a proc; the + # callback will be called only when it returns a false value. def force_ssl(options = {}) action_options = options.slice(*ACTION_OPTIONS) redirect_options = options.except(*ACTION_OPTIONS) @@ -71,8 +71,8 @@ module ActionController # Redirect the existing request to use the HTTPS protocol. # # ==== Parameters - # * <tt>host_or_options</tt> - Either a host name or any of the url & redirect options - # available to the <tt>force_ssl</tt> method. + # * <tt>host_or_options</tt> - Either a host name or any of the url & + # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? options = { diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index b4da381d26..fcaf3e6425 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -73,7 +73,7 @@ module ActionController # Provides a proxy to access helpers methods from outside the view. def helpers - @helper_proxy ||= begin + @helper_proxy ||= begin proxy = ActionView::Base.new proxy.config = config.inheritable_copy proxy.extend(_helpers) @@ -100,7 +100,7 @@ module ActionController def all_helpers_from_path(path) helpers = Array(path).flat_map do |_path| extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ - names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } + names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) } names.sort! end helpers.uniq! diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 06d625e4d5..cf6a64009f 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -380,11 +380,15 @@ module ActionController def fetch(key, *args, &block) convert_hashes_to_parameters( key, - @parameters.fetch(key, *args, &block), + @parameters.fetch(key) { + if block_given? + yield + else + args.fetch(0) { raise ActionController::ParameterMissing.new(key) } + end + }, false ) - rescue KeyError - raise ActionController::ParameterMissing.new(key) end # Returns a new <tt>ActionController::Parameters</tt> instance that diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index 6e058b829e..e826551f4b 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -34,11 +34,11 @@ module ActionDispatch end end - deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.") } - deep_strings, strings = strings.partition { |s| s.include?("\\.") } + deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) } + deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) } - regexps << Regexp.new(strings.join('|'), true) unless strings.empty? - deep_regexps << Regexp.new(deep_strings.join('|'), true) unless deep_strings.empty? + regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty? + deep_regexps << Regexp.new(deep_strings.join('|'.freeze), true) unless deep_strings.empty? new regexps, deep_regexps, blocks end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index f5b709ccd6..6fcf49030b 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -245,7 +245,7 @@ module ActionDispatch # req = Request.new 'HTTP_HOST' => 'example.com:8080' # req.host # => "example.com" def host - raw_host_with_port.sub(/:\d+$/, '') + raw_host_with_port.sub(/:\d+$/, ''.freeze) end # Returns a \host:\port string for this request, such as "example.com" or diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index bb01c087bc..cf6542b370 100644 --- a/actionpack/lib/action_dispatch/journey/nodes/node.rb +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -30,7 +30,7 @@ module ActionDispatch end def name - left.tr '*:', '' + left.tr '*:'.freeze, ''.freeze end def type diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index d02ed96d0d..9793ca1c7a 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -14,10 +14,10 @@ module ActionDispatch # normalize_path("/%ab") # => "/%AB" def self.normalize_path(path) path = "/#{path}" - path.squeeze!('/') - path.sub!(%r{/+\Z}, '') + path.squeeze!('/'.freeze) + path.sub!(%r{/+\Z}, ''.freeze) path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } - path = '/' if path == '' + path = '/' if path == ''.freeze path end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index b098ea389f..f38da4fdf6 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -35,7 +35,7 @@ module ActionDispatch paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p.force_encoding('UTF-8')) + path = File.join(@root, p.force_encoding('UTF-8'.freeze)) begin File.file?(path) && File.readable?(path) rescue SystemCallError @@ -76,7 +76,7 @@ module ActionDispatch end def content_type(path) - ::Rack::Mime.mime_type(::File.extname(path), 'text/plain') + ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze) end def gzip_encoding_accepted?(env) @@ -112,7 +112,7 @@ module ActionDispatch def call(env) case env['REQUEST_METHOD'] when 'GET', 'HEAD' - path = env['PATH_INFO'].chomp('/') + path = env['PATH_INFO'].chomp('/'.freeze) if match = @file_handler.match?(path) env['PATH_INFO'] = match return @file_handler.call(env) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 7cfe4693c1..ec530c6e8a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1538,7 +1538,7 @@ module ActionDispatch path = path_for_action(action, options.delete(:path)) raise ArgumentError, "path is required" if path.blank? - action = action.to_s.dup + action = action.to_s if action =~ /^[\w\-\/]+$/ options[:action] ||= action.tr('-', '_') unless action.include?("/") diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 454593b59f..42512cad91 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -671,7 +671,7 @@ module ActionDispatch # Remove leading slashes from controllers def normalize_controller! - @options[:controller] = controller.sub(%r{^/}, '') if controller + @options[:controller] = controller.sub(%r{^/}, ''.freeze) if controller end # Move 'index' action from options to recall diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 05532ec21b..9f7d14e85d 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -194,6 +194,19 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal "monkey", @params.fetch(:foo) { "monkey" } end + test "fetch doesnt raise ParameterMissing exception if there is a default that is nil" do + assert_equal nil, @params.fetch(:foo, nil) + assert_equal nil, @params.fetch(:foo) { nil } + end + + test 'KeyError in fetch block should not be covered up' do + params = ActionController::Parameters.new + e = assert_raises(KeyError) do + params.fetch(:missing_key) { {}.fetch(:also_missing) } + end + assert_match(/:also_missing$/, e.message) + end + test "not permitted is sticky beyond merges" do assert !@params.merge(a: "b").permitted? end diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index df8059d04e..4224346a2f 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -8,6 +8,13 @@ module ActionView @@cache = ThreadSafe::Cache.new @@digest_monitor = Monitor.new + class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc: + def call(env) + ActionView::Digestor.cache.clear + app.call(env) + end + end + class << self # Supported options: # @@ -41,10 +48,7 @@ module ActionView Digestor end - digest = klass.new(options).digest - # Store the actual digest if config.cache_template_loading is true - @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching? - digest + @@cache[cache_key] = stored_digest = klass.new(options).digest ensure # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index e32f8e219e..3041ad3ef7 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -237,7 +237,7 @@ module ActionView # image_alt('underscored_file_name.png') # # => Underscored file name def image_alt(src) - File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize + File.basename(src, '.*'.freeze).sub(/-[[:xdigit:]]{32}\z/, ''.freeze).tr('-_'.freeze, ' '.freeze).capitalize end # Returns an HTML video tag for the +sources+. If +sources+ is a string, diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index b19dc25025..3beb7fefb1 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -127,7 +127,7 @@ module ActionView return "" unless source.present? return source if source =~ URI_REGEXP - tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '') + tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, ''.freeze) if extname = compute_asset_extname(source, options) source = "#{source}#{extname}" diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb index a67ba580f1..93c7cba395 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -115,7 +115,7 @@ module ActionView # <li><%= link_to 'Home', action: 'index' %></li> # <% end %> # - # And in other place: + # And in another place: # # <% content_for :navigation do %> # <li><%= link_to 'Login', action: 'login' %></li> diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 1c8359005b..f4a10aa393 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -173,13 +173,13 @@ module ActionView # name instead of the prefix. def normalize_name(name, prefixes) #:nodoc: prefixes = prefixes.presence - parts = name.to_s.split('/') + parts = name.to_s.split('/'.freeze) parts.shift if parts.first.empty? name = parts.pop return name, prefixes || [""] if parts.empty? - parts = parts.join('/') + parts = parts.join('/'.freeze) prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts] return name, prefixes @@ -204,7 +204,7 @@ module ActionView # add :html as fallback to :js. def formats=(values) if values - values.concat(default_formats) if values.delete "*/*" + values.concat(default_formats) if values.delete "*/*".freeze if values == [:js] values << :html @html_fallback_for_js = true diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 5dc7950d6b..e829d86c99 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -42,6 +42,14 @@ module ActionView end end + initializer "action_view.per_request_digest_cache" do |app| + ActiveSupport.on_load(:action_view) do + if app.config.consider_all_requests_local + app.middleware.use ActionView::Digestor::PerRequestDigestCacheExpiry + end + end + end + initializer "action_view.setup_action_pack" do |app| ActiveSupport.on_load(:action_controller) do ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor) diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index d8585514d5..1ce9f94b13 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -190,7 +190,7 @@ module ActionView end def inspect - @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier + @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", ''.freeze) : identifier end # This method is responsible for properly setting the encoding of the @@ -337,13 +337,13 @@ module ActionView def method_name #:nodoc: @method_name ||= begin m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}" - m.tr!('-', '_') + m.tr!('-'.freeze, '_'.freeze) m end end def identifier_method_name #:nodoc: - inspect.tr('^a-z_', '_') + inspect.tr('^a-z_'.freeze, '_'.freeze) end def instrument(action, &block) @@ -366,7 +366,7 @@ module ActionView end def inferred_cache_name - @inferred_cache_name ||= @virtual_path.split('/').last.sub('_', '') + @inferred_cache_name ||= @virtual_path.split('/'.freeze).last.sub('_'.freeze, ''.freeze) end end end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 2b00b0303d..e7208cce49 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -222,7 +222,7 @@ module ActionView end def escape_entry(entry) - entry.gsub(/[*?{}\[\]]/, '\\\\\\&') + entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze) end # Returns the file mtime from the filesystem. @@ -234,7 +234,7 @@ module ActionView # from the path, or the handler, we should return the array of formats given # to the resolver. def extract_handler_and_format_and_variant(path, default_formats) - pieces = File.basename(path).split('.') + pieces = File.basename(path).split('.'.freeze) pieces.shift extension = pieces.pop diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index f0afcdb5ae..24bc76cbbb 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -219,7 +219,7 @@ class TemplateDigestorTest < ActionView::TestCase end def test_variants - assert_digest_difference("messages/new", false, variants: [:iphone]) do + assert_digest_difference("messages/new", variants: [:iphone]) do change_template("messages/new", :iphone) change_template("messages/_header", :iphone) end @@ -239,16 +239,6 @@ class TemplateDigestorTest < ActionView::TestCase assert_not_equal digest_phone, digest_fridge_phone end - def test_cache_template_loading - resolver_before = ActionView::Resolver.caching - ActionView::Resolver.caching = false - assert_digest_difference("messages/edit", true) do - change_template("comments/_comment") - end - ensure - ActionView::Resolver.caching = resolver_before - end - def test_digest_cache_cleanup_with_recursion first_digest = digest("level/_recursion") second_digest = digest("level/_recursion") @@ -291,9 +281,9 @@ class TemplateDigestorTest < ActionView::TestCase end end - def assert_digest_difference(template_name, persistent = false, options = {}) + def assert_digest_difference(template_name, options = {}) previous_digest = digest(template_name, options) - ActionView::Digestor.cache.clear unless persistent + ActionView::Digestor.cache.clear yield diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 286cd6c206..9033ec0dca 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -372,7 +372,7 @@ module ActiveModel "define_method(:'#{name}') do |*args|" end - extra = (extra.map!(&:inspect) << "*args").join(", ") + extra = (extra.map!(&:inspect) << "*args").join(", ".freeze) target = if send =~ CALL_COMPILABLE_REGEXP "#{"self." unless include_private}#{send}(#{extra})" diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 1f1749af4e..213f2d5b6a 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -192,7 +192,7 @@ module ActiveModel private def _singularize(string) - ActiveSupport::Inflector.underscore(string).tr('/', '_') + ActiveSupport::Inflector.underscore(string).tr('/'.freeze, '_'.freeze) end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index bda436d8d0..1da4df21e7 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -115,7 +115,7 @@ module ActiveModel key = "#{key.to_s.camelize}Validator" begin - validator = key.include?('::') ? key.constantize : const_get(key) + validator = key.include?('::'.freeze) ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 237b07f653..22f805fea4 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,61 @@ +* Deprecate the keys for association `restrict_dependent_destroy` errors in favor + of new key names. + + Previously `has_one` and `has_many` associations were using the + `one` and `many` keys respectively. Both of these keys have special + meaning in I18n (they are considered to be pluralizations) so by + renaming them to `has_one` and `has_many` we make the messages more explicit + and most importantly they don't clash with linguistical systems that need to + validate translation keys (and their pluralizations). + + The `:'restrict_dependent_destroy.one'` key should be replaced with + `:'restrict_dependent_destroy.has_one'`, and `:'restrict_dependent_destroy.many'` + with `:'restrict_dependent_destroy.has_many'`. + + *Roque Pinel*, *Christopher Dell* + +* Fix state being carried over from previous transaction. + + Considering the following example where `name` is a required attribute. + Before we had `new_record?` returning `true` for a persisted record: + + author = Author.create! name: 'foo' + author.name = nil + author.save # => false + author.new_record? # => true + + Fixes #20824. + + *Roque Pinel* + +* Correctly ignore `mark_for_destruction` when `autosave` isn't set to `true` + when validating associations. + + Fixes #20882. + + *Sean Griffin* + +* Fix a bug where counter_cache doesn't always work with polymorphic + relations. + + Fixes #16407. + + *Stefan Kanev & Sean Griffin* + +* Ensure that cyclic associations with autosave don't cause duplicate errors + to be added to the parent record. + + Fixes #20874. + + *Sean Griffin* + +* Ensure that `ActionController::Parameters` can still be passed to nested + attributes. + + Fixes #20922. + + *Sean Griffin* + * Deprecate force association reload by passing a truthy argument to association method. diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 97eb007f62..6e4a53f7fb 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -33,16 +33,24 @@ module ActiveRecord::Associations::Builder if (@_after_create_counter_called ||= false) @_after_create_counter_called = false - elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable? - model = reflection.klass + elsif attribute_changed?(foreign_key) && !new_record? + if reflection.polymorphic? + model = attribute(reflection.foreign_type).try(:constantize) + model_was = attribute_was(reflection.foreign_type).try(:constantize) + else + model = reflection.klass + model_was = reflection.klass + end + foreign_key_was = attribute_was foreign_key foreign_key = attribute foreign_key if foreign_key && model.respond_to?(:increment_counter) model.increment_counter(cache_column, foreign_key) end - if foreign_key_was && model.respond_to?(:decrement_counter) - model.decrement_counter(cache_column, foreign_key_was) + + if foreign_key_was && model_was.respond_to?(:decrement_counter) + model_was.decrement_counter(cache_column, foreign_key_was) end end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index ddeafb40ea..b5a8c81fe4 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -781,7 +781,7 @@ module ActiveRecord # person.pets.any? # => false # # person.pets << Pet.new(name: 'Snoop') - # person.pets.count # => 0 + # person.pets.count # => 1 # person.pets.any? # => true # # You can also pass a +block+ to define criteria. The behavior @@ -971,7 +971,7 @@ module ActiveRecord alias_method :append, :<< def prepend(*args) - raise NoMethodError, "prepend on association is not defined. Please use << or append" + raise NoMethodError, "prepend on association is not defined. Please use <<, push or append" end # Equivalent to +delete_all+. The difference is that returns +self+, instead diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index ca27c9fdde..9f6c832c1b 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -16,7 +16,14 @@ module ActiveRecord when :restrict_with_error unless empty? record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) + message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil + if message + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1. + Please use `:'restrict_dependent_destroy.has_many'` instead. + MESSAGE + end + owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record) throw(:abort) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 41a75b820e..5a92bc5e8a 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -12,7 +12,14 @@ module ActiveRecord when :restrict_with_error if load_target record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) + message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil + if message + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1. + Please use `:'restrict_dependent_destroy.has_one'` instead. + MESSAGE + end + owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record) throw(:abort) end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 9d58a19304..abe1d465a5 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -42,7 +42,7 @@ module ActiveRecord def [](name) @method_cache.compute_if_absent(name) do - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 0d989c2eca..2363cf7608 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -37,7 +37,7 @@ module ActiveRecord protected def define_method_attribute(name) - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index ab017c7b54..07d5e7d38e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -24,7 +24,7 @@ module ActiveRecord protected def define_method_attribute=(name) - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 0792d19c3e..dbb0e2fab2 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -222,6 +222,7 @@ module ActiveRecord true end validate validation_method + after_validation :_ensure_no_duplicate_errors end end end @@ -324,7 +325,7 @@ module ActiveRecord # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> # enabled records if they're marked_for_destruction? or destroyed. def association_valid?(reflection, record) - return true if record.destroyed? || record.marked_for_destruction? + return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) unless valid = record.valid?(validation_context) @@ -456,5 +457,11 @@ module ActiveRecord end end end + + def _ensure_no_duplicate_errors + errors.messages.each_key do |attribute| + errors[attribute].uniq! + end + end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 6d3a21a3dc..56227ddd80 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -107,6 +107,18 @@ module ActiveRecord @prepared_statements = false end + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map(&:to_i) + end + + def <=>(version_string) + @version <=> version_string.split('.').map(&:to_i) + end + end + class BindCollector < Arel::Collectors::Bind def compile(bvs, conn) casted_binds = conn.prepare_binds_for_database(bvs) 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 2027492f29..af156c9c78 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -307,7 +307,7 @@ module ActiveRecord # # http://bugs.mysql.com/bug.php?id=39170 def supports_transaction_isolation? - version[0] >= 5 + version >= '5.0.0' end def supports_indexes_in_create? @@ -319,11 +319,11 @@ module ActiveRecord end def supports_views? - version[0] >= 5 + version >= '5.0.0' end def supports_datetime_with_precision? - (version[0] == 5 && version[1] >= 6) || version[0] >= 6 + version >= '5.6.4' end def native_database_types @@ -386,6 +386,14 @@ module ActiveRecord 0 end + def quoted_date(value) + if supports_datetime_with_precision? + super + else + super.sub(/\.\d{6}\z/, '') + end + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: @@ -938,7 +946,7 @@ module ActiveRecord end def version - @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) + @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) end def mariadb? @@ -946,7 +954,7 @@ module ActiveRecord end def supports_rename_index? - mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + mariadb? ? false : version >= '5.7.6' end def configure_connection diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index 58715978f7..b2c49989a4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -12,7 +12,7 @@ module ActiveRecord end def sql_type - super.gsub(/\[\]$/, "") + super.gsub(/\[\]$/, "".freeze) end def ==(other) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7c809b088c..358039723f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -65,18 +65,6 @@ module ActiveRecord boolean: { name: "boolean" } } - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map(&:to_i) - end - - def <=>(version_string) - @version <=> version_string.split('.').map(&:to_i) - end - end - class StatementPool < ConnectionAdapters::StatementPool private diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index 8a3c27e6da..0b35027b2b 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -16,8 +16,8 @@ en: messages: record_invalid: "Validation failed: %{errors}" restrict_dependent_destroy: - one: "Cannot delete record because a dependent %{record} exists" - many: "Cannot delete record because dependent %{record} exist" + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" # Append your own errors here or at the model/attributes scope. # You can define own errors for models or model attributes. diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index c942d0e265..a6b76b25bf 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -386,6 +386,9 @@ module ActiveRecord # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) options = self.nested_attributes_options[association_name] + if attributes.respond_to?(:permitted?) + attributes = attributes.to_h + end attributes = attributes.with_indifferent_access existing_record = send(association_name) @@ -442,6 +445,9 @@ module ActiveRecord # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) options = self.nested_attributes_options[association_name] + if attributes_collection.respond_to?(:permitted?) + attributes_collection = attributes_collection.to_h + end unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" @@ -468,6 +474,9 @@ module ActiveRecord end attributes_collection.each do |attributes| + if attributes.respond_to?(:permitted?) + attributes = attributes.to_h + end attributes = attributes.with_indifferent_access if attributes['id'].blank? diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 86f2c30168..d75ec72b1a 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -18,7 +18,7 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } - const_set klass.name.gsub('::', '_'), delegate + const_set klass.name.gsub('::'.freeze, '_'.freeze), delegate cache[klass] = delegate end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 43e9afe853..d26db7d4cf 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -52,7 +52,7 @@ module ActiveRecord key else key = key.to_s - key.split('.').first if key.include?('.') + key.split('.'.freeze).first if key.include?('.'.freeze) end end.compact end @@ -123,10 +123,10 @@ module ActiveRecord end def convert_dot_notation_to_hash(attributes) - dot_notation = attributes.keys.select { |s| s.include?(".") } + dot_notation = attributes.keys.select { |s| s.include?(".".freeze) } dot_notation.each do |key| - table_name, column_name = key.split(".") + table_name, column_name = key.split(".".freeze) value = attributes.delete(key) attributes[table_name] ||= {} diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 3131723828..887d7a5903 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -204,9 +204,8 @@ module ActiveRecord # # Note that "TRUNCATE" is also a MySQL DDL statement! module ClassMethods - # See ActiveRecord::Transactions::ClassMethods for detailed documentation. + # See the ConnectionAdapters::DatabaseStatements#transaction API docs. def transaction(options = {}, &block) - # See the ConnectionAdapters::DatabaseStatements#transaction API docs. connection.transaction(options, &block) end @@ -380,6 +379,10 @@ module ActiveRecord raise ActiveRecord::Rollback unless status end status + ensure + if @transaction_state && @transaction_state.committed? + clear_transaction_record_state + end end protected diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index a296cf9d31..ca476947fc 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -12,4 +12,18 @@ class MysqlQuotingTest < ActiveRecord::MysqlTestCase def test_type_cast_false assert_equal 0, @conn.type_cast(false) end + + def test_quoted_date_precision_for_gte_564 + @conn.stubs(:full_version).returns('5.6.4') + @conn.remove_instance_variable(:@version) + t = Time.now.change(usec: 1) + assert_match(/\.000001\z/, @conn.quoted_date(t)) + end + + def test_quoted_date_precision_for_lt_564 + @conn.stubs(:full_version).returns('5.6.3') + @conn.remove_instance_variable(:@version) + t = Time.now.change(usec: 1) + refute_match(/\.000001\z/, @conn.quoted_date(t)) + end end diff --git a/activerecord/test/cases/adapters/mysql2/quoting_test.rb b/activerecord/test/cases/adapters/mysql2/quoting_test.rb new file mode 100644 index 0000000000..a49cbba4b4 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/quoting_test.rb @@ -0,0 +1,21 @@ +require "cases/helper" + +class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + test 'quoted date precision for gte 5.6.4' do + @connection.stubs(:full_version).returns('5.6.4') + @connection.remove_instance_variable(:@version) + t = Time.now.change(usec: 1) + assert_match(/\.000001\z/, @connection.quoted_date(t)) + end + + test 'quoted date precision for lt 5.6.4' do + @connection.stubs(:full_version).returns('5.6.3') + @connection.remove_instance_variable(:@version) + t = Time.now.change(usec: 1) + refute_match(/\.000001\z/, @connection.quoted_date(t)) + 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 f4bacbea2e..c2ef59d0f7 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1426,6 +1426,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.companies.exists?(:name => 'child') end + def test_restrict_with_error_is_deprecated_using_key_many + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { many: 'message for deprecated key' } } } } + + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.companies.create(name: 'child') + + assert !firm.companies.empty? + + assert_deprecated { firm.destroy } + + assert !firm.errors.empty? + + assert_equal 'message for deprecated key', firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.companies.exists?(name: 'child') + ensure + I18n.backend.reload! + end + def test_restrict_with_error firm = RestrictedWithErrorFirm.create!(:name => 'restrict') firm.companies.create(:name => 'child') diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index b7cb5a064f..5a8afaf4d2 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -178,6 +178,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert firm.account.present? end + def test_restrict_with_error_is_deprecated_using_key_one + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { one: 'message for deprecated key' } } } } + + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.create_account(credit_limit: 10) + + assert_not_nil firm.account + + assert_deprecated { firm.destroy } + + assert !firm.errors.empty? + assert_equal 'message for deprecated key', firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.account.present? + ensure + I18n.backend.reload! + end + def test_restrict_with_error firm = RestrictedWithErrorFirm.create!(:name => 'restrict') firm.create_account(:credit_limit => 10) diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index eeda9335ad..bd74821d87 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -142,7 +142,7 @@ module ActiveRecord end if current_adapter?(:PostgreSQLAdapter) - test "arrays types can be specified" do + test "array types can be specified" do klass = Class.new(OverloadedType) do attribute :my_array, :string, limit: 50, array: true attribute :my_int_array, :integer, array: true diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 676b29ed7c..37ec3f1106 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -67,6 +67,14 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase assert_no_difference_when_adding_callbacks_twice_for Pirate, :parrots end + def test_cyclic_autosaves_do_not_add_multiple_validations + ship = ShipWithoutNestedAttributes.new + ship.prisoners.build + + assert_not ship.valid? + assert_equal 1, ship.errors[:name].length + end + private def assert_no_difference_when_adding_callbacks_twice_for(model, association_name) @@ -1146,6 +1154,13 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase def test_should_not_load_the_associated_model assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! } end + + def test_mark_for_destruction_is_ignored_without_autosave_true + ship = ShipWithoutNestedAttributes.new(name: "The Black Flag") + ship.parts.build.mark_for_destruction + + assert_not ship.valid? + end end class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 1f5055b2a2..922cb59280 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'models/topic' require 'models/car' +require 'models/aircraft' require 'models/wheel' require 'models/engine' require 'models/reply' @@ -198,4 +199,16 @@ class CounterCacheTest < ActiveRecord::TestCase assert_equal 2, car.engines_count assert_equal 2, car.reload.engines_count end + + test "update counters in a polymorphic relationship" do + aircraft = Aircraft.create! + + assert_difference 'aircraft.reload.wheels_count' do + aircraft.wheels << Wheel.create! + end + + assert_difference 'aircraft.reload.wheels_count', -1 do + aircraft.wheels.first.destroy + end + end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 12c793c408..d9d0f929db 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -47,7 +47,8 @@ end def mysql_56? current_adapter?(:MysqlAdapter, :Mysql2Adapter) && - ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0" + ActiveRecord::Base.connection.send(:version) >= '5.6.0' && + ActiveRecord::Base.connection.send(:version) < '5.7.0' end def mysql_enforcing_gtid_consistency? diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index dbdcc84b7d..2e1363334d 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -270,7 +270,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase car.wheels << Wheel.create! end assert_difference 'car.wheels.count', -1 do - car.destroy + car.reload.destroy end assert car.destroyed? end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index d72225f3d3..b8a0401fe3 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -1064,4 +1064,39 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR assert_not part.valid? assert_equal ["Ship name can't be blank"], part.errors.full_messages end + + class ProtectedParameters + def initialize(hash) + @hash = hash + end + + def permitted? + true + end + + def [](key) + @hash[key] + end + + def to_h + @hash + end + end + + test "strong params style objects can be assigned for singular associations" do + params = { name: "Stern", ship_attributes: + ProtectedParameters.new(name: "The Black Rock") } + part = ShipPart.new(params) + + assert_equal "Stern", part.name + assert_equal "The Black Rock", part.ship.name + end + + test "strong params style objects can be assigned for collection associations" do + params = { trinkets_attributes: ProtectedParameters.new("0" => ProtectedParameters.new(name: "Necklace"), "1" => ProtectedParameters.new(name: "Spoon")) } + part = ShipPart.new(params) + + assert_equal "Necklace", part.trinkets[0].name + assert_equal "Spoon", part.trinkets[1].name + end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 4e163ca4a6..29a6ec7522 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -175,6 +175,13 @@ class TransactionTest < ActiveRecord::TestCase assert topic.new_record?, "#{topic.inspect} should be new record" end + def test_transaction_state_is_cleared_when_record_is_persisted + author = Author.create! name: 'foo' + author.name = nil + assert_not author.save + assert_not author.new_record? + end + def test_update_should_rollback_on_failure author = Author.find(1) posts_count = author.posts.size diff --git a/activerecord/test/models/aircraft.rb b/activerecord/test/models/aircraft.rb index 1f35ef45da..c4404a8094 100644 --- a/activerecord/test/models/aircraft.rb +++ b/activerecord/test/models/aircraft.rb @@ -1,4 +1,5 @@ class Aircraft < ActiveRecord::Base self.pluralize_table_names = false has_many :engines, :foreign_key => "car_id" + has_many :wheels, as: :wheelable end diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 312caef604..95172e4d3e 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -19,6 +19,18 @@ class Ship < ActiveRecord::Base end end +class ShipWithoutNestedAttributes < ActiveRecord::Base + self.table_name = "ships" + has_many :prisoners, inverse_of: :ship, foreign_key: :ship_id + has_many :parts, class_name: "ShipPart", foreign_key: :ship_id + + validates :name, presence: true +end + +class Prisoner < ActiveRecord::Base + belongs_to :ship, autosave: true, class_name: "ShipWithoutNestedAttributes", inverse_of: :prisoners +end + class FamousShip < ActiveRecord::Base self.table_name = 'ships' belongs_to :famous_pirate diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 6872b49ad9..7bab675b2a 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -37,6 +37,7 @@ ActiveRecord::Schema.define do create_table :aircraft, force: true do |t| t.string :name + t.integer :wheels_count, default: 0, null: false end create_table :articles, force: true do |t| @@ -678,6 +679,10 @@ ActiveRecord::Schema.define do t.datetime :updated_at end + create_table :prisoners, force: true do |t| + t.belongs_to :ship + end + create_table :speedometers, force: true, id: false do |t| t.string :speedometer_id t.string :name diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 1c69e0eb12..7148f289bb 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,11 @@ +* Fix `TimeWithZone#eql?` to properly handle `TimeWithZone` created from `DateTime`: + twz = DateTime.now.in_time_zone + twz.eql?(twz.dup) => true + + Fixes #14178. + + *Roque Pinel* + * ActiveSupport::HashWithIndifferentAccess `select` and `reject` will now return enumerator if called without block. diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index 01153606c9..40811dafc0 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb @@ -92,15 +92,28 @@ module DateAndTime end # Returns a new date/time at the start of the month. - # DateTime objects will have a time set to 0:00. + # + # today = Date.today # => Thu, 18 Jun 2015 + # today.beginning_of_month # => Mon, 01 Jun 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Thu, 18 Jun 2015 15:23:13 +0000 + # now.beginning_of_month # => Mon, 01 Jun 2015 00:00:00 +0000 def beginning_of_month first_hour(change(:day => 1)) end alias :at_beginning_of_month :beginning_of_month # Returns a new date/time at the start of the quarter. - # Example: 1st January, 1st July, 1st October. - # DateTime objects will have a time set to 0:00. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.beginning_of_quarter # => Wed, 01 Jul 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000 def beginning_of_quarter first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month } beginning_of_month.change(:month => first_quarter_month) @@ -108,8 +121,14 @@ module DateAndTime alias :at_beginning_of_quarter :beginning_of_quarter # Returns a new date/time at the end of the quarter. - # Example: 31st March, 30th June, 30th September. - # DateTime objects will have a time set to 23:59:59. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.end_of_quarter # => Wed, 30 Sep 2015 + # + # +DateTime+ objects will have a time set to 23:59:59. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000 def end_of_quarter last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month } beginning_of_month.change(:month => last_quarter_month).end_of_month @@ -117,8 +136,14 @@ module DateAndTime alias :at_end_of_quarter :end_of_quarter # Return a new date/time at the beginning of the year. - # Example: 1st January. - # DateTime objects will have a time set to 0:00. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.beginning_of_year # => Thu, 01 Jan 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.beginning_of_year # => Thu, 01 Jan 2015 00:00:00 +0000 def beginning_of_year change(:month => 1).beginning_of_month end @@ -138,8 +163,8 @@ module DateAndTime # # +DateTime+ objects have their time set to 0:00 unless +same_time+ is true. # - # now = Time.current # => Thu, 07 May 2015 13:31:16 UTC +00:00 - # now.next_week # => Mon, 11 May 2015 00:00:00 UTC +00:00 + # now = DateTime.current # => Thu, 07 May 2015 13:31:16 +0000 + # now.next_week # => Mon, 11 May 2015 00:00:00 +0000 def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false) result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week))) same_time ? copy_time_to(result) : result diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index d9fb392752..60732eb41a 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -23,7 +23,7 @@ class LoadError # Returns true if the given path name (except perhaps for the ".rb" # extension) is the missing file which caused the exception to be raised. def is_missing?(location) - location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '') + location.sub(/\.rb$/, ''.freeze) == path.sub(/\.rb$/, ''.freeze) end end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 9b7a429db9..9dc0dee1bd 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -167,7 +167,7 @@ class Module '' end - file, line = caller(1, 1).first.split(':', 2) + file, line = caller(1, 1).first.split(':'.freeze, 2) line = line.to_i to = to.to_s diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 770c845435..fc6f822969 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -149,7 +149,7 @@ module ActiveSupport #:nodoc: # Normalize the list of new constants, and add them to the list we will return new_constants.each do |suffix| - constants << ([namespace, suffix] - ["Object"]).join("::") + constants << ([namespace, suffix] - ["Object"]).join("::".freeze) end end constants @@ -431,7 +431,7 @@ module ActiveSupport #:nodoc: # Search for a file in autoload_paths matching the provided suffix. def search_for_file(path_suffix) - path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb") + path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb".freeze) autoload_paths.each do |root| path = File.join(root, path_suffix) @@ -516,7 +516,7 @@ module ActiveSupport #:nodoc: if file_path expanded = File.expand_path(file_path) - expanded.sub!(/\.rb\z/, '') + expanded.sub!(/\.rb\z/, ''.freeze) if loading.include?(expanded) raise "Circular dependency detected while autoloading constant #{qualified_name}" diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index be369d21c6..60ef249e37 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -91,10 +91,10 @@ module ActiveSupport def underscore(camel_cased_word) return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/ word = camel_cased_word.to_s.gsub('::'.freeze, '/'.freeze) - word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'}#{$2.downcase}" } - word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') - word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') - word.tr!("-", "_") + word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }#{$2.downcase}" } + word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze) + word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze) + word.tr!("-".freeze, "_".freeze) word.downcase! word end @@ -127,9 +127,9 @@ module ActiveSupport inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } - result.sub!(/\A_+/, '') - result.sub!(/_id\z/, '') - result.tr!('_', ' ') + result.sub!(/\A_+/, ''.freeze) + result.sub!(/_id\z/, ''.freeze) + result.tr!('_'.freeze, ' '.freeze) result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match] || match.downcase}" @@ -178,14 +178,14 @@ module ActiveSupport # classify('calculus') # => "Calculu" def classify(table_name) # strip out any leading schema name - camelize(singularize(table_name.to_s.sub(/.*\./, ''))) + camelize(singularize(table_name.to_s.sub(/.*\./, ''.freeze))) end # Replaces underscores with dashes in the string. # # dasherize('puni_puni') # => "puni-puni" def dasherize(underscored_word) - underscored_word.tr('_', '-') + underscored_word.tr('_'.freeze, '-'.freeze) end # Removes the module part from the expression in the string. @@ -248,7 +248,7 @@ module ActiveSupport # NameError is raised when the name is not in CamelCase or the constant is # unknown. def constantize(camel_cased_word) - names = camel_cased_word.split('::') + names = camel_cased_word.split('::'.freeze) # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(camel_cased_word) if names.empty? diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index edea142e82..2c03956672 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -79,7 +79,7 @@ module ActiveSupport # No more than one of the separator in a row. parameterized_string.gsub!(/#{re_sep}{2,}/, sep) # Remove leading/trailing separator. - parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '') + parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, ''.freeze) end parameterized_string.downcase end diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 92ab6fe648..c82a13511e 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -82,7 +82,7 @@ module ActiveSupport def _decrypt(encrypted_message) cipher = new_cipher - encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)} + encrypted_data, iv = encrypted_message.split("--".freeze).map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index b2a4404968..64c5232cf4 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -46,7 +46,7 @@ module ActiveSupport def valid_message?(signed_message) return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank? - data, digest = signed_message.split("--") + data, digest = signed_message.split("--".freeze) data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) end @@ -74,7 +74,7 @@ module ActiveSupport def verified(signed_message) if valid_message?(signed_message) begin - data = signed_message.split("--")[0] + data = signed_message.split("--".freeze)[0] @serializer.load(decode(data)) rescue ArgumentError => argument_error return if argument_error.message =~ %r{invalid base64} diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 35efebc65f..f4de4a80d9 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -58,7 +58,7 @@ module ActiveSupport # Returns a regular expression pattern that matches the passed Unicode # codepoints. def self.codepoints_to_pattern(array_of_codepoints) #:nodoc: - array_of_codepoints.collect{ |e| [e].pack 'U*' }.join('|') + array_of_codepoints.collect{ |e| [e].pack 'U*'.freeze }.join('|'.freeze) end TRAILERS_PAT = /(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+\Z/u LEADERS_PAT = /\A(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+/u diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 412c72d27c..f8f1b9ac2c 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -247,7 +247,7 @@ module ActiveSupport end def eql?(other) - utc.eql?(other) + other.eql?(utc) end def hash diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 477a42114b..ccb7f02331 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -253,10 +253,14 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_eql? + assert_equal true, @twz.eql?(@twz.dup) assert_equal true, @twz.eql?(Time.utc(2000)) assert_equal true, @twz.eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) ) assert_equal false, @twz.eql?( Time.utc(2000, 1, 1, 0, 0, 1) ) assert_equal false, @twz.eql?( DateTime.civil(1999, 12, 31, 23, 59, 59) ) + + other_twz = ActiveSupport::TimeWithZone.new(DateTime.now.utc, @time_zone) + assert_equal true, other_twz.eql?(other_twz.dup) end def test_hash diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 11561c55f9..58ba708a39 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -32,7 +32,7 @@ class TestController < ActionController::Base include Rails.application.routes.url_helpers def index - render text: 'Home' + render plain: 'Home' end end diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 66887398b9..1a4b736348 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -31,7 +31,7 @@ class TestController < ActionController::Base include Rails.application.routes.url_helpers def index - render text: 'Home' + render plain: 'Home' end end diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index e3cfabb327..4b4d70d3ce 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1266,6 +1266,18 @@ class Client < ActiveRecord::Base end ``` +NOTE: The `default_scope` is also applied while creating/building a record. +It is not applied while updating a record. E.g.: + +```ruby +class Client < ActiveRecord::Base + default_scope { where(active: true) } +end + +Client.new # => #<Client id: nil, active: true> +Client.unscoped.new # => #<Client id: nil, active: nil> +``` + ### Merging of scopes Just like `where` clauses scopes are merged using `AND` conditions. diff --git a/guides/source/testing.md b/guides/source/testing.md index d146df3dc6..f40e765242 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -1081,7 +1081,7 @@ Finally we can assert that our response was successful, template was rendered, a #### Taking it further -We were able to successfully test a very small workflow for visiting our blog and creating a new article. If we wanted to take this further we could add tests for commenting, removing articles, or editting comments. Integration tests are a great place to experiment with all kinds of use-cases for our applications. +We were able to successfully test a very small workflow for visiting our blog and creating a new article. If we wanted to take this further we could add tests for commenting, removing articles, or editing comments. Integration tests are a great place to experiment with all kinds of use-cases for our applications. Testing Your Mailers -------------------- diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index c3b8ef1181..5f4904fee1 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -28,4 +28,4 @@ <br> -<%%= link_to 'New <%= human_name %>', new_<%= singular_table_name %>_path %> +<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_table_name %>_path %> diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index b083381255..6a1c2faaab 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -36,13 +36,15 @@ group :development, :test do end group :development do +<%- unless options.api? -%> # Access an IRB console on exception pages or by using <%%= console %> in views <%- if options.dev? || options.edge? -%> gem 'web-console', github: 'rails/web-console' <%- else -%> gem 'web-console', '~> 2.0' <%- end -%> -<%- if spring_install? %> +<%- end -%> +<% if spring_install? -%> # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' <% end -%> diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb new file mode 100644 index 0000000000..3198e12662 --- /dev/null +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -0,0 +1,63 @@ +require 'isolation/abstract_unit' +require 'rack/test' +require 'minitest/mock' + +require 'action_view' +require 'active_support/testing/method_call_assertions' + +class PerRequestDigestCacheTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include ActiveSupport::Testing::MethodCallAssertions + include Rack::Test::Methods + + setup do + build_app + add_to_config 'config.consider_all_requests_local = true' + + app_file 'app/models/customer.rb', <<-RUBY + class Customer < Struct.new(:name, :id) + extend ActiveModel::Naming + include ActiveModel::Conversion + end + RUBY + + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + resources :customers, only: :index + end + RUBY + + app_file 'app/controllers/customers_controller.rb', <<-RUBY + class CustomersController < ApplicationController + def index + render [ Customer.new('david', 1), Customer.new('dingus', 2) ] + end + end + RUBY + + app_file 'app/views/customers/_customer.html.erb', <<-RUBY + <% cache customer do %> + <%= customer.name %> + <% end %> + RUBY + + require "#{app_path}/config/environment" + end + + teardown :teardown_app + + test "digests are reused when rendering the same template twice" do + get '/customers' + assert_equal 200, last_response.status + + assert_equal [ '8ba099b7749542fe765ff34a6824d548' ], ActionView::Digestor.cache.values + assert_equal %w(david dingus), last_response.body.split.map(&:strip) + end + + test "template digests are cleared before a request" do + assert_called(ActionView::Digestor.cache, :clear) do + get '/customers' + assert_equal 200, last_response.status + end + end +end diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index 9978ad0da1..998da3ef84 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -38,6 +38,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase assert_no_match(/gem 'jquery-rails'/, content) assert_no_match(/gem 'sass-rails'/, content) assert_no_match(/gem 'jbuilder'/, content) + assert_no_match(/gem 'web-console'/, content) assert_match(/gem 'active_model_serializers'/, content) end |