actionview 6.0.6.1 → 6.1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of actionview might be problematic. Click here for more details.

Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +226 -220
  3. data/MIT-LICENSE +1 -2
  4. data/lib/action_view/base.rb +18 -49
  5. data/lib/action_view/cache_expiry.rb +1 -2
  6. data/lib/action_view/dependency_tracker.rb +10 -4
  7. data/lib/action_view/digestor.rb +3 -2
  8. data/lib/action_view/gem_version.rb +3 -3
  9. data/lib/action_view/helpers/asset_tag_helper.rb +57 -17
  10. data/lib/action_view/helpers/asset_url_helper.rb +6 -4
  11. data/lib/action_view/helpers/atom_feed_helper.rb +2 -1
  12. data/lib/action_view/helpers/cache_helper.rb +10 -16
  13. data/lib/action_view/helpers/date_helper.rb +6 -5
  14. data/lib/action_view/helpers/form_helper.rb +66 -30
  15. data/lib/action_view/helpers/form_options_helper.rb +7 -16
  16. data/lib/action_view/helpers/form_tag_helper.rb +4 -3
  17. data/lib/action_view/helpers/javascript_helper.rb +3 -3
  18. data/lib/action_view/helpers/number_helper.rb +6 -6
  19. data/lib/action_view/helpers/rendering_helper.rb +11 -3
  20. data/lib/action_view/helpers/tag_helper.rb +98 -22
  21. data/lib/action_view/helpers/tags/base.rb +10 -6
  22. data/lib/action_view/helpers/tags/check_box.rb +1 -1
  23. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  24. data/lib/action_view/helpers/tags/date_select.rb +2 -2
  25. data/lib/action_view/helpers/tags/datetime_local_field.rb +1 -1
  26. data/lib/action_view/helpers/tags/hidden_field.rb +4 -0
  27. data/lib/action_view/helpers/tags/label.rb +4 -0
  28. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  29. data/lib/action_view/helpers/tags/select.rb +1 -1
  30. data/lib/action_view/helpers/tags/time_field.rb +1 -1
  31. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  32. data/lib/action_view/helpers/text_helper.rb +2 -2
  33. data/lib/action_view/helpers/translation_helper.rb +88 -50
  34. data/lib/action_view/helpers/url_helper.rb +136 -24
  35. data/lib/action_view/layouts.rb +3 -2
  36. data/lib/action_view/log_subscriber.rb +26 -10
  37. data/lib/action_view/lookup_context.rb +3 -18
  38. data/lib/action_view/path_set.rb +0 -3
  39. data/lib/action_view/railtie.rb +39 -46
  40. data/lib/action_view/renderer/abstract_renderer.rb +93 -14
  41. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  42. data/lib/action_view/renderer/object_renderer.rb +34 -0
  43. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +25 -26
  44. data/lib/action_view/renderer/partial_renderer.rb +20 -282
  45. data/lib/action_view/renderer/renderer.rb +44 -1
  46. data/lib/action_view/renderer/streaming_template_renderer.rb +5 -1
  47. data/lib/action_view/renderer/template_renderer.rb +15 -12
  48. data/lib/action_view/rendering.rb +3 -1
  49. data/lib/action_view/routing_url_for.rb +1 -1
  50. data/lib/action_view/template/handlers/erb/erubi.rb +9 -7
  51. data/lib/action_view/template/handlers/erb.rb +10 -14
  52. data/lib/action_view/template/handlers.rb +0 -26
  53. data/lib/action_view/template/html.rb +1 -11
  54. data/lib/action_view/template/raw_file.rb +0 -3
  55. data/lib/action_view/template/renderable.rb +24 -0
  56. data/lib/action_view/template/resolver.rb +82 -40
  57. data/lib/action_view/template/text.rb +0 -3
  58. data/lib/action_view/template.rb +9 -49
  59. data/lib/action_view/test_case.rb +18 -25
  60. data/lib/action_view/testing/resolvers.rb +10 -31
  61. data/lib/action_view/unbound_template.rb +3 -3
  62. data/lib/action_view/view_paths.rb +34 -36
  63. data/lib/action_view.rb +4 -1
  64. data/lib/assets/compiled/rails-ujs.js +2 -2
  65. metadata +14 -11
@@ -306,7 +306,7 @@ module ActionView
306
306
  RUBY
307
307
  when Proc
308
308
  define_method :_layout_from_proc, &_layout
309
- protected :_layout_from_proc
309
+ private :_layout_from_proc
310
310
  <<-RUBY
