actionview 6.0.0.beta1 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +206 -119
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -2
  5. data/lib/action_view/base.rb +81 -15
  6. data/lib/action_view/cache_expiry.rb +52 -0
  7. data/lib/action_view/context.rb +0 -6
  8. data/lib/action_view/dependency_tracker.rb +10 -4
  9. data/lib/action_view/digestor.rb +11 -19
  10. data/lib/action_view/flows.rb +0 -1
  11. data/lib/action_view/gem_version.rb +2 -2
  12. data/lib/action_view/helpers/active_model_helper.rb +0 -1
  13. data/lib/action_view/helpers/asset_tag_helper.rb +46 -21
  14. data/lib/action_view/helpers/asset_url_helper.rb +6 -4
  15. data/lib/action_view/helpers/atom_feed_helper.rb +2 -1
  16. data/lib/action_view/helpers/cache_helper.rb +16 -23
  17. data/lib/action_view/helpers/csp_helper.rb +4 -2
  18. data/lib/action_view/helpers/date_helper.rb +5 -6
  19. data/lib/action_view/helpers/form_helper.rb +63 -21
  20. data/lib/action_view/helpers/form_options_helper.rb +10 -18
  21. data/lib/action_view/helpers/form_tag_helper.rb +12 -9
  22. data/lib/action_view/helpers/javascript_helper.rb +7 -5
  23. data/lib/action_view/helpers/number_helper.rb +9 -8
  24. data/lib/action_view/helpers/output_safety_helper.rb +1 -1
  25. data/lib/action_view/helpers/rendering_helper.rb +17 -7
  26. data/lib/action_view/helpers/sanitize_helper.rb +10 -16
  27. data/lib/action_view/helpers/tag_helper.rb +94 -19
  28. data/lib/action_view/helpers/tags/base.rb +10 -7
  29. data/lib/action_view/helpers/tags/check_box.rb +0 -1
  30. data/lib/action_view/helpers/tags/collection_check_boxes.rb +0 -1
  31. data/lib/action_view/helpers/tags/collection_helpers.rb +0 -1
  32. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +0 -1
  33. data/lib/action_view/helpers/tags/color_field.rb +0 -1
  34. data/lib/action_view/helpers/tags/date_field.rb +1 -2
  35. data/lib/action_view/helpers/tags/date_select.rb +2 -3
  36. data/lib/action_view/helpers/tags/datetime_field.rb +0 -1
  37. data/lib/action_view/helpers/tags/datetime_local_field.rb +1 -2
  38. data/lib/action_view/helpers/tags/label.rb +4 -1
  39. data/lib/action_view/helpers/tags/month_field.rb +1 -2
  40. data/lib/action_view/helpers/tags/radio_button.rb +0 -1
  41. data/lib/action_view/helpers/tags/select.rb +1 -2
  42. data/lib/action_view/helpers/tags/text_field.rb +0 -1
  43. data/lib/action_view/helpers/tags/time_field.rb +1 -2
  44. data/lib/action_view/helpers/tags/week_field.rb +1 -2
  45. data/lib/action_view/helpers/text_helper.rb +1 -2
  46. data/lib/action_view/helpers/translation_helper.rb +99 -54
  47. data/lib/action_view/helpers/url_helper.rb +109 -15
  48. data/lib/action_view/layouts.rb +8 -10
  49. data/lib/action_view/log_subscriber.rb +26 -11
  50. data/lib/action_view/lookup_context.rb +59 -31
  51. data/lib/action_view/path_set.rb +3 -12
  52. data/lib/action_view/railtie.rb +36 -42
  53. data/lib/action_view/record_identifier.rb +0 -1
  54. data/lib/action_view/renderer/abstract_renderer.rb +142 -11
  55. data/lib/action_view/renderer/collection_renderer.rb +192 -0
  56. data/lib/action_view/renderer/object_renderer.rb +34 -0
  57. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +35 -29
  58. data/lib/action_view/renderer/partial_renderer.rb +21 -273
  59. data/lib/action_view/renderer/renderer.rb +59 -4
  60. data/lib/action_view/renderer/streaming_template_renderer.rb +9 -7
  61. data/lib/action_view/renderer/template_renderer.rb +35 -27
  62. data/lib/action_view/rendering.rb +49 -29
  63. data/lib/action_view/routing_url_for.rb +1 -1
  64. data/lib/action_view/template/error.rb +30 -15
  65. data/lib/action_view/template/handlers/builder.rb +2 -2
  66. data/lib/action_view/template/handlers/erb/erubi.rb +15 -9
  67. data/lib/action_view/template/handlers/erb.rb +14 -19
  68. data/lib/action_view/template/handlers/html.rb +1 -1
  69. data/lib/action_view/template/handlers/raw.rb +2 -2
  70. data/lib/action_view/template/handlers.rb +1 -1
  71. data/lib/action_view/template/html.rb +5 -6
  72. data/lib/action_view/template/inline.rb +22 -0
  73. data/lib/action_view/template/raw_file.rb +25 -0
  74. data/lib/action_view/template/renderable.rb +24 -0
  75. data/lib/action_view/template/resolver.rb +141 -140
  76. data/lib/action_view/template/sources/file.rb +17 -0
  77. data/lib/action_view/template/sources.rb +13 -0
  78. data/lib/action_view/template/text.rb +2 -3
  79. data/lib/action_view/template.rb +49 -75
  80. data/lib/action_view/test_case.rb +20 -28
  81. data/lib/action_view/testing/resolvers.rb +18 -27
  82. data/lib/action_view/unbound_template.rb +31 -0
  83. data/lib/action_view/view_paths.rb +59 -38
  84. data/lib/action_view.rb +7 -2
  85. data/lib/assets/compiled/rails-ujs.js +22 -13
  86. metadata +30 -18
