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 :Renderer
|
||||||
autoload :AbstractRenderer
|
autoload :AbstractRenderer
|
||||||
autoload :PartialRenderer
|
autoload :PartialRenderer
|
||||||
|
autoload :CollectionRenderer
|
||||||
|
autoload :ObjectRenderer
|
||||||
autoload :TemplateRenderer
|
autoload :TemplateRenderer
|
||||||
autoload :StreamingTemplateRenderer
|
autoload :StreamingTemplateRenderer
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "concurrent/map"
|
||||||
|
|
||||||
module ActionView
|
module ActionView
|
||||||
# This class defines the interface for a renderer. Each class that
|
# This class defines the interface for a renderer. Each class that
|
||||||
# subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
|
# subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
|
||||||
|
@ -27,6 +29,86 @@ module ActionView
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
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:
|
class RenderedCollection # :nodoc:
|
||||||
def self.empty(format)
|
def self.empty(format)
|
||||||
EmptyCollection.new format
|
EmptyCollection.new format
|
||||||
|
@ -74,12 +156,18 @@ module ActionView
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
NO_DETAILS = {}.freeze
|
||||||
|
|
||||||
def extract_details(options) # :doc:
|
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]
|
value = options[key]
|
||||||
|
|
||||||
details[key] = Array(value) if value
|
if value
|
||||||
|
(details ||= {})[key] = Array(value)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
details || NO_DETAILS
|
||||||
end
|
end
|
||||||
|
|
||||||
def instrument(name, **options) # :doc:
|
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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "concurrent/map"
|
|
||||||
require "action_view/renderer/partial_renderer/collection_caching"
|
require "action_view/renderer/partial_renderer/collection_caching"
|
||||||
|
|
||||||
module ActionView
|
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
|
# = Action View Partials
|
||||||
#
|
#
|
||||||
# There's also a convenience method for rendering sub templates within the current controller that depends on a
|
# 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
|
class PartialRenderer < AbstractRenderer
|
||||||
include CollectionCaching
|
include CollectionCaching
|
||||||
|
|
||||||
PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
|
def initialize(lookup_context, options)
|
||||||
h[k] = Concurrent::Map.new
|
super(lookup_context)
|
||||||
|
@options = options
|
||||||
|
@locals = @options[:locals] || {}
|
||||||
|
@details = extract_details(@options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(*)
|
def render(partial, context, block)
|
||||||
super
|
template = find_template(partial, template_keys(partial))
|
||||||
@context_prefix = @lookup_context.prefixes.first
|
|
||||||
end
|
|
||||||
|
|
||||||
def render(context, options, block)
|
if !block && (layout = @options[:layout])
|
||||||
as = as_variable(options)
|
layout = find_template(layout.to_s, template_keys(partial))
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if @collection
|
render_partial_template(context, @locals, template, layout, block)
|
||||||
render_collection(context, template)
|
|
||||||
else
|
|
||||||
render_partial(context, template)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def render_collection(view, template)
|
def template_keys(_)
|
||||||
identifier = (template && template.identifier) || @path
|
@locals.keys
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_partial(view, template)
|
def render_partial_template(view, locals, template, layout, block)
|
||||||
instrument(:partial, identifier: template.identifier) do |payload|
|
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|
|
content = template.render(view, locals) do |*name|
|
||||||
view._layout_for(*name, &block)
|
view._layout_for(*name, &block)
|
||||||
end
|
end
|
||||||
|
@ -364,195 +288,9 @@ module ActionView
|
||||||
end
|
end
|
||||||
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)
|
def find_template(path, locals)
|
||||||
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
|
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
|
||||||
@lookup_context.find_template(path, prefixes, true, locals, @details)
|
@lookup_context.find_template(path, prefixes, true, locals, @details)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,13 +13,19 @@ module ActionView
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def cache_collection_render(instrumentation_payload, view, template)
|
def will_cache?(options, view)
|
||||||
return yield unless @options[:cached] && view.controller.respond_to?(:perform_caching) && view.controller.perform_caching
|
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
|
# Result is a hash with the key represents the
|
||||||
# key used for cache lookup and the value is the item
|
# key used for cache lookup and the value is the item
|
||||||
# on which the partial is being rendered
|
# 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
|
# Pull all partials from cache
|
||||||
# Result is a hash, key matches the entry in
|
# Result is a hash, key matches the entry in
|
||||||
|
@ -29,17 +35,9 @@ module ActionView
|
||||||
instrumentation_payload[:cache_hits] = cached_partials.size
|
instrumentation_payload[:cache_hits] = cached_partials.size
|
||||||
|
|
||||||
# Extract the items for the keys that are not found
|
# Extract the items for the keys that are not found
|
||||||
# Set the uncached values to instance variable @collection
|
collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
|
||||||
# which is used by the caller
|
|
||||||
@collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
|
|
||||||
|
|
||||||
# If all elements are already in cache then
|
rendered_partials = collection.empty? ? [] : yield(collection_iterator.from_collection(collection))
|
||||||
# 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
|
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
keyed_partials = fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
|
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)
|
@options[:cached].respond_to?(:call)
|
||||||
end
|
end
|
||||||
|
|
||||||
def collection_by_cache_keys(view, template)
|
def collection_by_cache_keys(view, template, collection)
|
||||||
seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
|
seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
|
||||||
|
|
||||||
digest_path = view.digest_path_from_template(template)
|
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)
|
key = expanded_cache_key(seed.call(item), view, template, digest_path)
|
||||||
ordered_keys << key
|
ordered_keys << key
|
||||||
hash[key] = item
|
hash[key] = item
|
||||||
|
|
|
@ -65,7 +65,50 @@ module ActionView
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_partial_to_object(context, options, &block) #:nodoc:
|
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
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "active_record_unit"
|
require "active_record_unit"
|
||||||
require "active_record/railties/collection_cache_association_loading"
|
|
||||||
|
|
||||||
ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading)
|
|
||||||
|
|
||||||
class MultifetchCacheTest < ActiveRecordTestCase
|
class MultifetchCacheTest < ActiveRecordTestCase
|
||||||
fixtures :topics, :replies
|
fixtures :topics, :replies
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "abstract_unit"
|
require "abstract_unit"
|
||||||
require "action_view/renderer/partial_renderer"
|
require "action_view/renderer/collection_renderer"
|
||||||
|
|
||||||
class PartialIterationTest < ActiveSupport::TestCase
|
class PartialIterationTest < ActiveSupport::TestCase
|
||||||
def test_has_size_and_index
|
def test_has_size_and_index
|
||||||
|
|
|
@ -265,14 +265,14 @@ module RenderTestCases
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_render_partial_with_invalid_option_as
|
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; " \
|
assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " \
|
||||||
"make sure it starts with lowercase letter, " \
|
"make sure it starts with lowercase letter, " \
|
||||||
"and is followed by any combination of letters, numbers and underscores.", e.message
|
"and is followed by any combination of letters, numbers and underscores.", e.message
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_render_partial_with_hyphen_and_invalid_option_as
|
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; " \
|
assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " \
|
||||||
"make sure it starts with lowercase letter, " \
|
"make sure it starts with lowercase letter, " \
|
||||||
"and is followed by any combination of letters, numbers and underscores.", e.message
|
"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
|
||||||
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
|
initializer "active_record.set_reloader_hooks" do
|
||||||
ActiveSupport.on_load(:active_record) do
|
ActiveSupport.on_load(:active_record) do
|
||||||
ActiveSupport::Reloader.before_class_unload 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