311
311
  result = _layout_from_proc(#{_layout.arity == 0 ? '' : 'self'})
312
312
  return #{default_behavior} if result.nil?
@@ -321,6 +321,7 @@ module ActionView
321
321
  end
322
322
 
323
323
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
324
+ # frozen_string_literal: true
324
325
  def _layout(lookup_context, formats)
325
326
  if _conditional_layout?
326
327
  #{layout_definition}
@@ -395,7 +396,7 @@ module ActionView
395
396
  end
396
397
 
397
398
  def _normalize_layout(value)
398
- value.is_a?(String) && value !~ /\blayouts/ ? "layouts/#{value}" : value
399
+ value.is_a?(String) && !value.match?(/\blayouts/) ? "layouts/#{value}" : value
399
400
  end
400
401
 
401
402
  # Returns the default layout for this controller.
@@ -23,7 +23,7 @@ module ActionView
23
23
  end
24
24
 
25
25
  def render_partial(event)
26
- info do
26
+ debug do
27
27
  message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
28
28
  message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
29
29
  message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
@@ -32,19 +32,26 @@ module ActionView
32
32
  end
33
33
  end
34
34
 
35
+ def render_layout(event)
36
+ info do
37
+ message = +" Rendered layout #{from_rails_root(event.payload[:identifier])}"
38
+ message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
39
+ end
40
+ end
41
+
35
42
  def render_collection(event)
36
43
  identifier = event.payload[:identifier] || "templates"
37
44
 
38
- info do
39
- " Rendered collection of #{from_rails_root(identifier)}" \
40
- " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
45
+ debug do
46
+ message = +" Rendered collection of #{from_rails_root(identifier)}"
47
+ message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
48
+ message << " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
49
+ message
41
50
  end
42
51
  end
43
52
 
44
53
  def start(name, id, payload)
45
- if name == "render_template.action_view"
46
- log_rendering_start(payload)
47
- end
54
+ log_rendering_start(payload, name)
48
55
 
49
56
  super
50
57
  end
@@ -82,9 +89,18 @@ module ActionView
82
89
  end
83
90
  end
84
91
 
85
- def log_rendering_start(payload)
86
- info do
87
- message = +" Rendering #{from_rails_root(payload[:identifier])}"
92
+ def log_rendering_start(payload, name)
93
+ debug do
94
+ qualifier =
95
+ if name == "render_template.action_view"
96
+ ""
97
+ elsif name == "render_layout.action_view"
98
+ "layout "
99
+ end
100
+
101
+ return unless qualifier
102
+
103
+ message = +" Rendering #{qualifier}#{from_rails_root(payload[:identifier])}"
88
104
  message << " within #{from_rails_root(payload[:layout])}" if payload[:layout]
89
105
  message
90
106
  end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "concurrent/map"
4
- require "active_support/core_ext/module/remove_method"
5
4
  require "active_support/core_ext/module/attribute_accessors"
6
- require "active_support/deprecation"
7
5
  require "action_view/template/resolver"
8
6
 
9
7
  module ActionView
@@ -16,8 +14,6 @@ module ActionView
16
14
  # only once during the request, it speeds up all cache accesses.
17
15
  class LookupContext #:nodoc:
18
16
  attr_accessor :prefixes, :rendered_format
19
- deprecate :rendered_format
20
- deprecate :rendered_format=
21
17
 
22
18
  mattr_accessor :fallbacks, default: FallbackFileSystemResolver.instances
23
19
 
@@ -30,7 +26,7 @@ module ActionView
30
26
  Accessors.define_method(:"default_#{name}", &block)
31
27
  Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
32
28
  def #{name}
33
- @details.fetch(:#{name}, [])
29
+ @details[:#{name}] || []
34
30
  end
35
31
 
36
32
  def #{name}=(value)
@@ -132,9 +128,6 @@ module ActionView
132
128
  end
133
129
  alias :find_template :find
134
130
 
135
- alias :find_file :find
136
- deprecate :find_file
137
-
138
131
  def find_all(name, prefixes = [], partial = false, keys = [], options = {})
139
132
  @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
140
133
  end
@@ -155,18 +148,10 @@ module ActionView
155
148
  view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq)
156
149
 
157
150
  if block_given?
158
- ActiveSupport::Deprecation.warn <<~eowarn.squish
159
- Calling `with_fallbacks` with a block is deprecated. Call methods on
151
+ raise ArgumentError, <<~eowarn.squish
152
+ Calling `with_fallbacks` with a block is not supported. Call methods on
160
153
  the lookup context returned by `with_fallbacks` instead.
161
154
  eowarn
162
-
163
- begin
164
- _view_paths = @view_paths
165
- @view_paths = view_paths
166
- yield
167
- ensure
168
- @view_paths = _view_paths
169
- end
170
155
  else
171
156
  ActionView::LookupContext.new(view_paths, @details, @prefixes)
172
157
  end
@@ -48,9 +48,6 @@ module ActionView #:nodoc:
48
48
  find_all(*args).first || raise(MissingTemplate.new(self, *args))
49
49
  end
50
50
 
51
- alias :find_file :find
52
- deprecate :find_file
53
-
54
51
  def find_all(path, prefixes = [], *args)
55
52
  _find_all path, prefixes, args
56
53
  end
@@ -6,70 +6,59 @@ require "rails"
6
6
  module ActionView
7
7
  # = Action View Railtie
8
8
  class Railtie < Rails::Engine # :nodoc:
9
- NULL_OPTION = Object.new
10
-
11
9
  config.action_view = ActiveSupport::OrderedOptions.new
12
10
  config.action_view.embed_authenticity_token_in_remote_forms = nil
13
11
  config.action_view.debug_missing_translation = true
14
12
  config.action_view.default_enforce_utf8 = nil
15
- config.action_view.finalize_compiled_template_methods = NULL_OPTION
16
13
 
17
14
  config.eager_load_namespaces << ActionView
18
15
 
19
- initializer "action_view.embed_authenticity_token_in_remote_forms" do |app|
20
- ActiveSupport.on_load(:action_view) do
21
- ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms =
22
- app.config.action_view.delete(:embed_authenticity_token_in_remote_forms)
23
- end
16
+ config.after_initialize do |app|
17
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms =
18
+ app.config.action_view.delete(:embed_authenticity_token_in_remote_forms)
24
19
  end
25
20
 
26
- initializer "action_view.form_with_generates_remote_forms" do |app|
27
- ActiveSupport.on_load(:action_view) do
28
- form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
29
- ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
30
- end
21
+ config.after_initialize do |app|
22
+ form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
23
+ ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
31
24
  end
32
25
 
33
- initializer "action_view.form_with_generates_ids" do |app|
34
- ActiveSupport.on_load(:action_view) do
35
- form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids)
36
- unless form_with_generates_ids.nil?
37
- ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids
38
- end
26
+ config.after_initialize do |app|
27
+ form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids)
28
+ unless form_with_generates_ids.nil?
29
+ ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids
39
30
  end
40
31
  end
41
32
 
42
- initializer "action_view.default_enforce_utf8" do |app|
43
- ActiveSupport.on_load(:action_view) do
44
- default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8)
45
- unless default_enforce_utf8.nil?
46
- ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8
47
- end
33
+ config.after_initialize do |app|
34
+ default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8)
35
+ unless default_enforce_utf8.nil?
36
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8
48
37
  end
49
38
  end
50
39
 
51
- initializer "action_view.finalize_compiled_template_methods" do |app|
52
- ActiveSupport.on_load(:action_view) do
53
- option = app.config.action_view.delete(:finalize_compiled_template_methods)
54
-
55
- if option != NULL_OPTION
56
- ActiveSupport::Deprecation.warn "action_view.finalize_compiled_template_methods is deprecated and has no effect"
57
- end
58
- end
40
+ config.after_initialize do |app|
41
+ ActionView::Helpers::AssetTagHelper.preload_links_header = app.config.action_view.delete(:preload_links_header)
59
42
  end
60
43
 
61
- initializer "action_view.logger" do
62
- ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
63
- end
64
-
65
- initializer "action_view.set_configs" do |app|
44
+ config.after_initialize do |app|
66
45
  ActiveSupport.on_load(:action_view) do
67
46
  app.config.action_view.each do |k, v|
47
+ if k == :raise_on_missing_translations
48
+ ActiveSupport::Deprecation.warn \
49
+ "action_view.raise_on_missing_translations is deprecated and will be removed in Rails 7.0. " \
50
+ "Set i18n.raise_on_missing_translations instead. " \
51
+ "Note that this new setting also affects how missing translations are handled in controllers."
52
+ end
68
53
  send "#{k}=", v
69
54
  end
70
55
  end
71
56
  end
72
57
 
58
+ initializer "action_view.logger" do
59
+ ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
60
+ end
61
+
73
62
  initializer "action_view.caching" do |app|
74
63
  ActiveSupport.on_load(:action_view) do
75
64
  if app.config.action_view.cache_template_loading.nil?
@@ -78,14 +67,6 @@ module ActionView
78
67
  end
79
68
  end
80
69
 
81
- initializer "action_view.per_request_digest_cache" do |app|
82
- ActiveSupport.on_load(:action_view) do
83
- unless ActionView::Resolver.caching?
84
- app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
85
- end
86
- end
87
- end
88
-
89
70
  initializer "action_view.setup_action_pack" do |app|
90
71
  ActiveSupport.on_load(:action_controller) do
91
72
  ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
@@ -96,6 +77,18 @@ module ActionView
96
77
  PartialRenderer.collection_cache = app.config.action_controller.cache_store
97
78
  end
98
79
 
80
+ config.after_initialize do |app|
81
+ enable_caching = if app.config.action_view.cache_template_loading.nil?
82
+ app.config.cache_classes
83
+ else
84
+ app.config.action_view.cache_template_loading
85
+ end
86
+
87
+ unless enable_caching
88
+ app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
89
+ end
90
+ end
91
+
99
92
  rake_tasks do |app|
100
93
  unless app.config.api_only
101
94
  load "action_view/tasks/cache_digests.rake"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/map"
4
+
3
5
  module ActionView
4
6
  # This class defines the interface for a renderer. Each class that
5
7
  # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
@@ -14,7 +16,7 @@ module ActionView
14
16
  #
15
17
  # Whenever the +render+ method is called on the base +Renderer+ class, a new
16
18
  # renderer object of the correct type is created, and the +render+ method on
17
- # that new object is called in turn. This abstracts the setup and rendering
19
+ # that new object is called in turn. This abstracts the set up and rendering
18
20
  # into a separate classes for partials and templates.
19
21
  class AbstractRenderer #:nodoc:
20
22
  delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
@@ -27,6 +29,84 @@ module ActionView
27
29
  raise NotImplementedError
28
30
  end
29
31
 
32
+ module ObjectRendering # :nodoc:
33
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
34
+ h[k] = Concurrent::Map.new
35
+ end
36
+
37
+ def initialize(lookup_context, options)
38
+ super
39
+ @context_prefix = lookup_context.prefixes.first
40
+ end
41
+
42
+ private
43
+ def local_variable(path)
44
+ if as = @options[:as]
45
+ raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
46
+ as.to_sym
47
+ else
48
+ base = path.end_with?("/") ? "" : File.basename(path)
49
+ raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
50
+ $1.to_sym
51
+ end
52
+ end
53
+
54
+ IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
55
+ "make sure your partial name starts with underscore."
56
+
57
+ OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
58
+ "make sure it starts with lowercase letter, " \
59
+ "and is followed by any combination of letters, numbers and underscores."
60
+
61
+ def raise_invalid_identifier(path)
62
+ raise ArgumentError, IDENTIFIER_ERROR_MESSAGE % path
63
+ end
64
+
65
+ def raise_invalid_option_as(as)
66
+ raise ArgumentError, OPTION_AS_ERROR_MESSAGE % as
67
+ end
68
+
69
+ # Obtains the path to where the object's partial is located. If the object
70
+ # responds to +to_partial_path+, then +to_partial_path+ will be called and
71
+ # will provide the path. If the object does not respond to +to_partial_path+,
72
+ # then an +ArgumentError+ is raised.
73
+ #
74
+ # If +prefix_partial_path_with_controller_namespace+ is true, then this
75
+ # method will prefix the partial paths with a namespace.
76
+ def partial_path(object, view)
77
+ object = object.to_model if object.respond_to?(:to_model)
78
+
79
+ path = if object.respond_to?(:to_partial_path)
80
+ object.to_partial_path
81
+ else
82
+ raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
83
+ end
84
+
85
+ if view.prefix_partial_path_with_controller_namespace
86
+ PREFIXED_PARTIAL_NAMES[@context_prefix][path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
87
+ else
88
+ path
89
+ end
90
+ end
91
+
92
+ def merge_prefix_into_object_path(prefix, object_path)
93
+ if prefix.include?(?/) && object_path.include?(?/)
94
+ prefixes = []
95
+ prefix_array = File.dirname(prefix).split("/")
96
+ object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
97
+
98
+ prefix_array.each_with_index do |dir, index|
99
+ break if dir == object_path_array[index]
100
+ prefixes << dir
101
+ end
102
+
103
+ (prefixes << object_path).join("/")
104
+ else
105
+ object_path
106
+ end
107
+ end
108
+ end
109
+
30
110
  class RenderedCollection # :nodoc:
31
111
  def self.empty(format)
32
112
  EmptyCollection.new format
@@ -59,11 +139,10 @@ module ActionView
59
139
  end
60
140
 
61
141
  class RenderedTemplate # :nodoc:
62
- attr_reader :body, :layout, :template
142
+ attr_reader :body, :template
63
143
 
64
- def initialize(body, layout, template)
144
+ def initialize(body, template)
65
145
  @body = body
66
- @layout = layout
67
146
  @template = template
68
147
  end
69
148
 
@@ -75,18 +154,18 @@ module ActionView
75
154
  end
76
155
 
77
156
  private
157
+ NO_DETAILS = {}.freeze
158
+
78
159
  def extract_details(options) # :doc:
79
- @lookup_context.registered_details.each_with_object({}) do |key, details|
160
+ details = nil
161
+ @lookup_context.registered_details.each do |key|
80
162
  value = options[key]
81
163
 
82
- details[key] = Array(value) if value
83
- end
84
- end
85
-
86
- def instrument(name, **options) # :doc:
87
- ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
88
- yield payload
164
+ if value
165
+ (details ||= {})[key] = Array(value)
166
+ end
89
167
  end
168
+ details || NO_DETAILS
90
169
  end
91
170
 
92
171
  def prepend_formats(formats) # :doc:
@@ -96,8 +175,8 @@ module ActionView
96
175
  @lookup_context.formats = formats | @lookup_context.formats
97
176
  end
98
177
 
99
- def build_rendered_template(content, template, layout = nil)
100
- RenderedTemplate.new content, layout, template
178
+ def build_rendered_template(content, template)
179
+ RenderedTemplate.new content, template
101
180
  end
102
181
 
103
182
  def build_rendered_collection(templates, spacer)
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/renderer/partial_renderer"
4
+
5
+ module ActionView
6
+ class PartialIteration
7
+ # The number of iterations that will be done by the partial.
8
+ attr_reader :size
9
+
10
+ # The current iteration of the partial.
11
+ attr_reader :index
12
+
13
+ def initialize(size)
14
+ @size = size
15
+ @index = 0
16
+ end
17
+
18
+ # Check if this is the first iteration of the partial.
19
+ def first?
20
+ index == 0
21
+ end
22
+
23
+ # Check if this is the last iteration of the partial.
24
+ def last?
25
+ index == size - 1
26
+ end
27
+
28
+ def iterate! # :nodoc:
29
+ @index += 1
30
+ end
31
+ end
32
+
33
+ class CollectionRenderer < PartialRenderer # :nodoc:
34
+ include ObjectRendering
35
+
36
+ class CollectionIterator # :nodoc:
37
+ include Enumerable
38
+
39
+ def initialize(collection)
40
+ @collection = collection
41
+ end
42
+
43
+ def each(&blk)
44
+ @collection.each(&blk)
45
+ end
46
+
47
+ def size
48
+ @collection.size
49
+ end
50
+
51
+ def length
52
+ @collection.respond_to?(:length) ? @collection.length : size
53
+ end
54
+ end
55
+
56
+ class SameCollectionIterator < CollectionIterator # :nodoc:
57
+ def initialize(collection, path, variables)
58
+ super(collection)
59
+ @path = path
60
+ @variables = variables
61
+ end
62
+
63
+ def from_collection(collection)
64
+ self.class.new(collection, @path, @variables)
65
+ end
66
+
67
+ def each_with_info
68
+ return enum_for(:each_with_info) unless block_given?
69
+ variables = [@path] + @variables
70
+ @collection.each { |o| yield(o, variables) }
71
+ end
72
+ end
73
+
74
+ class PreloadCollectionIterator < SameCollectionIterator # :nodoc:
75
+ def initialize(collection, path, variables, relation)
76
+ super(collection, path, variables)
77
+ relation.skip_preloading! unless relation.loaded?
78
+ @relation = relation
79
+ end
80
+
81
+ def from_collection(collection)
82
+ self.class.new(collection, @path, @variables, @relation)
83
+ end
84
+
85
+ def each_with_info
86
+ return super unless block_given?
87
+ @relation.preload_associations(@collection)
88
+ super
89
+ end
90
+ end
91
+
92
+ class MixedCollectionIterator < CollectionIterator # :nodoc:
93
+ def initialize(collection, paths)
94
+ super(collection)
95
+ @paths = paths
96
+ end
97
+
98
+ def each_with_info
99
+ return enum_for(:each_with_info) unless block_given?
100
+ @collection.each_with_index { |o, i| yield(o, @paths[i]) }
101
+ end
102
+ end
103
+
104
+ def render_collection_with_partial(collection, partial, context, block)
105
+ iter_vars = retrieve_variable(partial)
106
+
107
+ collection = if collection.respond_to?(:preload_associations)
108
+ PreloadCollectionIterator.new(collection, partial, iter_vars, collection)
109
+ else
110
+ SameCollectionIterator.new(collection, partial, iter_vars)
111
+ end
112
+
113
+ template = find_template(partial, @locals.keys + iter_vars)
114
+
115
+ layout = if !block && (layout = @options[:layout])
116
+ find_template(layout.to_s, @locals.keys + iter_vars)
117
+ end
118
+
119
+ render_collection(collection, context, partial, template, layout, block)
120
+ end
121
+
122
+ def render_collection_derive_partial(collection, context, block)
123
+ paths = collection.map { |o| partial_path(o, context) }
124
+
125
+ if paths.uniq.length == 1
126
+ # Homogeneous
127
+ render_collection_with_partial(collection, paths.first, context, block)
128
+ else
129
+ if @options[:cached]
130
+ raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
131
+ end
132
+
133
+ paths.map! { |path| retrieve_variable(path).unshift(path) }
134
+ collection = MixedCollectionIterator.new(collection, paths)
135
+ render_collection(collection, context, nil, nil, nil, block)
136
+ end
137
+ end
138
+
139
+ private
140
+ def retrieve_variable(path)
141
+ variable = local_variable(path)
142
+ [variable, :"#{variable}_counter", :"#{variable}_iteration"]
143
+ end
144
+
145
+ def render_collection(collection, view, path, template, layout, block)
146
+ identifier = (template && template.identifier) || path
147
+ ActiveSupport::Notifications.instrument(
148
+ "render_collection.action_view",
149
+ identifier: identifier,
150
+ layout: layout && layout.virtual_path,
151
+ count: collection.length
152
+ ) do |payload|
153
+ spacer = if @options.key?(:spacer_template)
154
+ spacer_template = find_template(@options[:spacer_template], @locals.keys)
155
+ build_rendered_template(spacer_template.render(view, @locals), spacer_template)
156
+ else
157
+ RenderedTemplate::EMPTY_SPACER
158
+ end
159
+
160
+ collection_body = if template
161
+ cache_collection_render(payload, view, template, collection) do |filtered_collection|
162
+ collection_with_template(view, template, layout, filtered_collection)
163
+ end
164
+ else
165
+ collection_with_template(view, nil, layout, collection)
166
+ end
167
+
168
+ return RenderedCollection.empty(@lookup_context.formats.first) if collection_body.empty?
169
+
170
+ build_rendered_collection(collection_body, spacer)
171
+ end
172
+ end
173
+
174
+ def collection_with_template(view, template, layout, collection)
175
+ locals = @locals
176
+ cache = {}
177
+
178
+ partial_iteration = PartialIteration.new(collection.size)
179
+
180
+ collection.each_with_info.map do |object, (path, as, counter, iteration)|
181
+ index = partial_iteration.index
182
+
183
+ locals[as] = object
184
+ locals[counter] = index
185
+ locals[iteration] = partial_iteration
186
+
187
+ _template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
188
+
189
+ content = _template.render(view, locals)
190
+ content = layout.render(view, locals) { content } if layout
191
+ partial_iteration.iterate!
192
+ build_rendered_template(content, _template)
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ class ObjectRenderer < PartialRenderer # :nodoc:
5
+ include ObjectRendering
6
+
7
+ def initialize(lookup_context, options)
8
+ super
9
+ @object = nil
10
+ @local_name = nil
11
+ end
12
+
13
+ def render_object_with_partial(object, partial, context, block)
14
+ @object = object
15
+ @local_name = local_variable(partial)
16
+ render(partial, context, block)
17
+ end
18
+
19
+ def render_object_derive_partial(object, context, block)
20
+ path = partial_path(object, context)
21
+ render_object_with_partial(object, path, context, block)
22
+ end
23
+
24
+ private
25
+ def template_keys(path)
26
+ super + [@local_name]
27
+ end
28
+
29
+ def render_partial_template(view, locals, template, layout, block)
30
+ locals[@local_name || template.variable] = @object
31
+ super(view, locals, template, layout, block)
32
+ end
33
+ end
34
+ end