@@ -10,46 +10,44 @@ module ActionView
10
10
  config.action_view.embed_authenticity_token_in_remote_forms = nil
11
11
  config.action_view.debug_missing_translation = true
12
12
  config.action_view.default_enforce_utf8 = nil
13
- config.action_view.finalize_compiled_template_methods = true
14
13
 
15
14
  config.eager_load_namespaces << ActionView
16
15
 
17
- initializer "action_view.embed_authenticity_token_in_remote_forms" do |app|
18
- ActiveSupport.on_load(:action_view) do
19
- ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms =
20
- app.config.action_view.delete(:embed_authenticity_token_in_remote_forms)
21
- 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)
22
19
  end
23
20
 
24
- initializer "action_view.form_with_generates_remote_forms" do |app|
25
- ActiveSupport.on_load(:action_view) do
26
- form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
27
- ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
28
- 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
29
24
  end
30
25
 
31
- initializer "action_view.form_with_generates_ids" do |app|
32
- ActiveSupport.on_load(:action_view) do
33
- form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids)
34
- unless form_with_generates_ids.nil?
35
- ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids
36
- 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
37
30
  end
38
31
  end
39
32
 
40
- initializer "action_view.default_enforce_utf8" do |app|
41
- ActiveSupport.on_load(:action_view) do
42
- default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8)
43
- unless default_enforce_utf8.nil?
44
- ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8
45
- 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
46
37
  end
47
38
  end
48
39
 
49
- initializer "action_view.finalize_compiled_template_methods" do |app|
40
+ config.after_initialize do |app|
50
41
  ActiveSupport.on_load(:action_view) do
51
- ActionView::Template.finalize_compiled_template_methods =
52
- app.config.action_view.delete(:finalize_compiled_template_methods)
42
+ app.config.action_view.each do |k, v|
43
+ if k == :raise_on_missing_translations
44
+ ActiveSupport::Deprecation.warn \
45
+ "action_view.raise_on_missing_translations is deprecated and will be removed in Rails 6.2. " \
46
+ "Set i18n.raise_on_missing_translations instead. " \
47
+ "Note that this new setting also affects how missing translations are handled in controllers."
48
+ end
49
+ send "#{k}=", v
50
+ end
53
51
  end
54
52
  end
55
53
 
@@ -57,14 +55,6 @@ module ActionView
57
55
  ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
58
56
  end
59
57
 
60
- initializer "action_view.set_configs" do |app|
61
- ActiveSupport.on_load(:action_view) do
62
- app.config.action_view.each do |k, v|
63
- send "#{k}=", v
64
- end
65
- end
66
- end
67
-
68
58
  initializer "action_view.caching" do |app|
69
59
  ActiveSupport.on_load(:action_view) do
70
60
  if app.config.action_view.cache_template_loading.nil?
@@ -73,14 +63,6 @@ module ActionView
73
63
  end
74
64
  end
75
65
 
76
- initializer "action_view.per_request_digest_cache" do |app|
77
- ActiveSupport.on_load(:action_view) do
78
- unless ActionView::Resolver.caching?
79
- app.executor.to_run ActionView::Digestor::PerExecutionDigestCacheExpiry
80
- end
81
- end
82
- end
83
-
84
66
  initializer "action_view.setup_action_pack" do |app|
