mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #38594 from rails/collection-refactor
Refactoring PartialRenderer
This commit is contained in:
commit
fc4ef77d47
12 changed files with 387 additions and 342 deletions
|
@ -51,6 +51,8 @@ module ActionView
|
|||
autoload :Renderer
|
||||
autoload :AbstractRenderer
|
||||
autoload :PartialRenderer
|
||||
autoload :CollectionRenderer
|
||||
autoload :ObjectRenderer
|
||||
autoload :TemplateRenderer
|
||||
autoload :StreamingTemplateRenderer
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "concurrent/map"
|
||||
|
||||
module ActionView
|
||||
# This class defines the interface for a renderer. Each class that
|
||||
# subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
|
||||
|
@ -27,6 +29,86 @@ module ActionView
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
module ObjectRendering # :nodoc:
|
||||
PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
|
||||
h[k] = Concurrent::Map.new
|
||||
end
|
||||
|
||||
def initialize(lookup_context, options)
|
||||
super
|
||||
@context_prefix = lookup_context.prefixes.first
|
||||
end
|
||||
|
||||
private
|
||||
def local_variable(path)
|
||||
if as = @options[:as]
|
||||
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
|
||||
as.to_sym
|
||||
else
|
||||
begin
|
||||
base = path[-1] == "/" ? "" : File.basename(path)
|
||||
raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
|
||||
$1.to_sym
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
|
||||
"make sure your partial name starts with underscore."
|
||||
|
||||
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
|
||||
"make sure it starts with lowercase letter, " \
|
||||
"and is followed by any combination of letters, numbers and underscores."
|
||||
|
||||
def raise_invalid_identifier(path)
|
||||
raise ArgumentError, IDENTIFIER_ERROR_MESSAGE % path
|
||||
end
|
||||
|
||||
def raise_invalid_option_as(as)
|
||||
raise ArgumentError, OPTION_AS_ERROR_MESSAGE % as
|
||||
end
|
||||
|
||||
# Obtains the path to where the object's partial is located. If the object
|
||||
# responds to +to_partial_path+, then +to_partial_path+ will be called and
|
||||
# will provide the path. If the object does not respond to +to_partial_path+,
|
||||
# then an +ArgumentError+ is raised.
|
||||
#
|
||||
# If +prefix_partial_path_with_controller_namespace+ is true, then this
|
||||
# method will prefix the partial paths with a namespace.
|
||||
def partial_path(object, view)
|
||||
object = object.to_model if object.respond_to?(:to_model)
|
||||
|
||||
path = if object.respond_to?(:to_partial_path)
|
||||
object.to_partial_path
|
||||
else
|
||||
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
|
||||
end
|
||||
|
||||
if view.prefix_partial_path_with_controller_namespace
|
||||
PREFIXED_PARTIAL_NAMES[@context_prefix][path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
|
||||
else
|
||||
path
|
||||
end
|
||||
end
|
||||
|
||||
def merge_prefix_into_object_path(prefix, object_path)
|
||||
if prefix.include?(?/) && object_path.include?(?/)
|
||||
prefixes = []
|
||||
prefix_array = File.dirname(prefix).split("/")
|
||||
object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
|
||||
|
||||
prefix_array.each_with_index do |dir, index|
|
||||
break if dir == object_path_array[index]
|
||||
prefixes << dir
|
||||
end
|
||||
|
||||
(prefixes << object_path).join("/")
|
||||
else
|
||||
object_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class RenderedCollection # :nodoc:
|
||||
def self.empty(format)
|
||||
EmptyCollection.new format
|
||||
|
@ -74,12 +156,18 @@ module ActionView
|
|||
end
|
||||
|
||||
private
|
||||
NO_DETAILS = {}.freeze
|
||||
|
||||
def extract_details(options) # :doc:
|
||||
@lookup_context.registered_details.each_with_object({}) do |key, details|
|
||||
details = nil
|
||||
@lookup_context.registered_details.each do |key|
|
||||
value = options[key]
|
||||
|
||||
details[key] = Array(value) if value
|
||||
if value
|
||||
(details ||= {})[key] = Array(value)
|
||||
end
|
||||
end
|
||||
details || NO_DETAILS
|
||||
end
|
||||
|
||||
def instrument(name, **options) # :doc:
|
||||
|
|
188
actionview/lib/action_view/renderer/collection_renderer.rb
Normal file
188
actionview/lib/action_view/renderer/collection_renderer.rb
Normal file
|
@ -0,0 +1,188 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "action_view/renderer/partial_renderer"
|
||||
|
||||
module ActionView
|
||||
class PartialIteration
|
||||
# The number of iterations that will be done by the partial.
|
||||
attr_reader :size
|
||||
|
||||
# The current iteration of the partial.
|
||||
attr_reader :index
|
||||
|
||||
def initialize(size)
|
||||
@size = size
|
||||
@index = 0
|
||||
end
|
||||
|
||||
# Check if this is the first iteration of the partial.
|
||||
def first?
|
||||
index == 0
|
||||
end
|
||||
|
||||
# Check if this is the last iteration of the partial.
|
||||
def last?
|
||||
index == size - 1
|
||||
end
|
||||
|
||||
def iterate! # :nodoc:
|
||||
@index += 1
|
||||
end
|
||||
end
|
||||
|
||||
class CollectionRenderer < PartialRenderer # :nodoc:
|
||||
include ObjectRendering
|
||||
|
||||
class CollectionIterator # :nodoc:
|
||||
include Enumerable
|
||||
|
||||
def initialize(collection)
|
||||
@collection = collection
|
||||
end
|
||||
|
||||
def each(&blk)
|
||||
@collection.each(&blk)
|
||||
end
|
||||
|
||||
def size
|
||||
@collection.size
|
||||
end
|
||||
end
|
||||
|
||||
class SameCollectionIterator < CollectionIterator # :nodoc:
|
||||
def initialize(collection, path, variables)
|
||||
super(collection)
|
||||
@path = path
|
||||
@variables = variables
|
||||
end
|
||||
|
||||
def from_collection(collection)
|
||||
self.class.new(collection, @path, @variables)
|
||||
end
|
||||
|
||||
def each_with_info
|
||||
return enum_for(:each_with_info) unless block_given?
|
||||
variables = [@path] + @variables
|
||||
@collection.each { |o| yield(o, variables) }
|
||||
end
|
||||
end
|
||||
|
||||
class PreloadCollectionIterator < SameCollectionIterator # :nodoc:
|
||||
def initialize(collection, path, variables, relation)
|
||||
super(collection, path, variables)
|
||||
relation.skip_preloading! unless relation.loaded?
|
||||
@relation = relation
|
||||
end
|
||||
|
||||
def from_collection(collection)
|
||||
self.class.new(collection, @path, @variables, @relation)
|
||||
end
|
||||
|
||||
def each_with_info
|
||||
return super unless block_given?
|
||||
@relation.preload_associations(@collection)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
class MixedCollectionIterator < CollectionIterator # :nodoc:
|
||||
def initialize(collection, paths)
|
||||
super(collection)
|
||||
@paths = paths
|
||||
end
|
||||
|
||||
def each_with_info
|
||||
return enum_for(:each_with_info) unless block_given?
|
||||
@collection.each_with_index { |o, i| yield(o, @paths[i]) }
|
||||
end
|
||||
end
|
||||
|
||||
def render_collection_with_partial(collection, partial, context, block)
|
||||
iter_vars = retrieve_variable(partial)
|
||||
|
||||
collection = if collection.respond_to?(:preload_associations)
|
||||
PreloadCollectionIterator.new(collection, partial, iter_vars, collection)
|
||||
else
|
||||
SameCollectionIterator.new(collection, partial, iter_vars)
|
||||
end
|
||||
|
||||
|
||||
template = find_template(partial, @locals.keys + iter_vars)
|
||||
|
||||
layout = if !block && (layout = @options[:layout])
|
||||
find_template(layout.to_s, @locals.keys + iter_vars)
|
||||
end
|
||||
|
||||
render_collection(collection, context, partial, template, layout, block)
|
||||
end
|
||||
|
||||
def render_collection_derive_partial(collection, context, block)
|
||||
paths = collection.map { |o| partial_path(o, context) }
|
||||
|
||||
if paths.uniq.length == 1
|
||||
# Homogeneous
|
||||
render_collection_with_partial(collection, paths.first, context, block)
|
||||
else
|
||||
if @options[:cached]
|
||||
raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
|
||||
end
|
||||
|
||||
paths.map! { |path| retrieve_variable(path).unshift(path) }
|
||||
collection = MixedCollectionIterator.new(collection, paths)
|
||||
render_collection(collection, context, nil, nil, nil, block)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def retrieve_variable(path)
|
||||
variable = local_variable(path)
|
||||
[variable, :"#{variable}_counter", :"#{variable}_iteration"]
|
||||
end
|
||||
|
||||
def render_collection(collection, view, path, template, layout, block)
|
||||
identifier = (template && template.identifier) || path
|
||||
instrument(:collection, identifier: identifier, count: collection.size) do |payload|
|
||||
spacer = if @options.key?(:spacer_template)
|
||||
spacer_template = find_template(@options[:spacer_template], @locals.keys)
|
||||
build_rendered_template(spacer_template.render(view, @locals), spacer_template)
|
||||
else
|
||||
RenderedTemplate::EMPTY_SPACER
|
||||
end
|
||||
|
||||
collection_body = if template
|
||||
cache_collection_render(payload, view, template, collection) do |filtered_collection|
|
||||
collection_with_template(view, template, layout, filtered_collection)
|
||||
end
|
||||
else
|
||||
collection_with_template(view, nil, layout, collection)
|
||||
end
|
||||
|
||||
return RenderedCollection.empty(@lookup_context.formats.first) if collection_body.empty?
|
||||
|
||||
build_rendered_collection(collection_body, spacer)
|
||||
end
|
||||
end
|
||||
|
||||
def collection_with_template(view, template, layout, collection)
|
||||
locals = @locals
|
||||
cache = {}
|
||||
|
||||
partial_iteration = PartialIteration.new(collection.size)
|
||||
|
||||
collection.each_with_info.map do |object, (path, as, counter, iteration)|
|
||||
index = partial_iteration.index
|
||||
|
||||
locals[as] = object
|
||||
locals[counter] = index
|
||||
locals[iteration] = partial_iteration
|
||||
|
||||
_template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
|
||||
|
||||
content = _template.render(view, locals)
|
||||
content = layout.render(view, locals) { content } if layout
|
||||
partial_iteration.iterate!
|
||||
build_rendered_template(content, _template)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
34
actionview/lib/action_view/renderer/object_renderer.rb
Normal file
34
actionview/lib/action_view/renderer/object_renderer.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActionView
|
||||
class ObjectRenderer < PartialRenderer # :nodoc:
|
||||
include ObjectRendering
|
||||
|
||||
def initialize(lookup_context, options)
|
||||
super
|
||||
@object = nil
|
||||
@local_name = nil
|
||||
end
|
||||
|
||||
def render_object_with_partial(object, partial, context, block)
|
||||
@object = object
|
||||
@local_name = local_variable(partial)
|
||||
render(partial, context, block)
|
||||
end
|
||||
|
||||
def render_object_derive_partial(object, context, block)
|
||||
path = partial_path(object, context)
|
||||
render_object_with_partial(object, path, context, block)
|
||||
end
|
||||
|
||||
private
|
||||
def template_keys(path)
|
||||
super + [@local_name]
|
||||
end
|
||||
|
||||
def render_partial_template(view, locals, template, layout, block)
|
||||
locals[@local_name || template.variable] = @object
|
||||
super(view, locals, template, layout, block)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,36 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "concurrent/map"
|
||||
require "action_view/renderer/partial_renderer/collection_caching"
|
||||
|
||||
module ActionView
|
||||
class PartialIteration
|
||||
# The number of iterations that will be done by the partial.
|
||||
attr_reader :size
|
||||
|
||||
# The current iteration of the partial.
|
||||
attr_reader :index
|
||||
|
||||
def initialize(size)
|
||||
@size = size
|
||||
@index = 0
|
||||
end
|
||||
|
||||
# Check if this is the first iteration of the partial.
|
||||
def first?
|
||||
index == 0
|
||||
end
|
||||
|
||||
# Check if this is the last iteration of the partial.
|
||||
def last?
|
||||
index == size - 1
|
||||
end
|
||||
|
||||
def iterate! # :nodoc:
|
||||
@index += 1
|
||||
end
|
||||
end
|
||||
|
||||
# = Action View Partials
|
||||
#
|
||||
# There's also a convenience method for rendering sub templates within the current controller that depends on a
|
||||
|
@ -282,78 +254,30 @@ module ActionView
|
|||
class PartialRenderer < AbstractRenderer
|
||||
include CollectionCaching
|
||||
|
||||
PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
|
||||
h[k] = Concurrent::Map.new
|
||||
def initialize(lookup_context, options)
|
||||
super(lookup_context)
|
||||
@options = options
|
||||
@locals = @options[:locals] || {}
|
||||
@details = extract_details(@options)
|
||||
end
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
@context_prefix = @lookup_context.prefixes.first
|
||||
end
|
||||
def render(partial, context, block)
|
||||
template = find_template(partial, template_keys(partial))
|
||||
|
||||
def render(context, options, block)
|
||||
as = as_variable(options)
|
||||
setup(context, options, as, block)
|
||||
|
||||
if @path
|
||||
if @has_object || @collection
|
||||
@variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
|
||||
@template_keys = retrieve_template_keys(@variable)
|
||||
else
|
||||
@template_keys = @locals.keys
|
||||
end
|
||||
template = find_template(@path, @template_keys)
|
||||
@variable ||= template.variable
|
||||
else
|
||||
if options[:cached]
|
||||
raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
|
||||
end
|
||||
template = nil
|
||||
if !block && (layout = @options[:layout])
|
||||
layout = find_template(layout.to_s, template_keys(partial))
|
||||
end
|
||||
|
||||
if @collection
|
||||
render_collection(context, template)
|
||||
else
|
||||
render_partial(context, template)
|
||||
end
|
||||
render_partial_template(context, @locals, template, layout, block)
|
||||
end
|
||||
|
||||
private
|
||||
def render_collection(view, template)
|
||||
identifier = (template && template.identifier) || @path
|
||||
instrument(:collection, identifier: identifier, count: @collection.size) do |payload|
|
||||
return RenderedCollection.empty(@lookup_context.formats.first) if @collection.blank?
|
||||
|
||||
spacer = if @options.key?(:spacer_template)
|
||||
spacer_template = find_template(@options[:spacer_template], @locals.keys)
|
||||
build_rendered_template(spacer_template.render(view, @locals), spacer_template)
|
||||
else
|
||||
RenderedTemplate::EMPTY_SPACER
|
||||
end
|
||||
|
||||
collection_body = if template
|
||||
cache_collection_render(payload, view, template) do
|
||||
collection_with_template(view, template)
|
||||
end
|
||||
else
|
||||
collection_without_template(view)
|
||||
end
|
||||
build_rendered_collection(collection_body, spacer)
|
||||
end
|
||||
def template_keys(_)
|
||||
@locals.keys
|
||||
end
|
||||
|
||||
def render_partial(view, template)
|
||||
def render_partial_template(view, locals, template, layout, block)
|
||||
instrument(:partial, identifier: template.identifier) do |payload|
|
||||
locals, block = @locals, @block
|
||||
object, as = @object, @variable
|
||||
|
||||
if !block && (layout = @options[:layout])
|
||||
layout = find_template(layout.to_s, @template_keys)
|
||||
end
|
||||
|
||||
object = locals[as] if object.nil? # Respect object when object is false
|
||||
locals[as] = object if @has_object
|
||||
|
||||
content = template.render(view, locals) do |*name|
|
||||
view._layout_for(*name, &block)
|
||||
end
|
||||
|
@ -364,195 +288,9 @@ module ActionView
|
|||
end
|
||||
end
|
||||
|
||||
# Sets up instance variables needed for rendering a partial. This method
|
||||
# finds the options and details and extracts them. The method also contains
|
||||
# logic that handles the type of object passed in as the partial.
|
||||
#
|
||||
# If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
|
||||
# set to that string. Otherwise, the +options[:partial]+ object must
|
||||
# respond to +to_partial_path+ in order to set up the path.
|
||||
def setup(context, options, as, block)
|
||||
@options = options
|
||||
@block = block
|
||||
|
||||
@locals = options[:locals] || {}
|
||||
@details = extract_details(options)
|
||||
|
||||
partial = options[:partial]
|
||||
|
||||
if String === partial
|
||||
@has_object = options.key?(:object)
|
||||
@object = options[:object]
|
||||
@collection = collection_from_options
|
||||
@path = partial
|
||||
else
|
||||
@has_object = true
|
||||
@object = partial
|
||||
@collection = collection_from_object || collection_from_options
|
||||
|
||||
if @collection
|
||||
paths = @collection_data = @collection.map { |o| partial_path(o, context) }
|
||||
if paths.uniq.length == 1
|
||||
@path = paths.first
|
||||
else
|
||||
paths.map! { |path| retrieve_variable(path, as).unshift(path) }
|
||||
@path = nil
|
||||
end
|
||||
else
|
||||
@path = partial_path(@object, context)
|
||||
end
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def as_variable(options)
|
||||
if as = options[:as]
|
||||
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
|
||||
as.to_sym
|
||||
end
|
||||
end
|
||||
|
||||
def collection_from_options
|
||||
if @options.key?(:collection)
|
||||
collection = @options[:collection]
|
||||
collection ? collection.to_a : []
|
||||
end
|
||||
end
|
||||
|
||||
def collection_from_object
|
||||
@object.to_ary if @object.respond_to?(:to_ary)
|
||||
end
|
||||
|
||||
def find_template(path, locals)
|
||||
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
|
||||
@lookup_context.find_template(path, prefixes, true, locals, @details)
|
||||
end
|
||||
|
||||
def collection_with_template(view, template)
|
||||
locals = @locals
|
||||
as, counter, iteration = @variable, @variable_counter, @variable_iteration
|
||||
|
||||
if layout = @options[:layout]
|
||||
layout = find_template(layout, @template_keys)
|
||||
end
|
||||
|
||||
partial_iteration = PartialIteration.new(@collection.size)
|
||||
locals[iteration] = partial_iteration
|
||||
|
||||
@collection.map do |object|
|
||||
locals[as] = object
|
||||
locals[counter] = partial_iteration.index
|
||||
|
||||
content = template.render(view, locals)
|
||||
content = layout.render(view, locals) { content } if layout
|
||||
partial_iteration.iterate!
|
||||
build_rendered_template(content, template)
|
||||
end
|
||||
end
|
||||
|
||||
def collection_without_template(view)
|
||||
locals, collection_data = @locals, @collection_data
|
||||
cache = {}
|
||||
keys = @locals.keys
|
||||
|
||||
partial_iteration = PartialIteration.new(@collection.size)
|
||||
|
||||
@collection.map do |object|
|
||||
index = partial_iteration.index
|
||||
path, as, counter, iteration = collection_data[index]
|
||||
|
||||
locals[as] = object
|
||||
locals[counter] = index
|
||||
locals[iteration] = partial_iteration
|
||||
|
||||
template = (cache[path] ||= find_template(path, keys + [as, counter, iteration]))
|
||||
content = template.render(view, locals)
|
||||
partial_iteration.iterate!
|
||||
build_rendered_template(content, template)
|
||||
end
|
||||
end
|
||||
|
||||
# Obtains the path to where the object's partial is located. If the object
|
||||
# responds to +to_partial_path+, then +to_partial_path+ will be called and
|
||||
# will provide the path. If the object does not respond to +to_partial_path+,
|
||||
# then an +ArgumentError+ is raised.
|
||||
#
|
||||
# If +prefix_partial_path_with_controller_namespace+ is true, then this
|
||||
# method will prefix the partial paths with a namespace.
|
||||
def partial_path(object, view)
|
||||
object = object.to_model if object.respond_to?(:to_model)
|
||||
|
||||
path = if object.respond_to?(:to_partial_path)
|
||||
object.to_partial_path
|
||||
else
|
||||
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
|
||||
end
|
||||
|
||||
if view.prefix_partial_path_with_controller_namespace
|
||||
prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
|
||||
else
|
||||
path
|
||||
end
|
||||
end
|
||||
|
||||
def prefixed_partial_names
|
||||
@prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
|
||||
end
|
||||
|
||||
def merge_prefix_into_object_path(prefix, object_path)
|
||||
if prefix.include?(?/) && object_path.include?(?/)
|
||||
prefixes = []
|
||||
prefix_array = File.dirname(prefix).split("/")
|
||||
object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
|
||||
|
||||
prefix_array.each_with_index do |dir, index|
|
||||
break if dir == object_path_array[index]
|
||||
prefixes << dir
|
||||
end
|
||||
|
||||
(prefixes << object_path).join("/")
|
||||
else
|
||||
object_path
|
||||
end
|
||||
end
|
||||
|
||||
def retrieve_template_keys(variable)
|
||||
keys = @locals.keys
|
||||
keys << variable
|
||||
if @collection
|
||||
keys << @variable_counter
|
||||
keys << @variable_iteration
|
||||
end
|
||||
keys
|
||||
end
|
||||
|
||||
def retrieve_variable(path, as)
|
||||
variable = as || begin
|
||||
base = path[-1] == "/" ? "" : File.basename(path)
|
||||
raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
|
||||
$1.to_sym
|
||||
end
|
||||
if @collection
|
||||
variable_counter = :"#{variable}_counter"
|
||||
variable_iteration = :"#{variable}_iteration"
|
||||
end
|
||||
[variable, variable_counter, variable_iteration]
|
||||
end
|
||||
|
||||
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
|
||||
"make sure your partial name starts with underscore."
|
||||
|
||||
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
|
||||
"make sure it starts with lowercase letter, " \
|
||||
"and is followed by any combination of letters, numbers and underscores."
|
||||
|
||||
def raise_invalid_identifier(path)
|
||||
raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
|
||||
end
|
||||
|
||||
def raise_invalid_option_as(as)
|
||||
raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,13 +13,19 @@ module ActionView
|
|||
end
|
||||
|
||||
private
|
||||
def cache_collection_render(instrumentation_payload, view, template)
|
||||
return yield unless @options[:cached] && view.controller.respond_to?(:perform_caching) && view.controller.perform_caching
|
||||
def will_cache?(options, view)
|
||||
options[:cached] && view.controller.respond_to?(:perform_caching) && view.controller.perform_caching
|
||||
end
|
||||
|
||||
def cache_collection_render(instrumentation_payload, view, template, collection)
|
||||
return yield(collection) unless will_cache?(@options, view)
|
||||
|
||||
collection_iterator = collection
|
||||
|
||||
# Result is a hash with the key represents the
|
||||
# key used for cache lookup and the value is the item
|
||||
# on which the partial is being rendered
|
||||
keyed_collection, ordered_keys = collection_by_cache_keys(view, template)
|
||||
keyed_collection, ordered_keys = collection_by_cache_keys(view, template, collection)
|
||||
|
||||
# Pull all partials from cache
|
||||
# Result is a hash, key matches the entry in
|
||||
|
@ -29,17 +35,9 @@ module ActionView
|
|||
instrumentation_payload[:cache_hits] = cached_partials.size
|
||||
|
||||
# Extract the items for the keys that are not found
|
||||
# Set the uncached values to instance variable @collection
|
||||
# which is used by the caller
|
||||
@collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
|
||||
collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
|
||||
|
||||
# If all elements are already in cache then
|
||||
# rendered partials will be an empty array
|
||||
#
|
||||
# If the cache is missing elements then
|
||||
# the block will be called against the remaining items
|
||||
# in the @collection.
|
||||
rendered_partials = @collection.empty? ? [] : yield
|
||||
rendered_partials = collection.empty? ? [] : yield(collection_iterator.from_collection(collection))
|
||||
|
||||
index = 0
|
||||
keyed_partials = fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
|
||||
|
@ -57,12 +55,12 @@ module ActionView
|
|||
@options[:cached].respond_to?(:call)
|
||||
end
|
||||
|
||||
def collection_by_cache_keys(view, template)
|
||||
def collection_by_cache_keys(view, template, collection)
|
||||
seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
|
||||
|
||||
digest_path = view.digest_path_from_template(template)
|
||||
|
||||
@collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
|
||||
collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
|
||||
key = expanded_cache_key(seed.call(item), view, template, digest_path)
|
||||
ordered_keys << key
|
||||
hash[key] = item
|
||||
|
|
|
@ -65,7 +65,50 @@ module ActionView
|
|||
end
|
||||
|
||||
def render_partial_to_object(context, options, &block) #:nodoc:
|
||||
PartialRenderer.new(@lookup_context).render(context, options, block)
|
||||
partial = options[:partial]
|
||||
if String === partial
|
||||
collection = collection_from_options(options)
|
||||
|
||||
if collection
|
||||
# Collection + Partial
|
||||
renderer = CollectionRenderer.new(@lookup_context, options)
|
||||
renderer.render_collection_with_partial(collection, partial, context, block)
|
||||
else
|
||||
if options.key?(:object)
|
||||
# Object + Partial
|
||||
renderer = ObjectRenderer.new(@lookup_context, options)
|
||||
renderer.render_object_with_partial(options[:object], partial, context, block)
|
||||
else
|
||||
# Partial
|
||||
renderer = PartialRenderer.new(@lookup_context, options)
|
||||
renderer.render(partial, context, block)
|
||||
end
|
||||
end
|
||||
else
|
||||
collection = collection_from_object(partial) || collection_from_options(options)
|
||||
|
||||
if collection
|
||||
# Collection + Derived Partial
|
||||
renderer = CollectionRenderer.new(@lookup_context, options)
|
||||
renderer.render_collection_derive_partial(collection, context, block)
|
||||
else
|
||||
# Object + Derived Partial
|
||||
renderer = ObjectRenderer.new(@lookup_context, options)
|
||||
renderer.render_object_derive_partial(partial, context, block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def collection_from_options(options)
|
||||
if options.key?(:collection)
|
||||
collection = options[:collection]
|
||||
collection || []
|
||||
end
|
||||
end
|
||||
|
||||
def collection_from_object(object)
|
||||
object if object.respond_to?(:to_ary)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "active_record_unit"
|
||||
require "active_record/railties/collection_cache_association_loading"
|
||||
|
||||
ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading)
|
||||
|
||||
class MultifetchCacheTest < ActiveRecordTestCase
|
||||
fixtures :topics, :replies
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "abstract_unit"
|
||||
require "action_view/renderer/partial_renderer"
|
||||
require "action_view/renderer/collection_renderer"
|
||||
|
||||
class PartialIterationTest < ActiveSupport::TestCase
|
||||
def test_has_size_and_index
|
||||
|
|
|
@ -265,14 +265,14 @@ module RenderTestCases
|
|||
end
|
||||
|
||||
def test_render_partial_with_invalid_option_as
|
||||
e = assert_raises(ArgumentError) { @view.render(partial: "test/partial_only", as: "a-in") }
|
||||
e = assert_raises(ArgumentError) { @view.render(partial: "test/partial_only", as: "a-in", object: nil) }
|
||||
assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " \
|
||||
"make sure it starts with lowercase letter, " \
|
||||
"and is followed by any combination of letters, numbers and underscores.", e.message
|
||||
end
|
||||
|
||||
def test_render_partial_with_hyphen_and_invalid_option_as
|
||||
e = assert_raises(ArgumentError) { @view.render(partial: "test/a-in", as: "a-in") }
|
||||
e = assert_raises(ArgumentError) { @view.render(partial: "test/a-in", as: "a-in", object: nil) }
|
||||
assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " \
|
||||
"make sure it starts with lowercase letter, " \
|
||||
"and is followed by any combination of letters, numbers and underscores.", e.message
|
||||
|
|
|
@ -221,13 +221,6 @@ To keep using the current cache store, you can turn off cache versioning entirel
|
|||
end
|
||||
end
|
||||
|
||||
initializer "active_record.collection_cache_association_loading" do
|
||||
require "active_record/railties/collection_cache_association_loading"
|
||||
ActiveSupport.on_load(:action_view) do
|
||||
ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading)
|
||||
end
|
||||
end
|
||||
|
||||
initializer "active_record.set_reloader_hooks" do
|
||||
ActiveSupport.on_load(:active_record) do
|
||||
ActiveSupport::Reloader.before_class_unload do
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActiveRecord
|
||||
module Railties # :nodoc:
|
||||
module CollectionCacheAssociationLoading #:nodoc:
|
||||
def setup(context, options, as, block)
|
||||
@relation = nil
|
||||
|
||||
return super unless options[:cached]
|
||||
|
||||
@relation = relation_from_options(options[:partial], options[:collection])
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def relation_from_options(partial, collection)
|
||||
relation = partial if partial.is_a?(ActiveRecord::Relation)
|
||||
relation ||= collection if collection.is_a?(ActiveRecord::Relation)
|
||||
|
||||
if relation && !relation.loaded?
|
||||
relation.skip_preloading!
|
||||
end
|
||||
end
|
||||
|
||||
def collection_without_template(*)
|
||||
@relation.preload_associations(@collection) if @relation
|
||||
super
|
||||
end
|
||||
|
||||
def collection_with_template(*)
|
||||
@relation.preload_associations(@collection) if @relation
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue