diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 426ca919a1c5f..1dbecb2cbc140 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -67,4 +67,5 @@ def self.eager_load! ActiveSupport.on_load(:action_view) do ActionView::Base.default_formats ||= Mime::SET.symbols ActionView::Template::Types.delegate_to Mime + ActionView::LookupContext::DetailsKey.clear end diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 83e3e0c37c857..7a34911e9dc5d 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -22,7 +22,6 @@ module ActionController # config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211') # config.action_controller.cache_store = MyOwnStore.new('parameter') module Caching - extend ActiveSupport::Autoload extend ActiveSupport::Concern included do diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 8365ddca570ab..b2485db5f8b45 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -22,7 +22,7 @@ module ActionController # Third, if we DON'T find a template AND the request is a page load in a web # browser (technically, a non-XHR GET request for an HTML response) where # you reasonably expect to have rendered a template, then we raise - # <tt>ActionView::UnknownFormat</tt> with an explanation. + # <tt>ActionController::MissingExactTemplate</tt> with an explanation. # # Finally, if we DON'T find a template AND the request isn't a browser page # load, then we implicitly respond with <tt>204 No Content</tt>. diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index ea931cec5195a..0aa1b4ab46b44 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -252,7 +252,7 @@ class Parameters # to change these is to specify `always_permitted_parameters` in your # config. For instance: # - # config.always_permitted_parameters = %w( controller action format ) + # config.action_controller.always_permitted_parameters = %w( controller action format ) cattr_accessor :always_permitted_parameters, default: %w( controller action ) class << self diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 5d64c61b7706b..ba4b62fb97057 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -116,4 +116,5 @@ module Session ActiveSupport.on_load(:action_view) do ActionView::Base.default_formats ||= Mime::SET.symbols ActionView::Template::Types.delegate_to Mime + ActionView::LookupContext::DetailsKey.clear end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 4f7b59c79aa68..d6df5a0fd2fee 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -334,7 +334,7 @@ def raw_post # variable is already set, wrap it in a StringIO. def body if raw_post = get_header("RAW_POST_DATA") - raw_post = raw_post.dup.force_encoding(Encoding::BINARY) + raw_post = (+raw_post).force_encoding(Encoding::BINARY) StringIO.new(raw_post) else body_stream diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 7464309b1e6a5..fc6a79b1448f0 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -81,7 +81,7 @@ def each(&block) CONTENT_TYPE = "Content-Type" SET_COOKIE = "Set-Cookie" LOCATION = "Location" - NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304] + NO_CONTENT_CODES = [100, 101, 102, 103, 204, 205, 304] cattr_accessor :default_charset, default: "utf-8" cattr_accessor :default_headers @@ -448,8 +448,8 @@ def parsed_content_type_header end def set_content_type(content_type, charset) - type = (content_type || "").dup - type << "; charset=#{charset.to_s.downcase}" if charset + type = content_type || "" + type = "#{type}; charset=#{charset.to_s.downcase}" if charset set_header CONTENT_TYPE, type end diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index c320d164dee99..f6146687fc993 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -81,6 +81,11 @@ def processing head 102 end + def early_hints + self.content_type = "text/html" + head 103 + end + def no_content self.content_type = "text/html" head 204 @@ -121,6 +126,12 @@ class HeadTest < ActiveSupport::TestCase assert_nil headers["Content-Length"] end + test "head :early_hints (103) does not return a content-type header" do + headers = HeadController.action(:early_hints).call(Rack::MockRequest.env_for("/")).second + assert_nil headers["Content-Type"] + assert_nil headers["Content-Length"] + end + test "head :no_content (204) does not return a content-type header" do headers = HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")).second assert_nil headers["Content-Type"] diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 1877afb1b63b7..153c3c55ca2ee 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -153,7 +153,7 @@ def test_only_set_charset_still_defaults_to_text_html end test "content length" do - [100, 101, 102, 204].each do |c| + [100, 101, 102, 103, 204].each do |c| @response = ActionDispatch::Response.new @response.status = c.to_s @response.set_header "Content-Length", "0" @@ -163,7 +163,7 @@ def test_only_set_charset_still_defaults_to_text_html end test "does not contain a message-body" do - [100, 101, 102, 204, 304].each do |c| + [100, 101, 102, 103, 204, 304].each do |c| @response = ActionDispatch::Response.new @response.status = c.to_s @response.body = "Body must not be included" diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 4197c4c021c0f..50b9e5be44faf 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,12 @@ +* Ensure cache fragment digests include all relevant template dependencies when + fragments are contained in a block passed to the render helper. Remove the + virtual_path keyword arguments found in CacheHelper as they no longer possess + any function following 1581cab. + + Fixes #38984 + + *Aaron Lipman* + * Deprecate `config.action_view.raise_on_missing_translations` in favor of `config.i18n.raise_on_missing_translations`. diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index e9002e9b8b399..3840ac0caf175 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -269,13 +269,13 @@ def initialize(lookup_context = nil, assigns = {}, controller = nil, formats = N _prepare_context end - def _run(method, template, locals, buffer, &block) - _old_output_buffer, _old_virtual_path, _old_template = @output_buffer, @virtual_path, @current_template - @current_template = template + def _run(method, template, locals, buffer, add_to_stack: true, &block) + _old_output_buffer, _old_template = @output_buffer, @current_template + @current_template = template if add_to_stack @output_buffer = buffer send(method, locals, buffer, &block) ensure - @output_buffer, @virtual_path, @current_template = _old_output_buffer, _old_virtual_path, _old_template + @output_buffer, @current_template = _old_output_buffer, _old_template end def compiled_method_container diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb index 2b22c30a3accb..a9320640858a6 100644 --- a/actionview/lib/action_view/context.rb +++ b/actionview/lib/action_view/context.rb @@ -18,7 +18,6 @@ module Context def _prepare_context @view_flow = OutputFlow.new @output_buffer = nil - @virtual_path = nil end # Encapsulates the interaction with the view flow so it diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 3f396da288a81..3f87bcf9e1c8c 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -165,7 +165,7 @@ module CacheHelper # expire the cache. def cache(name = {}, options = {}, &block) if controller.respond_to?(:perform_caching) && controller.perform_caching - name_options = options.slice(:skip_digest, :virtual_path) + name_options = options.slice(:skip_digest) safe_concat(fragment_for(cache_fragment_name(name, **name_options), options, &block)) else yield @@ -205,14 +205,11 @@ def cache_unless(condition, name = {}, options = {}, &block) # fragments can be manually bypassed. This is useful when cache fragments # cannot be manually expired unless you know the exact key which is the # case when using memcached. - # - # The digest will be generated using +virtual_path:+ if it is provided. - # - def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil, digest_path: nil) + def cache_fragment_name(name = {}, skip_digest: nil, digest_path: nil) if skip_digest name else - fragment_name_with_digest(name, virtual_path, digest_path) + fragment_name_with_digest(name, digest_path) end end @@ -227,14 +224,11 @@ def digest_path_from_template(template) # :nodoc: end private - def fragment_name_with_digest(name, virtual_path, digest_path) - virtual_path ||= @virtual_path - - if virtual_path || digest_path - name = controller.url_for(name).split("://").last if name.is_a?(Hash) + def fragment_name_with_digest(name, digest_path) + name = controller.url_for(name).split("://").last if name.is_a?(Hash) + if @current_template&.virtual_path || digest_path digest_path ||= digest_path_from_template(@current_template) - [ digest_path, name ] else name @@ -243,10 +237,10 @@ def fragment_name_with_digest(name, virtual_path, digest_path) def fragment_for(name = {}, options = nil, &block) if content = read_fragment_for(name, options) - @view_renderer.cache_hits[@virtual_path] = :hit if defined?(@view_renderer) + @view_renderer.cache_hits[@current_template&.virtual_path] = :hit if defined?(@view_renderer) content else - @view_renderer.cache_hits[@virtual_path] = :miss if defined?(@view_renderer) + @view_renderer.cache_hits[@current_template&.virtual_path] = :miss if defined?(@view_renderer) write_fragment_for(name, options, &block) end end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index acca4df74cbaa..c794d2b67f8e6 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1793,7 +1793,7 @@ def initialize(object_name, object, template, options) # Wraps ActionView::Helpers::FormHelper#time_field for form builders: # # <%= form_with model: @user do |f| %> - # <%= f.time_field :borned_at %> + # <%= f.time_field :born_at %> # <% end %> # # Please refer to the documentation of the base helper for details. diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 49e7e6d3be1e7..028ffc2f8747b 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -124,10 +124,10 @@ def localize(object, **options) def scope_key_by_partial(key) stringified_key = key.to_s if stringified_key.start_with?(".") - if @virtual_path + if @current_template&.virtual_path @_scope_key_by_partial_cache ||= {} - @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".") - "#{@_scope_key_by_partial_cache[@virtual_path]}#{stringified_key}" + @_scope_key_by_partial_cache[@current_template.virtual_path] ||= @current_template.virtual_path.gsub(%r{/_?}, ".") + "#{@_scope_key_by_partial_cache[@current_template.virtual_path]}#{stringified_key}" else raise "Cannot use t(#{key.inspect}) shortcut because path is not available" end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 1503ba734d442..39d24bd4f878c 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -282,7 +282,7 @@ def render_partial_template(view, locals, template, layout, block) identifier: template.identifier, layout: layout && layout.virtual_path ) do |payload| - content = template.render(view, locals) do |*name| + content = template.render(view, locals, add_to_stack: !block) do |*name| view._layout_for(*name, &block) end diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 788d171a90391..ea02ffcae9de1 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -68,7 +68,7 @@ def collection_by_cache_keys(view, template, collection) end def expanded_cache_key(key, view, template, digest_path) - key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: template.virtual_path, digest_path: digest_path)) + key = view.combined_fragment_cache_key(view.cache_fragment_name(key, digest_path: digest_path)) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index cf0ef749593bb..d2d24299fe14f 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -176,10 +176,10 @@ def supports_streaming? # This method is instrumented as "!render_template.action_view". Notice that # we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. - def render(view, locals, buffer = ActionView::OutputBuffer.new, &block) + def render(view, locals, buffer = ActionView::OutputBuffer.new, add_to_stack: true, &block) instrument_render_template do compile!(view) - view._run(method_name, self, locals, buffer, &block) + view._run(method_name, self, locals, buffer, add_to_stack: add_to_stack, &block) end rescue => e handle_render_error(view, e) diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 0f79f6febe995..b04a7cab6670e 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -35,6 +35,44 @@ def to_str alias :to_s :to_str end + class PathParser # :nodoc: + def initialize + @regex = build_path_regex + end + + def build_path_regex + handlers = Template::Handlers.extensions.map { |x| Regexp.escape(x) }.join("|") + formats = Template::Types.symbols.map { |x| Regexp.escape(x) }.join("|") + locales = "[a-z]{2}(?:-[A-Z]{2})?" + variants = "[^.]*" + + %r{ + \A + (?:(?<prefix>.*)/)? + (?<partial>_)? + (?<action>.*?) + (?:\.(?<locale>#{locales}))?? + (?:\.(?<format>#{formats}))?? + (?:\+(?<variant>#{variants}))?? + (?:\.(?<handler>#{handlers}))? + \z + }x + end + + def parse(path) + match = @regex.match(path) + { + prefix: match[:prefix] || "", + action: match[:action], + partial: !!match[:partial], + locale: match[:locale]&.to_sym, + handler: match[:handler]&.to_sym, + format: match[:format]&.to_sym, + variant: match[:variant] + } + end + end + # Threadsafe template cache class Cache #:nodoc: class SmallCache < Concurrent::Map @@ -172,11 +210,13 @@ def initialize(pattern = nil) @pattern = DEFAULT_PATTERN end @unbound_templates = Concurrent::Map.new + @path_parser = PathParser.new super() end def clear_cache @unbound_templates.clear + @path_parser = PathParser.new super() end @@ -278,18 +318,11 @@ def escape_entry(entry) # 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) - pieces = File.basename(path).split(".") - pieces.shift + details = @path_parser.parse(path) - extension = pieces.pop - - handler = Template.handler_for_extension(extension) - format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last - format = if format - Template::Types[format]&.ref - elsif handler.respond_to?(:default_format) # default_format can return nil - handler.default_format - end + handler = Template.handler_for_extension(details[:handler]) + format = details[:format] || handler.try(:default_format) + variant = details[:variant] # Template::Types[format] and handler.default_format can return nil [handler, format, variant] diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 36c4bc85670c6..ddd34da7bda52 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -59,8 +59,6 @@ def view end def render_erb(string) - @virtual_path = nil - template = ActionView::Template.new( string.strip, "test template", diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index f946e4360d8e9..0b2627c29007c 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -18,9 +18,13 @@ def self._implied_layout_name; to_s.underscore.gsub(/_controller$/, "") ; end module TemplateHandlerHelper def with_template_handler(*extensions, handler) ActionView::Template.register_template_handler(*extensions, handler) + ActionController::Base.view_paths.paths.each(&:clear_cache) + ActionView::LookupContext::DetailsKey.clear yield ensure ActionView::Template.unregister_template_handler(*extensions) + ActionController::Base.view_paths.paths.each(&:clear_cache) + ActionView::LookupContext::DetailsKey.clear end end diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb index 26f3bfbcbc84f..2a5cc67a66735 100644 --- a/actionview/test/activerecord/relation_cache_test.rb +++ b/actionview/test/activerecord/relation_cache_test.rb @@ -10,7 +10,6 @@ def setup view_paths = ActionController::Base.view_paths lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) @view_renderer = ActionView::Renderer.new(lookup_context) - @virtual_path = "path" @current_template = lookup_context.find "test/hello_world" controller.cache_store = ActiveSupport::Cache::MemoryStore.new diff --git a/actionview/test/fixtures/test/cache_fragment_inside_render_layout_block_1.html.erb b/actionview/test/fixtures/test/cache_fragment_inside_render_layout_block_1.html.erb new file mode 100644 index 0000000000000..60ec279ff480e --- /dev/null +++ b/actionview/test/fixtures/test/cache_fragment_inside_render_layout_block_1.html.erb @@ -0,0 +1 @@ +<%= render layout: "layouts/yield_only" do %><%= cache "foo" do %>CAT<% end %><% end %> \ No newline at end of file diff --git a/actionview/test/fixtures/test/cache_fragment_inside_render_layout_block_2.html.erb b/actionview/test/fixtures/test/cache_fragment_inside_render_layout_block_2.html.erb new file mode 100644 index 0000000000000..49df88f56cc48 --- /dev/null +++ b/actionview/test/fixtures/test/cache_fragment_inside_render_layout_block_2.html.erb @@ -0,0 +1 @@ +<%= render layout: "layouts/yield_only" do %><%= cache "foo" do %>DOG<% end %><% end %> \ No newline at end of file diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index e315d6e4e67e1..56f2674eb0cb0 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -22,6 +22,7 @@ def combined_fragment_cache_key(key) controller = TestController.new controller.perform_caching = true + controller.cache_store = :memory_store @view.controller = controller @controller_view = controller.view_context_class.with_empty_template_cache.new( @@ -705,6 +706,13 @@ def setup assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class setup_view(view_paths) end + + def test_cache_fragments_inside_render_layout_call_with_block + cat = @view.render(template: "test/cache_fragment_inside_render_layout_block_1") + dog = @view.render(template: "test/cache_fragment_inside_render_layout_block_2") + + assert_not_equal cat, dog + end end class LazyViewRenderTest < ActiveSupport::TestCase diff --git a/actionview/test/template/resolver_shared_tests.rb b/actionview/test/template/resolver_shared_tests.rb index 332eb30a58f0a..f513cca0bb78b 100644 --- a/actionview/test/template/resolver_shared_tests.rb +++ b/actionview/test/template/resolver_shared_tests.rb @@ -148,22 +148,83 @@ def test_templates_with_optional_locale_shares_common_object def test_templates_sort_by_formats_json_first with_file "test/hello_world.html.erb", "Hello HTML!" - with_file "test/hello_world.json.jbuilder", "Hello JSON!" + with_file "test/hello_world.json.builder", "Hello JSON!" - templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:json, :html], variants: :any, handlers: [:erb, :jbuilder]) + templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:json, :html], variants: :any, handlers: [:erb, :builder]) assert_equal 2, templates.size assert_equal "Hello JSON!", templates[0].source + assert_equal :json, templates[0].format + assert_equal "Hello HTML!", templates[1].source + assert_equal :html, templates[1].format end def test_templates_sort_by_formats_html_first with_file "test/hello_world.html.erb", "Hello HTML!" - with_file "test/hello_world.json.jbuilder", "Hello JSON!" + with_file "test/hello_world.json.builder", "Hello JSON!" - templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:html, :json], variants: :any, handlers: [:erb, :jbuilder]) + templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:html, :json], variants: :any, handlers: [:erb, :builder]) assert_equal 2, templates.size assert_equal "Hello HTML!", templates[0].source + assert_equal :html, templates[0].format + assert_equal "Hello JSON!", templates[1].source + assert_equal :json, templates[1].format + end + + def test_templates_with_variant + with_file "test/hello_world.html+mobile.erb", "Hello HTML!" + + templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:html, :json], variants: :any, handlers: [:erb, :builder]) + + assert_equal 1, templates.size + assert_equal "Hello HTML!", templates[0].source + assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler + assert_equal :html, templates[0].format + assert_equal "mobile", templates[0].variant + end + + def test_finds_variants_in_order + with_file "test/hello_world.html+tricorder.erb", "Hello Spock!" + with_file "test/hello_world.html+lcars.erb", "Hello Geordi!" + + tricorder = context.find("hello_world", "test", false, [], { variants: [:tricorder] }) + lcars = context.find("hello_world", "test", false, [], { variants: [:lcars] }) + + assert_equal "Hello Spock!", tricorder.source + assert_equal "tricorder", tricorder.variant + assert_equal "Hello Geordi!", lcars.source + assert_equal "lcars", lcars.variant + + templates = context.find_all("hello_world", "test", false, [], { variants: [:tricorder, :lcars] }) + assert_equal [tricorder, lcars], templates + + templates = context.find_all("hello_world", "test", false, [], { variants: [:lcars, :tricorder] }) + assert_equal [lcars, tricorder], templates + end + + def test_templates_no_format_with_variant + with_file "test/hello_world+mobile.erb", "Hello HTML!" + + templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:html, :json], variants: :any, handlers: [:erb, :builder]) + + assert_equal 1, templates.size + assert_equal "Hello HTML!", templates[0].source + assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler + assert_nil templates[0].format + assert_equal "mobile", templates[0].variant + end + + def test_templates_no_format_or_handler_with_variant + with_file "test/hello_world+mobile", "Hello HTML!" + + templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:html, :json], variants: :any, handlers: [:erb, :builder]) + + assert_equal 1, templates.size + assert_equal "Hello HTML!", templates[0].source + assert_kind_of ActionView::Template::Handlers::Raw, templates[0].handler + assert_nil templates[0].format + assert_equal "mobile", templates[0].variant end def test_virtual_path_is_preserved_with_dot diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index d8dcd33d691a1..e4ec4ee024ef2 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -22,7 +22,6 @@ class Context < ActionView::Base def initialize(*) super @output_buffer = "original" - @virtual_path = nil end def hello @@ -35,7 +34,7 @@ def apostrophe def partial ActionView::Template.new( - "<%= @virtual_path %>", + "<%= @current_template.virtual_path %>", "partial", ERBHandler, virtual_path: "partial", @@ -115,9 +114,9 @@ def test_restores_buffer end def test_virtual_path - @template = new_template("<%= @virtual_path %>" \ + @template = new_template("<%= @current_template.virtual_path %>" \ "<%= partial.render(self, {}) %>" \ - "<%= @virtual_path %>") + "<%= @current_template.virtual_path %>") assert_equal "hellopartialhello", render end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 5fa9abad45558..5ea9f908efefa 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -133,7 +133,7 @@ def scope def reflection_scope @reflection_scope ||= begin - reflection.join_scopes(klass.arel_table, klass.predicate_builder).inject(klass.unscoped, &:merge!) + reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass).inject(&:merge!) || klass.unscoped end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index de0b50504bc11..1192cbc58b37f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -612,6 +612,12 @@ def add_column(table_name, column_name, type, **options) execute schema_creation.accept at end + def add_columns(table_name, *column_names, type:, **options) # :nodoc: + column_names.each do |column_name| + add_column(table_name, column_name, type, **options) + end + end + # Removes the given columns from the table definition. # # remove_columns(:suppliers, :qualification, :experience) @@ -619,9 +625,11 @@ def add_column(table_name, column_name, type, **options) # +type+ and other column options can be passed to make migration reversible. # # remove_columns(:suppliers, :qualification, :experience, type: :string, null: false) - def remove_columns(table_name, *column_names, **options) - raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.empty? - type = options.delete(:type) + def remove_columns(table_name, *column_names, type: nil, **options) + if column_names.empty? + raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") + end + column_names.each do |column_name| remove_column(table_name, column_name, type, **options) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 3435e2884ec80..697c6e5de78fa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -565,17 +565,8 @@ def initialize_type_map(m = type_map) m.alias_type %r(year)i, "integer" m.alias_type %r(bit)i, "binary" - m.register_type(%r(enum)i) do |sql_type| - limit = sql_type[/^enum\s*\((.+)\)/i, 1] - .split(",").map { |enum| enum.strip.length - 2 }.max - MysqlString.new(limit: limit) - end - - m.register_type(%r(^set)i) do |sql_type| - limit = sql_type[/^set\s*\((.+)\)/i, 1] - .split(",").map { |set| set.strip.length - 1 }.sum - 1 - MysqlString.new(limit: limit) - end + m.register_type %r(^enum)i, MysqlString.new + m.register_type %r(^set)i, MysqlString.new end def register_integer_type(mapping, key, **options) 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 bcd300f3dbd60..f66bc84a4993c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -49,7 +49,7 @@ def schema_type(column) end def schema_limit(column) - super unless /\A(?:enum|set|(?:tiny|medium|long)?(?:text|blob))\b/.match?(column.sql_type) + super unless /\A(?:tiny|medium|long)?(?:text|blob)\b/.match?(column.sql_type) end def schema_precision(column) diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index b4dfc5f04c1b7..f71f77883fe19 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -74,12 +74,7 @@ def revert # recorder.record(:method_name, [:arg1, :arg2]) def record(*command, &block) if @reverting - inverse_command = inverse_of(*command, &block) - if inverse_command[0].is_a?(Array) - @commands = @commands.concat(inverse_command) - else - @commands << inverse_command - end + @commands << inverse_of(*command, &block) else @commands << (command << block) end @@ -186,11 +181,7 @@ def invert_remove_columns(args) raise ActiveRecord::IrreversibleMigration, "remove_columns is only reversible if given a type." end - column_options = args.extract_options! - type = column_options.delete(:type) - args[1..-1].map do |arg| - [:add_column, [args[0], arg, type, column_options]] - end + [:add_columns, args] end def invert_rename_index(args) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 934e231eee55a..60444a86d52dd 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -201,9 +201,9 @@ def join_scope(table, foreign_table, foreign_klass) klass_scope end - def join_scopes(table, predicate_builder) # :nodoc: + def join_scopes(table, predicate_builder, klass = self.klass) # :nodoc: if scope - [scope_for(build_scope(table, predicate_builder))] + [scope_for(build_scope(table, predicate_builder, klass))] else [] end @@ -292,7 +292,7 @@ def get_join_keys(association_klass) JoinKeys.new(join_primary_key(association_klass), join_foreign_key) end - def build_scope(table, predicate_builder = predicate_builder(table)) + def build_scope(table, predicate_builder = predicate_builder(table), klass = self.klass) Relation.create( klass, table: table, @@ -853,8 +853,8 @@ def scopes source_reflection.scopes + super end - def join_scopes(table, predicate_builder) # :nodoc: - source_reflection.join_scopes(table, predicate_builder) + super + def join_scopes(table, predicate_builder, klass = self.klass) # :nodoc: + source_reflection.join_scopes(table, predicate_builder, klass) + super end def has_scope? @@ -1017,9 +1017,9 @@ def initialize(reflection, previous_reflection) @previous_reflection = previous_reflection end - def join_scopes(table, predicate_builder) # :nodoc: + def join_scopes(table, predicate_builder, klass = self.klass) # :nodoc: scopes = @previous_reflection.join_scopes(table, predicate_builder) + super - scopes << build_scope(table, predicate_builder).instance_exec(nil, &source_type_scope) + scopes << build_scope(table, predicate_builder, klass).instance_exec(nil, &source_type_scope) end def constraints diff --git a/activerecord/lib/active_record/relation/query_composer.rb b/activerecord/lib/active_record/relation/query_composer.rb new file mode 100644 index 0000000000000..15d7dcd3fa9ea --- /dev/null +++ b/activerecord/lib/active_record/relation/query_composer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module ActiveRecord + class QueryComposer + delegate :[], to: :arel_table + + def initialize(model) + @model = model + @arel_table = model.arel_table + @reflections = model._reflections + define_attribute_accessors + end + + def method_missing(name, *_args) + if reflections.key?(name.to_s) + self.class.new(reflections[name.to_s].klass) + else + super + end + end + + private + attr_reader :model, :arel_table, :reflections + + def define_attribute_accessors + model.attribute_names.each do |attr| + define_singleton_method attr do + arel_table[attr] + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 4ca72b0cf9ccf..12bcaa10a9911 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -2,6 +2,7 @@ require "active_record/relation/from_clause" require "active_record/relation/query_attribute" +require "active_record/relation/query_composer" require "active_record/relation/where_clause" require "active_record/relation/where_clause_factory" require "active_model/forbidden_attributes_protection" @@ -662,14 +663,24 @@ def left_outer_joins!(*args) # :nodoc: # # If the condition is any blank-ish object, then #where is a no-op and returns # the current relation. - def where(opts = :chain, *rest) - if :chain == opts + def where(opts = :chain, *rest, &block) + new_scope = if :chain == opts && block_given? + self + elsif :chain == opts WhereChain.new(spawn) elsif opts.blank? self else spawn.where!(opts, *rest) end + + if block_given? + composer = QueryComposer.new(model) + constraints = composer.instance_exec(composer, &block) + new_scope.where!(constraints) + else + new_scope + end end def where!(opts, *rest) # :nodoc: @@ -1399,7 +1410,7 @@ def order_column(field) def resolve_arel_attributes(attrs) attrs.flat_map do |attr| case attr - when Arel::Attributes::Attribute + when Arel::Predications attr when Hash attr.flat_map do |table, columns| diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 3a706a02797ec..dd92beb136290 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/array/extract" + module ActiveRecord class Relation class WhereClause # :nodoc: @@ -101,7 +103,9 @@ def contradiction? def extract_attributes attrs = [] predicates.each do |node| - Arel.fetch_attribute(node) { |attr| attrs << attr } + Arel.fetch_attribute(node) { |attr| attrs << attr } || begin + attrs << node.left if node.equality? && node.left.is_a?(Arel::Predications) + end end attrs end @@ -153,15 +157,14 @@ def invert_predicate(node) end def except_predicates(columns) + attrs = columns.extract! { |node| node.is_a?(Arel::Attribute) } + non_attrs = columns.extract! { |node| node.is_a?(Arel::Predications) } + predicates.reject do |node| - Arel.fetch_attribute(node) do |attr| - columns.any? do |column| - if column.is_a?(Arel::Attributes::Attribute) - attr == column - else - attr.name.to_s == column.to_s - end - end + if !non_attrs.empty? && node.equality? && node.left.is_a?(Arel::Predications) + non_attrs.include?(node.left) + end || Arel.fetch_attribute(node) do |attr| + attrs.include?(attr) || columns.include?(attr.name.to_s) end end end diff --git a/activerecord/lib/active_record/signed_id.rb b/activerecord/lib/active_record/signed_id.rb index abd7ddf37e9ab..e9f1eba3e8f6e 100644 --- a/activerecord/lib/active_record/signed_id.rb +++ b/activerecord/lib/active_record/signed_id.rb @@ -40,7 +40,7 @@ module ClassMethods # travel_back # User.find_signed signed_id, purpose: :password_reset # => User.first def find_signed(signed_id, purpose: nil) - raise UnknownPrimaryKey.new(@klass) if primary_key.nil? + raise UnknownPrimaryKey.new(self) if primary_key.nil? if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose)) find_by primary_key => id diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index 1168b3677ebd2..50ee9191636b5 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -15,11 +15,6 @@ def setup end end - def test_enum_limit - column = EnumTest.columns_hash["enum_column"] - assert_equal 8, column.limit - end - def test_should_not_be_unsigned column = EnumTest.columns_hash["enum_column"] assert_not_predicate column, :unsigned? diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 9436abdb58b1f..4cdcba3bb68f3 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -465,7 +465,14 @@ def test_polymorphic_association_class def test_with_polymorphic_and_condition sponsor = Sponsor.create member = Member.create name: "Bert" + sponsor.sponsorable = member + sponsor.save! + + assert_equal member, sponsor.sponsorable + assert_nil sponsor.sponsorable_with_conditions + + sponsor = Sponsor.preload(:sponsorable, :sponsorable_with_conditions).last assert_equal member, sponsor.sponsorable assert_nil sponsor.sponsorable_with_conditions diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 5fae3aaefa674..34fab5a159c24 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -724,8 +724,7 @@ def test_pluck end def test_pluck_with_empty_in - Topic.send(:load_schema) - assert_no_queries do + assert_queries(0) do assert_equal [], Topic.where(id: []).pluck(:id) end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 769ee22985bae..82ba22714f96e 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -397,22 +397,19 @@ def test_find_by_ids_with_limit_and_offset end def test_find_with_large_number - Topic.send(:load_schema) - assert_no_queries do + assert_queries(0) do assert_raises(ActiveRecord::RecordNotFound) { Topic.find("9999999999999999999999999999999") } end end def test_find_by_with_large_number - Topic.send(:load_schema) - assert_no_queries do + assert_queries(0) do assert_nil Topic.find_by(id: "9999999999999999999999999999999") end end def test_find_by_id_with_large_number - Topic.send(:load_schema) - assert_no_queries do + assert_queries(0) do assert_nil Topic.find_by_id("9999999999999999999999999999999") end end diff --git a/activerecord/test/cases/null_relation_test.rb b/activerecord/test/cases/null_relation_test.rb index ee96ea1af6579..407aa232383c8 100644 --- a/activerecord/test/cases/null_relation_test.rb +++ b/activerecord/test/cases/null_relation_test.rb @@ -17,8 +17,7 @@ def test_none end def test_none_chainable - Developer.send(:load_schema) - assert_no_queries do + assert_queries(0) do assert_equal [], Developer.none.where(name: "David") end end diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 803d0e316e83e..fc7b4de0c00e1 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -130,21 +130,29 @@ def test_relation_to_sql end def test_relation_merging_with_arel_equalities_keeps_last_equality - devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge( - Developer.where(Developer.arel_table[:salary].eq(9000)) - ) + salary_attr = Developer.arel_table[:salary] + + devs = Developer.where(salary_attr.eq(80000)).merge(Developer.where(salary_attr.eq(9000))) + assert_equal [developers(:poor_jamis)], devs.to_a + + devs = Developer.where(salary_attr.eq(80000)).merge(Developer.where(salary_attr.eq(9000)), rewhere: true) + assert_equal [developers(:poor_jamis)], devs.to_a + + devs = Developer.where(salary_attr.eq(80000)).rewhere(salary_attr.eq(9000)) assert_equal [developers(:poor_jamis)], devs.to_a end def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand salary_attr = Developer.arel_table[:salary] - devs = Developer.where( - Arel::Nodes::NamedFunction.new("abs", [salary_attr]).eq(80000) - ).merge( - Developer.where( - Arel::Nodes::NamedFunction.new("abs", [salary_attr]).eq(9000) - ) - ) + abs_salary = Arel::Nodes::NamedFunction.new("abs", [salary_attr]) + + devs = Developer.where(abs_salary.eq(80000)).merge(Developer.where(abs_salary.eq(9000))) + assert_equal [developers(:poor_jamis)], devs.to_a + + devs = Developer.where(abs_salary.eq(80000)).merge(Developer.where(abs_salary.eq(9000)), rewhere: true) + assert_equal [developers(:poor_jamis)], devs.to_a + + devs = Developer.where(abs_salary.eq(80000)).rewhere(abs_salary.eq(9000)) assert_equal [developers(:poor_jamis)], devs.to_a end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 2b5f6d942c834..d09eb1b0cb8fa 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -419,15 +419,13 @@ def test_skip_preloading_after_arel_has_been_generated end test "no queries on empty IN" do - Post.send(:load_schema) - assert_no_queries do + assert_queries(0) do Post.where(id: []).load end end test "can unscope empty IN" do - Post.send(:load_schema) - assert_queries 1 do + assert_queries(1) do Post.where(id: []).unscope(where: :id).load end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 078e6a188ae09..3dccc4070e557 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -63,6 +63,26 @@ def test_multivalue_where assert_equal 1, posts.to_a.size end + def test_block_where + posts = Post.where { |post| post[:author_id].eq(1).and(post[:id].eq(1)) } + assert_equal 1, posts.to_a.size + end + + def test_block_where_can_chain + posts = Post.where { |post| post[:author_id].eq(1) }.where { |post| post[:id].eq(1) } + assert_equal 1, posts.to_a.size + end + + def test_block_method_where + posts = Post.where { |post| post.author_id.eq(1).and(post.id.eq(1)) } + assert_equal 1, posts.to_a.size + end + + def test_block_instance_method_where + posts = Post.where { author_id.eq(1).and(id.eq(1)) } + assert_equal 1, posts.to_a.size + end + def test_scoped topics = Topic.all assert_kind_of ActiveRecord::Relation, topics @@ -504,6 +524,24 @@ def test_finding_with_hash_conditions_on_joined_table assert_equal companies(:rails_core), firms.first end + def tests_finding_with_block_on_joined_table + firms = DependentFirm.joins(:account).where { |firm| firm[:name].eq("RailsCore").and(firm.accounts[:credit_limit].eq(55..60)) }.to_a + assert_equal 1, firms.size + assert_equal companies(:rails_core), firms.first + end + + def tests_finding_with_block_and_methods_on_joined_table + firms = DependentFirm.joins(:account).where { |firm| firm.name.eq("RailsCore").and(firm.accounts.credit_limit.eq(55..60)) }.to_a + assert_equal 1, firms.size + assert_equal companies(:rails_core), firms.first + end + + def tests_finding_with_block_and_instance_methods_on_joined_table + firms = DependentFirm.joins(:account).where { name.eq("RailsCore").and(accounts.credit_limit.eq(55..60)) }.to_a + assert_equal 1, firms.size + assert_equal companies(:rails_core), firms.first + end + def test_find_all_with_join developers_on_project_one = Developer.joins("LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id"). where("project_id=1").to_a diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 2cb28506bc9ec..6b4290c737ce7 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -367,10 +367,10 @@ def test_three_level_nested_exclusive_scoped_find Developer.where("name = 'Jamis'").scoping do assert_equal "Jamis", Developer.first.name - Developer.unscoped.where("name = 'David'") do + Developer.unscoped.where("name = 'David'").scoping do assert_equal "David", Developer.first.name - Developer.unscoped.where("name = 'Maiha'") do + Developer.unscoped.where("name = 'Maiha'").scoping do assert_nil Developer.first end diff --git a/activerecord/test/cases/signed_id_test.rb b/activerecord/test/cases/signed_id_test.rb index 9933401ebe83d..d42a4d02f2b0c 100644 --- a/activerecord/test/cases/signed_id_test.rb +++ b/activerecord/test/cases/signed_id_test.rb @@ -27,9 +27,10 @@ class SignedIdTest < ActiveRecord::TestCase end test "raise UnknownPrimaryKey when model have no primary key" do - assert_raises(ActiveRecord::UnknownPrimaryKey) do + error = assert_raises(ActiveRecord::UnknownPrimaryKey) do Matey.find_signed("this will not be even verified") end + assert_equal "Unknown primary key for table mateys in model Matey.", error.message end test "find signed record with a bang" do diff --git a/activestorage/lib/active_storage/attached/model.rb b/activestorage/lib/active_storage/attached/model.rb index 027d52c459bee..2c600fae01d7f 100644 --- a/activestorage/lib/active_storage/attached/model.rb +++ b/activestorage/lib/active_storage/attached/model.rb @@ -46,7 +46,8 @@ def has_one_attached(name, dependent: :purge_later, service: nil) generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 # frozen_string_literal: true def #{name} - @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self) + @active_storage_attached ||= {} + @active_storage_attached[:#{name}] ||= ActiveStorage::Attached::One.new("#{name}", self) end def #{name}=(attachable) @@ -116,7 +117,8 @@ def has_many_attached(name, dependent: :purge_later, service: nil) generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 # frozen_string_literal: true def #{name} - @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self) + @active_storage_attached ||= {} + @active_storage_attached[:#{name}] ||= ActiveStorage::Attached::Many.new("#{name}", self) end def #{name}=(attachables) @@ -183,6 +185,12 @@ def changed_for_autosave? #:nodoc: super || attachment_changes.any? end + def initialize_dup(*) #:nodoc: + super + @active_storage_attached = nil + @attachment_changes = nil + end + def reload(*) #:nodoc: super.tap { @attachment_changes = nil } end diff --git a/activestorage/test/models/attached/many_test.rb b/activestorage/test/models/attached/many_test.rb index 6be89a6afa799..62620f9f84b04 100644 --- a/activestorage/test/models/attached/many_test.rb +++ b/activestorage/test/models/attached/many_test.rb @@ -552,6 +552,20 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase end end + test "duped record does not share attachments" do + @user.highlights.attach [ create_blob(filename: "funky.jpg") ] + + assert_not_equal @user.highlights.first, @user.dup.highlights.first + end + + test "duped record does not share attachment changes" do + @user.highlights.attach [ create_blob(filename: "funky.jpg") ] + assert_not_predicate @user, :changed_for_autosave? + + @user.dup.highlights.attach [ create_blob(filename: "town.mp4") ] + assert_not_predicate @user, :changed_for_autosave? + end + test "clearing change on reload" do @user.highlights = [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ] assert @user.highlights.attached? diff --git a/activestorage/test/models/attached/one_test.rb b/activestorage/test/models/attached/one_test.rb index 3d5a7dc3602c6..f3c5ead528a14 100644 --- a/activestorage/test/models/attached/one_test.rb +++ b/activestorage/test/models/attached/one_test.rb @@ -529,6 +529,20 @@ def upload.open end end + test "duped record does not share attachments" do + @user.avatar.attach create_blob(filename: "funky.jpg") + + assert_not_equal @user.avatar.attachment, @user.dup.avatar.attachment + end + + test "duped record does not share attachment changes" do + @user.avatar.attach create_blob(filename: "funky.jpg") + assert_not_predicate @user, :changed_for_autosave? + + @user.dup.avatar.attach create_blob(filename: "town.jpg") + assert_not_predicate @user, :changed_for_autosave? + end + test "clearing change on reload" do @user.avatar = create_blob(filename: "funky.jpg") assert @user.avatar.attached? diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb index ab055c7648f7f..84f0dd7a0d824 100644 --- a/railties/test/application/per_request_digest_cache_test.rb +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -62,7 +62,7 @@ def index end test "template digests are cleared before a request" do - assert_called(ActionView::LookupContext::DetailsKey, :clear) do + assert_called(ActionView::LookupContext::DetailsKey, :clear, times: 3) do get "/customers" assert_equal 200, last_response.status end diff --git a/railties/test/application/rendering_test.rb b/railties/test/application/rendering_test.rb index ab1591f38801f..bf2382695f513 100644 --- a/railties/test/application/rendering_test.rb +++ b/railties/test/application/rendering_test.rb @@ -42,5 +42,47 @@ def show get "/pages/foo.bar" assert_equal 200, last_response.status end + + test "New formats and handlers are detected from initializers" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: 'pages#show' + end + RUBY + + app_file "app/controllers/pages_controller.rb", <<-RUBY + class PagesController < ApplicationController + layout false + + def show + render :show, formats: [:awesome], handlers: [:rubby] + end + end + RUBY + + app_file "app/views/pages/show.awesome.rubby", <<-RUBY + { + format: @current_template.format, + handler: @current_template.handler + }.inspect + RUBY + + app_file "config/initializers/mime_types.rb", <<-RUBY + Mime::Type.register "text/awesome", :awesome + RUBY + + app_file "config/initializers/template_handlers.rb", <<-RUBY + module RubbyHandler + def self.call(_, source) + source + end + end + ActionView::Template.register_template_handler(:rubby, RubbyHandler) + RUBY + + get "/" + assert_equal 200, last_response.status + assert_equal "{:format=>:awesome, :handler=>RubbyHandler}", last_response.body + end end end