85
67
  ActiveSupport.on_load(:action_controller) do
86
68
  ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
@@ -91,6 +73,18 @@ module ActionView
91
73
  PartialRenderer.collection_cache = app.config.action_controller.cache_store
92
74
  end
93
75
 
76
+ config.after_initialize do |app|
77
+ enable_caching = if app.config.action_view.cache_template_loading.nil?
78
+ app.config.cache_classes
79
+ else
80
+ app.config.action_view.cache_template_loading
81
+ end
82
+
83
+ unless enable_caching
84
+ app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
85
+ end
86
+ end
87
+
94
88
  rake_tasks do |app|
95
89
  unless app.config.api_only
96
90
  load "action_view/tasks/cache_digests.rake"
@@ -95,7 +95,6 @@ module ActionView
95
95
  end
96
96
 
97
97
  private
98
-
99
98
  # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
100
99
  # This can be overwritten to customize the default generated string representation if desired.
101
100
  # If you need to read back a key from a dom_id in order to query for the underlying database record,
@@ -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,10 +16,10 @@ 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
- delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context
22
+ delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
21
23
 
22
24
  def initialize(lookup_context)
23
25
  @lookup_context = lookup_context
@@ -27,22 +29,143 @@ module ActionView
27
29
  raise NotImplementedError
28
30
  end
29
31
 
30
- private
32
+ module ObjectRendering # :nodoc:
33
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
34
+ h[k] = Concurrent::Map.new
35
+ end
31
36
 
32
- def extract_details(options) # :doc:
33
- @lookup_context.registered_details.each_with_object({}) do |key, details|
34
- value = options[key]
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
+
110
+ class RenderedCollection # :nodoc:
111
+ def self.empty(format)
112
+ EmptyCollection.new format
113
+ end
35
114
 
36
- details[key] = Array(value) if value
115
+ attr_reader :rendered_templates
116
+
117
+ def initialize(rendered_templates, spacer)
118
+ @rendered_templates = rendered_templates
119
+ @spacer = spacer
120
+ end
121
+
122
+ def body
123
+ @rendered_templates.map(&:body).join(@spacer.body).html_safe
124
+ end
125
+
126
+ def format
127
+ rendered_templates.first.format
128
+ end
129
+
130
+ class EmptyCollection
131
+ attr_reader :format
132
+
133
+ def initialize(format)
134
+ @format = format
37
135
  end
136
+
137
+ def body; nil; end
38
138
  end
139
+ end
39
140
 
40
- def instrument(name, **options) # :doc:
41
- options[:identifier] ||= (@template && @template.identifier) || @path
141
+ class RenderedTemplate # :nodoc:
142
+ attr_reader :body, :template
42
143
 
43
- ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
44
- yield payload
144
+ def initialize(body, template)
145
+ @body = body
146
+ @template = template
147
+ end
148
+
149
+ def format
150
+ template.format
151
+ end
152
+
153
+ EMPTY_SPACER = Struct.new(:body).new
154
+ end
155
+
156
+ private
157
+ NO_DETAILS = {}.freeze
158
+
159
+ def extract_details(options) # :doc:
160
+ details = nil
161
+ @lookup_context.registered_details.each do |key|
162
+ value = options[key]
163
+
164
+ if value
165
+ (details ||= {})[key] = Array(value)
166
+ end
45
167
  end
168
+ details || NO_DETAILS
46
169
  end
47
170
 
48
171
  def prepend_formats(formats) # :doc:
@@ -51,5 +174,13 @@ module ActionView
51
174
 
52
175
  @lookup_context.formats = formats | @lookup_context.formats
53
176
  end
177
+
178
+ def build_rendered_template(content, template)
179
+ RenderedTemplate.new content, template
180
+ end
181
+
182
+ def build_rendered_collection(templates, spacer)
183
+ RenderedCollection.new templates, spacer
184
+ end
54
185
  end
55
186
  end
@@ -0,0 +1,192 @@
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
+ end
51
+
52
+ class SameCollectionIterator < CollectionIterator # :nodoc:
53
+ def initialize(collection, path, variables)
54
+ super(collection)
55
+ @path = path
56
+ @variables = variables
57
+ end
58
+
59
+ def from_collection(collection)
60
+ self.class.new(collection, @path, @variables)
61
+ end
62
+
63
+ def each_with_info
64
+ return enum_for(:each_with_info) unless block_given?
65
+ variables = [@path] + @variables
66
+ @collection.each { |o| yield(o, variables) }
67
+ end
68
+ end
69
+
70
+ class PreloadCollectionIterator < SameCollectionIterator # :nodoc:
71
+ def initialize(collection, path, variables, relation)
72
+ super(collection, path, variables)
73
+ relation.skip_preloading! unless relation.loaded?
74
+ @relation = relation
75
+ end
76
+
77
+ def from_collection(collection)
78
+ self.class.new(collection, @path, @variables, @relation)
79
+ end
80
+
81
+ def each_with_info
82
+ return super unless block_given?
83
+ @relation.preload_associations(@collection)
84
+ super
85
+ end
86
+ end
87
+
88
+ class MixedCollectionIterator < CollectionIterator # :nodoc:
89
+ def initialize(collection, paths)
90
+ super(collection)
91
+ @paths = paths
92
+ end
93
+
94
+ def each_with_info
95
+ return enum_for(:each_with_info) unless block_given?
96
+ @collection.each_with_index { |o, i| yield(o, @paths[i]) }
97
+ end
98
+ end
99
+
100
+ def render_collection_with_partial(collection, partial, context, block)
101
+ iter_vars = retrieve_variable(partial)
102
+
103
+ collection = if collection.respond_to?(:preload_associations)
104
+ PreloadCollectionIterator.new(collection, partial, iter_vars, collection)
105
+ else
106
+ SameCollectionIterator.new(collection, partial, iter_vars)
107
+ end
108
+
109
+ template = find_template(partial, @locals.keys + iter_vars)
110
+
111
+ layout = if !block && (layout = @options[:layout])
112
+ find_template(layout.to_s, @locals.keys + iter_vars)
113
+ end
114
+
115
+ render_collection(collection, context, partial, template, layout, block)
116
+ end
117
+
118
+ def render_collection_derive_partial(collection, context, block)
119
+ paths = collection.map { |o| partial_path(o, context) }
120
+
121
+ if paths.uniq.length == 1
122
+ # Homogeneous
123
+ render_collection_with_partial(collection, paths.first, context, block)
124
+ else
125
+ if @options[:cached]
126
+ raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
127
+ end
128
+
129
+ paths.map! { |path| retrieve_variable(path).unshift(path) }
130
+ collection = MixedCollectionIterator.new(collection, paths)
131
+ render_collection(collection, context, nil, nil, nil, block)
132
+ end
133
+ end
134
+
135
+ private
136
+ def retrieve_variable(path)
137
+ variable = local_variable(path)
138
+ [variable, :"#{variable}_counter", :"#{variable}_iteration"]
139
+ end
140
+
141
+ def render_collection(collection, view, path, template, layout, block)
142
+ identifier = (template && template.identifier) || path
143
+ ActiveSupport::Notifications.instrument(
144
+ "render_collection.action_view",
145
+ identifier: identifier,
146
+ layout: layout && layout.virtual_path,
147
+ count: collection.size
148
+ ) do |payload|
149
+ spacer = if @options.key?(:spacer_template)
150
+ spacer_template = find_template(@options[:spacer_template], @locals.keys)
151
+ build_rendered_template(spacer_template.render(view, @locals), spacer_template)
152
+ else
153
+ RenderedTemplate::EMPTY_SPACER
154
+ end
155
+
156
+ collection_body = if template
157
+ cache_collection_render(payload, view, template, collection) do |filtered_collection|
158
+ collection_with_template(view, template, layout, filtered_collection)
159
+ end
160
+ else
161
+ collection_with_template(view, nil, layout, collection)
162
+ end
163
+
164
+ return RenderedCollection.empty(@lookup_context.formats.first) if collection_body.empty?
165
+
166
+ build_rendered_collection(collection_body, spacer)
167
+ end
168
+ end
169
+
170
+ def collection_with_template(view, template, layout, collection)
171
+ locals = @locals
172
+ cache = {}
173
+
174
+ partial_iteration = PartialIteration.new(collection.size)
175
+
176
+ collection.each_with_info.map do |object, (path, as, counter, iteration)|
177
+ index = partial_iteration.index
178
+
179
+ locals[as] = object
180
+ locals[counter] = index
181
+ locals[iteration] = partial_iteration
182
+
183
+ _template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
184
+
185
+ content = _template.render(view, locals)
186
+ content = layout.render(view, locals) { content } if layout
187
+ partial_iteration.iterate!
188
+ build_rendered_template(content, _template)
189
+ end
190
+ end
191
+ end
192
+ 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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/enumerable"
4
+
3
5
  module ActionView
4
6
  module CollectionCaching # :nodoc:
5
7
  extend ActiveSupport::Concern
@@ -11,63 +13,65 @@ module ActionView
11
13
  end
12
14
 
13
15
  private
14
- def cache_collection_render(instrumentation_payload)
15
- return yield unless @options[:cached]
16
+ def will_cache?(options, view)
17
+ options[:cached] && view.controller.respond_to?(:perform_caching) && view.controller.perform_caching
18
+ end
19
+
20
+ def cache_collection_render(instrumentation_payload, view, template, collection)
21
+ return yield(collection) unless will_cache?(@options, view)
22
+
23
+ collection_iterator = collection
16
24
 
17
25
  # Result is a hash with the key represents the
18
26
  # key used for cache lookup and the value is the item
19
27
  # on which the partial is being rendered
20
- keyed_collection = collection_by_cache_keys
28
+ keyed_collection, ordered_keys = collection_by_cache_keys(view, template, collection)
21
29
 
22
30
  # Pull all partials from cache
23
31
  # Result is a hash, key matches the entry in
24
32
  # `keyed_collection` where the cache was retrieved and the
25
33
  # value is the value that was present in the cache
26
- cached_partials = collection_cache.read_multi(*keyed_collection.keys)
34
+ cached_partials = collection_cache.read_multi(*keyed_collection.keys)
27
35
  instrumentation_payload[:cache_hits] = cached_partials.size
28
36
 
29
37
  # Extract the items for the keys that are not found
30
- # Set the uncached values to instance variable @collection
31
- # which is used by the caller
32
- @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
33
-
34
- # If all elements are already in cache then
35
- # rendered partials will be an empty array
36
- #
37
- # If the cache is missing elements then
38
- # the block will be called against the remaining items
39
- # in the @collection.
40
- rendered_partials = @collection.empty? ? [] : yield
38
+ collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
39
+
40
+ rendered_partials = collection.empty? ? [] : yield(collection_iterator.from_collection(collection))
41
41
 
42
42
  index = 0
43
- fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do
43
+ keyed_partials = fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
44
44
  # This block is called once
45
45
  # for every cache miss while preserving order.
46
46
  rendered_partials[index].tap { index += 1 }
47
47
  end
48
+
49
+ ordered_keys.map do |key|
50
+ keyed_partials[key]
51
+ end
48
52
  end
49
53
 
50
54
  def callable_cache_key?
51
55
  @options[:cached].respond_to?(:call)
52
56
  end
53
57
 
54
- def collection_by_cache_keys
58
+ def collection_by_cache_keys(view, template, collection)
55
59
  seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
56
60
 
57
- @collection.each_with_object({}) do |item, hash|
58
- hash[expanded_cache_key(seed.call(item))] = item
61
+ digest_path = view.digest_path_from_template(template)
62
+
63
+ collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
64
+ key = expanded_cache_key(seed.call(item), view, template, digest_path)
65
+ ordered_keys << key
66
+ hash[key] = item
59
67
  end
60
68
  end
61
69
 
62
- def expanded_cache_key(key)
63
- key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path, digest_path: digest_path))
70
+ def expanded_cache_key(key, view, template, digest_path)
71
+ key = view.combined_fragment_cache_key(view.cache_fragment_name(key, digest_path: digest_path))
64
72
  key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
65
73
  end
66
74
 
67
- def digest_path
68
- @digest_path ||= @view.digest_path_from_virtual(@template.virtual_path)
69
- end
70
-
71
75
  # `order_by` is an enumerable object containing keys of the cache,
72
76
  # all keys are passed in whether found already or not.
73
77
  #
@@ -83,11 +87,13 @@ module ActionView
83
87
  #
84
88
  # If the partial is not already cached it will also be
85
89
  # written back to the underlying cache store.
86
- def fetch_or_cache_partial(cached_partials, order_by:)
87
- order_by.map do |cache_key|
88
- cached_partials.fetch(cache_key) do
90
+ def fetch_or_cache_partial(cached_partials, template, order_by:)
91
+ order_by.index_with do |cache_key|
92
+ if content = cached_partials[cache_key]
93
+ build_rendered_template(content, template)
94
+ else
89
95
  yield.tap do |rendered_partial|
90
- collection_cache.write(cache_key, rendered_partial)
96
+ collection_cache.write(cache_key, rendered_partial.body)
91
97
  end
92
98
  end
93
99
  end